Skip to content

BeamDyn: rotating-frame-consistent linearization Jacobians when RotStates=True#3396

Closed
mayankchetan wants to merge 6 commits into
OpenFAST:devfrom
mayankchetan:f/bd-rotstates-framefix
Closed

BeamDyn: rotating-frame-consistent linearization Jacobians when RotStates=True#3396
mayankchetan wants to merge 6 commits into
OpenFAST:devfrom
mayankchetan:f/bd-rotstates-framefix

Conversation

@mayankchetan

Copy link
Copy Markdown
Contributor

Feature or improvement description

Fixes #3394: with RotStates = True (the default), BeamDyn's linearization Jacobians are a
frozen root-aligned inertial snapshot, yet the .lin rows/columns are flagged
RotatingFrame = T — so MBC3-based postprocessors inject spurious spin-softening
(f² = f₀² − αΩ²) into speed-independent branches (e.g. the 5-MW tower side-side droops
2.29 % at 12 rpm; the IEA-22 droops ~8 % at rated).

This PR makes the returned Jacobians frame-consistent with the flag. A new
BD_JacRotFrame in modules/beamdyn/src/BeamDyn.f90 applies, per paired (q, dq/dt) state
triplet (pairing offset Nx/2):

A_r = S·A·S⁻¹ + blkdiag(−ω̃)     with  S = [[I, 0], [−ω̃, I]]
B_r = S·B,   C_r = C·S⁻¹

to dXdx / dXdu / dYdx, where ω is the full root RotationVel 3-vector mapped into the
frozen GlbRot basis (a scalar Ω is insufficient — precone alone puts −0.0548 rad/s in z at
12 rpm on the 5-MW). The transform is exactly identity at standstill. This is not a revert
of the PR #2063 code removed in 5fbd380f8: that code was ≈identity at the operating point
and lacked the transport (Coriolis) term, so it never produced rotating-frame Jacobians.

Design choice — where the transform lives: it is gated by a new optional IsLin flag
threaded ModGlue_Linearize → FAST_JacobianPInput / FAST_JacobianPContState (FAST_Funcs.f90) → BD, so it applies only at linearization snapshots. The tight-coupling solver
(BuildJacobianTC) and AeroMap calls are unflagged and keep the raw frozen-frame Jacobians
their Newton iterations require. All existing callers use keyword arguments; no C-binding,
Simulink, or module-driver interfaces are affected, and no registry regeneration is needed.
An alternative placement — doing the transform glue-side in FAST_ModGlue.f90 and leaving the
module API flag-free — is equally workable; happy to rework to that if maintainers prefer.

Runtime guards: fatal on state-layout-invariant violation or gross re-anchoring deviation
(>1e-3), warning (once per snapshot) on measurable re-anchoring deviation (>1e-6), ω̇ ≠ 0, or
|WM parameters| > 0.2 (first-order rotation-composition limit).

Known limitation (documented in #3394): floating platforms — platform/yaw rates fold into the
root ω, where MBC3's scalar-Ω assumption independently breaks down anyway.

Related issue, if one exists

Fixes #3394. Companion defect (independent, also corrupts BD Campbell work): #3395, fixed by
PR #3393 (.lin full-precision writer) — the two PRs are independent but were isolated in the
same investigation, and verification decks here neutralize #3395 via OutFmt "ES18.9E3".

Impacted areas of the software

  • modules/beamdyn/src/BeamDyn.f90 — new BD_JacRotFrame, applied in
    BD_JacobianPInput / BD_JacobianPContState when p%RotStates .and. IsLin; dead
    RotateStates declarations removed.
  • modules/openfast-library/src/FAST_Funcs.f90, FAST_ModGlue.f90 — thread the optional
    IsLin flag from ModGlue_Linearize only.
  • Time marching is byte-identical code; standstill linearization output is bit-identical.

Additional supporting information

Verification (double precision, dev @ 0160d29a0, wide-format decks):

  • NREL 5-MW Campbell 0–25 rpm (CompElast=2): tower-SS flatness 2.29 % → 0.015 %;
    matches an independent python-side transform of the unpatched matrices to ≤0.02 % and the
    ElastoDyn reference to ≤0.6 %; all damping ratios positive.
  • IEA fleet (29 cases, IEA-15/3.4/10/22 ported to dev): patched Fortran vs python
    reference ≤0.022 % per point; SS flatness 0.111 % / 0.029 % / 0.026 % / 0.379 % — the
    IEA-22 residual equals the python reference's own 0.361 % (second-order WM/prebend effect,
    not introduced here; raw unpatched droop is ~8 %).
  • Standstill: .lin numerics bit-identical patched vs unpatched (timestamp-only diff).
  • ω mapping: instrumented print reproduced the per-blade target vector incl. the −2.5°
    precone z-component (instrumentation removed).

Generative AI usage

Developed with Anthropic Claude (Claude Code) under human direction; independently
whole-branch reviewed before submission.

Co-authored-by: Anthropic Claude claude@anthropic.com

Test results, if applicable

  • r-test branch merging required

r-test impact: baselines change for 5MW_Land_BD_Linear, 5MW_Land_BD_Linear_Aero, and
Damped_Beam_Rotating (the fix working — rotating-case Jacobians change by design);
Damped_Beam_Rotating runs clean on the patched binary; standstill beam cases are identical
and all CompElast = 1 cases are unaffected. Opened as draft until the companion r-test
baseline-regeneration branch is up; can regenerate on request or follow maintainer process.

mayankchetan and others added 5 commits April 1, 2025 09:39
…RotStates=T

BD's linearized states are a frozen root-aligned inertial snapshot but were
flagged RotatingFrame=T, so MBC3's Omega-corrections injected spurious
spin-softening (tower-mode 'dip', w^2 = w0^2 - alpha*Omega^2, alpha ~ blade
mass fraction). Re-express dXdx/dXdu/dYdx in the true rotating frame
(A_r = S A S^-1 + blkdiag(-wt), B_r = S B, C_r = C S^-1, with
S = [[I,0],[-wt,I]] per paired (q, dqdt) state triplet and wt built from the
root angular velocity in the frozen BD basis) so the exported metadata is
honest. Identity at standstill (.lin numerics bit-identical). Replaces the
rotation-only transform removed in 5fbd380 (which was ~identity at the OP
and lacked the velocity-mixing and transport terms).

The transform applies only on the glue linearization path: a new optional
IsLin flag threads from ModGlue_Linearize through FAST_JacobianPInput /
FAST_JacobianPContState into BD. Solver (BuildJacobianTC) and AeroMap calls
are unflagged and keep the raw frozen-frame Jacobians their iterations
require (the linearization-only invariant that root orientation matches the
frozen reference frame does not hold mid-step, and the integrator's Newton
matrix must stay consistent with its residual).

Guards: fatal on state-layout or re-anchoring-invariant violation; warnings
for nonzero root angular acceleration (omega-dot transport term omitted) and
large WM rotation states (first-order transform).

Acceptance (NREL 5-MW BD 0-12 rpm, wide-OutFmt baseline): unpatched SS
branch droops 2.29%; patched flat to 0.015%, matches python frame-fix
expectation within 0.02% and ElastoDyn within 0.6% at every speed.
The root-orientation-vs-GlbRot invariant check was fatal at >1e-8, which
trips on healthy linearization snapshots (deviation between the per-step
re-anchoring input and the lin-time input is solver-convergence noise;
observed >1e-8 on IEA-15 at 4 rpm while NREL 5-MW stays below). The
transform basis remains exact regardless (omega is mapped through GlbRot,
the frame the states are actually anchored to); the deviation only bounds
the S-basis error. Now: warn above 1e-6 (reporting the measured value),
fatal only above 1e-3 (gross misalignment = states not root-aligned at
all, e.g. a caller outside the linearization snapshot path).

5-MW acceptance checker re-run after the change: PASS, unchanged
(SS flatness 0.015%).
…ion snapshot

Gate the RootDev warning behind DoTransport (the dXdx call) so it fires once
per snapshot per BD instance instead of on all three transformed blocks; the
fatal gross-misalignment check remains unconditional. Also fill the IEA-15
fleet-verdict row (patched SS flatness 0.111%, max dev vs python reference
0.022%). 5-MW acceptance checker re-run: PASS unchanged (0.015%).
…till no-op, no silent return under root acceleration

Two edge-case orderings found by an adversarial review of the branch:

- The re-anchoring invariant guard ran before the standstill check, so a
  parked linearization with a grossly mis-anchored root would abort fatally
  even though every transform operation is an exact identity at omega = 0.
  The standstill early-return now precedes the guard, making the advertised
  standstill no-op unconditional.
- The standstill early-return could skip the omega-dot warning, so a snapshot
  taken at exactly zero speed while the root is accelerating (e.g. LinTimes
  during startup) returned silently despite the omitted -skew(alpha) transport
  term being nonzero there. The zero-speed path now warns before returning.

No change to the transform itself; the 7-speed 5-MW acceptance checker passes
with identical numbers (SS flatness 0.015%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CyaECKUrqvcsDCu78FAXi1
@mayankchetan

Copy link
Copy Markdown
Contributor Author

Follow-up commit f230c16a4 from an additional adversarial review pass on the branch: two guard-ordering edge cases in BD_JacRotFrame (no change to the transform itself).

  1. The standstill early-return now precedes the re-anchoring invariant guard — at ω = 0 every operation is an exact identity regardless of frame alignment, so the routine returns the Jacobian unchanged instead of potentially aborting on a mis-anchored root. This makes the advertised standstill no-op unconditional.
  2. The zero-speed path no longer returns silently when the root is accelerating (ω = 0, ω̇ ≠ 0, e.g. a LinTimes snapshot during startup): it now emits the same omitted-ω̇-transport-term warning as the spinning path before returning.

Re-verified after the change: the 7-speed 5-MW acceptance checker passes with identical numbers (tower-SS flatness 0.015 %).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant