Skip to content

AeroDyn: AD_UpdateStates discards the wind it computes and feeds BEMT a stale, frozen inflow #3447

Description

@andrew-platt

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

  1. 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.

  2. 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).

  3. 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:

  1. Building any OpenFAST configuration with Wake_Mod = 1 or 2 (BEMT/DBEMT) and UA_Mod > 0.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions