Bug description
In AD_UpdateStates (modules/aerodyn/src/AeroDyn.f90:1879-1891), the inflow that is freshly computed for each BEMT input time is written to m%Inflow(1) and then never read. SetInputs instead reads InflowInterp, which is assigned exactly once before the loop and never updated inside it.
The result is that both BEMT input slots (m%BEMT_u(1) at t and m%BEMT_u(2) at t+dt) receive the identical wind field — the one sampled at utimes(1). The wind seen by the BEMT/DBEMT/UA state march is frozen across the update step.
This is an incomplete refactor rather than a design decision; see Root cause below.
Affected code (dev @ AeroDyn.f90)
1871: call AD_CopyInflowType( m%Inflow(1), InflowInterp, MESH_NEWCOPY, errStat2, errMsg2) ! <-- only write to InflowInterp
1872: if (Failed()) return
...
1877: BEMT_utimes(2) = t+p%DT
1878: BEMT_utimes(1) = t
1879: do i=2,1,-1
1880: call AD_Input_ExtrapInterp(u,utimes,uInterp,BEMT_utimes(i), errStat2, errMsg2)
1881: if (Failed()) return
1882:
1883: ! Calculate wind using uInterp
1884: call AD_CalcWind(utimes(i),uInterp, p%FLowField, p, m, OtherState, m%Inflow(1), ErrStat2, ErrMsg2) ! <-- result discarded
1885: if (Failed()) return
1886:
1887: do iR = 1,size(p%rotors)
1888: call SetInputs(t, p%rotors(iR), p, uInterp%rotors(iR), InflowInterp%RotInflow(iR), m%rotors(iR), i, errStat2, errMsg2) ! <-- reads the stale snapshot
1889: if (Failed()) return
1890: enddo
1891: end do
InflowInterp is referenced in only four places in the file — declaration (:1855), the MESH_NEWCOPY at :1871, the read at :1888, and AD_DestroyInflowType in Cleanup (:1935). AD_CopyInflowType with MESH_NEWCOPY performs a deep copy (it allocates and assigns RotInflow/InflowWakeVel), so InflowInterp does not alias m%Inflow(1) and is genuinely frozen for the duration of the loop.
Three distinct defects in these two statements
-
Stale inflow (primary). SetInputs reads InflowInterp — a snapshot at utimes(1) — for both i = 2 and i = 1. m%BEMT_u(1) and m%BEMT_u(2) therefore differ only in blade-node kinematics (uInterp is interpolated correctly); the wind component is identical.
-
Mismatched time and positions (latent). uInterp is extrapolated to BEMT_utimes(i), but AD_CalcWind is called with utimes(i). Per the comment at :1876 the glue code supplies u(1) at t+dt and u(2) at t, so utimes = [t+dt, t] while BEMT_utimes = [t, t+dt] — the two are reversed. The wind is sampled at the wrong time for the given node positions on both iterations. This is currently masked by defect 1 (the result is thrown away), and would be exposed by a naive fix that only redirects SetInputs to m%Inflow(1).
-
m%Inflow(1) is clobbered. The first loop (:1864-1867) correctly fills m%Inflow(i) to match u(i). The second loop then overwrites m%Inflow(1) twice. On exit, m%Inflow(1) holds wind evaluated at time utimes(1) = t+dt at node positions from t — it no longer corresponds to u(1). Two consumers later in the same routine read it:
SetInputsForAA(p%rotors(iR), u(1)%rotors(iR), m%Inflow(1)%RotInflow(iR), ...) (:1904) — AeroAcoustics is handed u(1) paired with a mismatched inflow.
SetInputsForFVW(p, u(i), i, m, ...) (:1915) for i = 1, which sets m%FVW_u(1)%V_wind = m%Inflow(1)%InflowWakeVel and the lifting-line disturbed inflow used for UA.
m%Inflow(2) is untouched and remains correct. The corruption does not propagate past AD_UpdateStates, because AD_CalcOutput recomputes m%Inflow(1) at its start (:2193).
Root cause
Introduced in commit 8024b9895 ("AD15: calculate wind based on u_interp instead of extrapolating", 2024-03-05). Before that commit InflowInterp was a scratch buffer recomputed every iteration:
!Extrapolate Inflow (should match previous extrapolations)
call AD_InflowType_ExtrapInterp(m%Inflow(1:size(utimes)),utimes,InflowInterp,BEMT_utimes(i), errStat2, errMsg2)
if (Failed()) return
...
call SetInputs(p%rotors(iR), p, uInterp%rotors(iR), InflowInterp%RotInflow(iR), m%rotors(iR), i, errStat2, errMsg2)
The commit replaced the AD_InflowType_ExtrapInterp call with a direct AD_CalcWind writing to m%Inflow(1), but left the SetInputs argument pointing at InflowInterp. The pre-loop AD_CopyInflowType had existed only to allocate InflowInterp as the ExtrapInterp destination; with the extrapolation gone it became the only write, converting a per-iteration scratch buffer into a frozen snapshot. The same commit also switched the time argument from BEMT_utimes(i) to utimes(i), which is defect 2.
To Reproduce
This is a code-inspection finding; no runtime failure or error message is produced. It can be confirmed by inspection of the four InflowInterp references listed above, or at runtime by:
- Building any OpenFAST configuration with
Wake_Mod = 1 or 2 (BEMT/DBEMT) and UA_Mod > 0.
- Running a case with a strongly time-varying inflow — full-field turbulence (
WindType = 3/4), or a steep uniform-wind time series — with a relatively large DT_Aero.
- Breaking in
SetInputs (or printing RotInflow%Blade(1)%InflowVel(:,1)) for i = 2 and i = 1 within the loop at :1879, and observing that the two are bit-identical while uInterp differs.
Expected behavior
m%BEMT_u(1) and m%BEMT_u(2) should carry the wind sampled at BEMT_utimes(1) = t and BEMT_utimes(2) = t+dt respectively, each evaluated at the node positions uInterp holds for that same time; and m%Inflow(1) should still correspond to u(1) when the routine's later consumers (SetInputsForAA, SetInputsForFVW) read it.
Suggested fix
Restore InflowInterp to its original role as the per-iteration scratch buffer. This addresses all three defects in two lines and leaves the pre-loop AD_CopyInflowType (which allocates it) and the Cleanup destroy call meaningful:
! Calculate wind using uInterp
- call AD_CalcWind(utimes(i),uInterp, p%FLowField, p, m, OtherState, m%Inflow(1), ErrStat2, ErrMsg2)
+ call AD_CalcWind(BEMT_utimes(i), uInterp, p%FLowField, p, m, OtherState, InflowInterp, ErrStat2, ErrMsg2)
if (Failed()) return
do iR = 1,size(p%rotors)
call SetInputs(t, p%rotors(iR), p, uInterp%rotors(iR), InflowInterp%RotInflow(iR), m%rotors(iR), i, errStat2, errMsg2)
Writing to InflowInterp rather than m%Inflow(1) also stops the clobbering, so SetInputsForAA and SetInputsForFVW see m%Inflow(i) correctly paired with u(i).
Note that AD_CalcWind fills the whole AD_InflowType (all rotors plus InflowWakeVel for OLAF wake points), so InflowInterp must remain a full AD_InflowType, as it is today.
Expected impact
- No change for steady, uniform, spatially-invariant inflow with small structural motion — the sampled wind is the same at both times, so the three defects cancel.
- Non-zero change wherever the sampled wind varies appreciably over one
DT_Aero: full-field turbulence, sheared inflow combined with blade rotation, large platform/blade motion, or MHK cases with waves. The dynamic-wake (DBEMT) and unsteady-aerofoil (UA) states are marched with dU/dt = 0 across the step, so the dynamic-inflow forcing is under-represented.
- The OLAF path additionally receives a
V_wind/Vwnd_LL at index 1 that does not correspond to u(1).
The numerical magnitude has not been quantified — fixing this will change regression-test baselines for any case with time-varying inflow, so a full r-test sweep with baseline review is warranted.
OpenFAST Version
Present on dev (verified at AeroDyn.f90:1871/1884/1888) and in every release since the offending commit: v4.0.0 – v4.2.1 and v5.0.0. Not present in v3.x.
System Information
Compiler-independent; this is a control-flow defect in the source, not a build or platform issue.
Additional context
Found while reviewing PR #3311 (generalized support structures in AeroDyn). That PR threads a new GSInflow argument through SetInputs, and at :1888 it passes InflowInterp%GSInflow — so the new generalized-support inflow inherits exactly the same staleness. Fixing this issue first would let #3311 pick up the correct behaviour for free; the two changes touch the same two lines.
Bug description
In
AD_UpdateStates(modules/aerodyn/src/AeroDyn.f90:1879-1891), the inflow that is freshly computed for each BEMT input time is written tom%Inflow(1)and then never read.SetInputsinstead readsInflowInterp, which is assigned exactly once before the loop and never updated inside it.The result is that both BEMT input slots (
m%BEMT_u(1)attandm%BEMT_u(2)att+dt) receive the identical wind field — the one sampled atutimes(1). The wind seen by the BEMT/DBEMT/UA state march is frozen across the update step.This is an incomplete refactor rather than a design decision; see Root cause below.
Affected code (
dev@AeroDyn.f90)InflowInterpis referenced in only four places in the file — declaration (:1855), theMESH_NEWCOPYat:1871, the read at:1888, andAD_DestroyInflowTypeinCleanup(:1935).AD_CopyInflowTypewithMESH_NEWCOPYperforms a deep copy (it allocates and assignsRotInflow/InflowWakeVel), soInflowInterpdoes not aliasm%Inflow(1)and is genuinely frozen for the duration of the loop.Three distinct defects in these two statements
Stale inflow (primary).
SetInputsreadsInflowInterp— a snapshot atutimes(1)— for bothi = 2andi = 1.m%BEMT_u(1)andm%BEMT_u(2)therefore differ only in blade-node kinematics (uInterpis interpolated correctly); the wind component is identical.Mismatched time and positions (latent).
uInterpis extrapolated toBEMT_utimes(i), butAD_CalcWindis called withutimes(i). Per the comment at:1876the glue code suppliesu(1)att+dtandu(2)att, soutimes = [t+dt, t]whileBEMT_utimes = [t, t+dt]— the two are reversed. The wind is sampled at the wrong time for the given node positions on both iterations. This is currently masked by defect 1 (the result is thrown away), and would be exposed by a naive fix that only redirectsSetInputstom%Inflow(1).m%Inflow(1)is clobbered. The first loop (:1864-1867) correctly fillsm%Inflow(i)to matchu(i). The second loop then overwritesm%Inflow(1)twice. On exit,m%Inflow(1)holds wind evaluated at timeutimes(1) = t+dtat node positions fromt— it no longer corresponds tou(1). Two consumers later in the same routine read it:SetInputsForAA(p%rotors(iR), u(1)%rotors(iR), m%Inflow(1)%RotInflow(iR), ...)(:1904) — AeroAcoustics is handedu(1)paired with a mismatched inflow.SetInputsForFVW(p, u(i), i, m, ...)(:1915) fori = 1, which setsm%FVW_u(1)%V_wind = m%Inflow(1)%InflowWakeVeland the lifting-line disturbed inflow used for UA.m%Inflow(2)is untouched and remains correct. The corruption does not propagate pastAD_UpdateStates, becauseAD_CalcOutputrecomputesm%Inflow(1)at its start (:2193).Root cause
Introduced in commit
8024b9895("AD15: calculate wind based on u_interp instead of extrapolating", 2024-03-05). Before that commitInflowInterpwas a scratch buffer recomputed every iteration:!Extrapolate Inflow (should match previous extrapolations) call AD_InflowType_ExtrapInterp(m%Inflow(1:size(utimes)),utimes,InflowInterp,BEMT_utimes(i), errStat2, errMsg2) if (Failed()) return ... call SetInputs(p%rotors(iR), p, uInterp%rotors(iR), InflowInterp%RotInflow(iR), m%rotors(iR), i, errStat2, errMsg2)The commit replaced the
AD_InflowType_ExtrapInterpcall with a directAD_CalcWindwriting tom%Inflow(1), but left theSetInputsargument pointing atInflowInterp. The pre-loopAD_CopyInflowTypehad existed only to allocateInflowInterpas theExtrapInterpdestination; with the extrapolation gone it became the only write, converting a per-iteration scratch buffer into a frozen snapshot. The same commit also switched the time argument fromBEMT_utimes(i)toutimes(i), which is defect 2.To Reproduce
This is a code-inspection finding; no runtime failure or error message is produced. It can be confirmed by inspection of the four
InflowInterpreferences listed above, or at runtime by:Wake_Mod = 1or2(BEMT/DBEMT) andUA_Mod > 0.WindType = 3/4), or a steep uniform-wind time series — with a relatively largeDT_Aero.SetInputs(or printingRotInflow%Blade(1)%InflowVel(:,1)) fori = 2andi = 1within the loop at:1879, and observing that the two are bit-identical whileuInterpdiffers.Expected behavior
m%BEMT_u(1)andm%BEMT_u(2)should carry the wind sampled atBEMT_utimes(1) = tandBEMT_utimes(2) = t+dtrespectively, each evaluated at the node positionsuInterpholds for that same time; andm%Inflow(1)should still correspond tou(1)when the routine's later consumers (SetInputsForAA,SetInputsForFVW) read it.Suggested fix
Restore
InflowInterpto its original role as the per-iteration scratch buffer. This addresses all three defects in two lines and leaves the pre-loopAD_CopyInflowType(which allocates it) and theCleanupdestroy call meaningful:Writing to
InflowInterprather thanm%Inflow(1)also stops the clobbering, soSetInputsForAAandSetInputsForFVWseem%Inflow(i)correctly paired withu(i).Note that
AD_CalcWindfills the wholeAD_InflowType(all rotors plusInflowWakeVelfor OLAF wake points), soInflowInterpmust remain a fullAD_InflowType, as it is today.Expected impact
DT_Aero: full-field turbulence, sheared inflow combined with blade rotation, large platform/blade motion, or MHK cases with waves. The dynamic-wake (DBEMT) and unsteady-aerofoil (UA) states are marched withdU/dt = 0across the step, so the dynamic-inflow forcing is under-represented.V_wind/Vwnd_LLat index 1 that does not correspond tou(1).The numerical magnitude has not been quantified — fixing this will change regression-test baselines for any case with time-varying inflow, so a full r-test sweep with baseline review is warranted.
OpenFAST Version
Present on
dev(verified atAeroDyn.f90:1871/1884/1888) and in every release since the offending commit: v4.0.0 – v4.2.1 and v5.0.0. Not present in v3.x.System Information
Compiler-independent; this is a control-flow defect in the source, not a build or platform issue.
Additional context
Found while reviewing PR #3311 (generalized support structures in AeroDyn). That PR threads a new
GSInflowargument throughSetInputs, and at:1888it passesInflowInterp%GSInflow— so the new generalized-support inflow inherits exactly the same staleness. Fixing this issue first would let #3311 pick up the correct behaviour for free; the two changes touch the same two lines.