Skip to content

fix(bem): fix windmill/helicopter branch discontinuity at lambda_climb boundary#8

Merged
mcroomp merged 6 commits into
mainfrom
fix/quasi-static-bem-windmill-boundary
Jul 19, 2026
Merged

fix(bem): fix windmill/helicopter branch discontinuity at lambda_climb boundary#8
mcroomp merged 6 commits into
mainfrom
fix/quasi-static-bem-windmill-boundary

Conversation

@mcroomp

@mcroomp mcroomp commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes a spurious discontinuous jump in thrust and torque at lambda_climb == 0 (the hover/descent boundary) in the quasi-static BEM solver.

Root Causes

  1. solve_bem_element was seeding from the descent branch when lambda_climb < 0, even in the near-hover band where that branch is physically wrong — no distinct windmill flow topology exists yet.
  2. solve_bem_element_windmill was activating for any lambda_climb < 0, including tiny values where its Brent bracket is ill-conditioned and lam_local = omega*r/|v_climb| diverges.

Fix

  • Add MIN_LAMBDA_CLIMB_WINDMILL = 0.02 threshold (tip-referenced inflow ratio). Below this, the windmill solver returns None unconditionally, so all elements/azimuths use the same (helicopter) solver branch together.
  • solve_bem_element always seeds and selects the climb/hover root, since it is only reached when the windmill solver has declined.
  • The gate uses tip speed (not per-element omega*r) so every element switches branch together, preventing inboard/outboard inconsistency at the same rotor-level v_climb.

Additional Changes

  • VPM: Add corrected Pedrizzetti relaxation (relax_corrected_pedrizzetti) that preserves |gamma| exactly via the normalization factor 1/sqrt(b2). The previous linear blend was only first-order in r.
  • Cargo.toml: Add steady_flight_simple to workspace members.
  • Version bump: 0.5.0 → 0.6.0

mcroomp added 6 commits July 19, 2026 08:16
…b boundary

Quasi-static BEM solver had a spurious discontinuous jump in thrust and
torque right at lambda_climb == 0 (the hover/descent boundary).

Root causes:
1. solve_bem_element was seeding from the descent branch when
   lambda_climb < 0, even in the near-hover band where that branch
   is physically wrong (no distinct windmill flow topology yet).
2. solve_bem_element_windmill activated for ANY lambda_climb < 0,
   including tiny values where its Brent bracket is ill-conditioned
   and the lam_local = omega*r/|v_climb| term diverges.

Fix:
- Add MIN_LAMBDA_CLIMB_WINDMILL = 0.02 threshold (tip-referenced).
  Below this, windmill solver returns None unconditionally so all
  elements/azimuths use the same (helicopter) solver branch together.
- solve_bem_element always seeds and selects the climb/hover root,
  since it is only reached when windmill has declined.
- Gate is rotor-level (uses tip speed) so every element switches
  branch together, preventing inboard/outboard inconsistency.

Also:
- VPM: add corrected Pedrizzetti relaxation that preserves |gamma|
  exactly via the normalization factor sqrt(b2). Previous blend was
  only first-order in r.
- Cargo.toml: add steady_flight_simple to workspace members.
- Bump version to 0.6.0.
…t boundary tests

solve_bem_element previously changed to always seed the climb branch,
which broke deep-descent physics (lambda_r should be negative when
strong upward wind drives net upward inflow).

Fix: restore conditional seeding based on whether v_climb is inside
the near-hover band (|v_climb| < MIN_LAMBDA_CLIMB_WINDMILL * tip_speed).
- Near hover: climb branch (fixes the discontinuity at v_climb = 0)
- Deep descent: descent branch (physically correct; windmill solver
  handles this in the full pipeline, but solve_bem_element may be
  called as a fallback for azimuths where the Brent bracket is absent)

Low-level element behavior tested in Rust (test_hover_boundary_continuity,
test_deep_descent_negative_lambda_r). Python test_autorotation_lambda_r_negative
reverted to its original physics-correct assertion (lambda_r < 0 for
strong upward wind), since that belongs at the Python/aero-API layer.
…c API

solve_bem_element is an internal implementation detail of the BEM element
solver (raw helicopter momentum quadratic, no windmill routing). Exposing
it at the Python layer created confusion about its contract and made it
impossible to change the internal routing logic without breaking Python
tests.

Changes:
- dynbem/src/lib.rs: remove PyBEMElementResult pyclass and
  solve_bem_element pyfunction; remove from module registration
- dynbem/python/dynbem/bem.py: remove from re-exports and __all__
- tests/test_bem_components.py: remove TestBEMElementConvergence class
  (5 test methods) and associated solve_bem_element import
- dynbem_rs/src/quasi_static_bem.rs: port all 5 element-level tests
  to Rust (test_momentum_balance_residual_hover_and_climb,
  test_zero_omega_gives_zero_forces, test_hover_lambda_r_positive,
  test_hover_thrust_matches_momentum_theory) alongside the existing
  boundary tests
- verification/caradonna_tung_spanwise_cl.py: replace solve_bem_element
  call with a self-contained Python Newton BEM element solve for hover
  (v_climb=0); no internal API needed
… Python API

These are internal BEM/Pitt-Peters implementation helpers with no
user-facing purpose. Same pattern as the previous solve_bem_element
removal.

- dynbem/src/lib.rs: remove three pyfunctions and their module registrations
- dynbem/python/dynbem/__init__.py: remove from _dynbem imports and __all__
- dynbem/python/dynbem/bem.py: remove prandtl re-exports
- dynbem/python/dynbem/pitt_peters.py: remove vrs_lambda1 re-export
- tests/test_bem_components.py: remove TestPrandtlTipLoss and TestPrandtlHubLoss
- tests/test_pitt_peters.py: remove TestVRSPolynomial and vrs_lambda1 import
- dynbem_rs/src/quasi_static_bem.rs: port Prandtl tests to Rust
  (test_prandtl_tip_loss_matches_formula, test_prandtl_tip_loss_boundary_cases,
  test_prandtl_hub_loss_matches_formula, test_prandtl_hub_loss_boundary_cases)
- dynbem_rs/src/common.rs: add vrs_lambda1 Rust tests
  (hover boundary, WBS boundary, VRS peak)
@mcroomp
mcroomp merged commit 5a3e18d into main Jul 19, 2026
4 checks passed
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