dynbem 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+
8
+ # Distribution / packaging
9
+ *.egg-info/
10
+ *.egg
11
+ dist/
12
+ build/
13
+ wheels/
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+
20
+ # Testing
21
+ .pytest_cache/
22
+ .coverage
23
+ htmlcov/
24
+
25
+ # IDE
26
+ .vscode/
27
+ .idea/
28
+ *.swp
29
+
30
+ # OS
31
+ .DS_Store
32
+ Thumbs.db
33
+
34
+ # Output artifacts
35
+ out/
36
+ *.npz
37
+
38
+ # One-off / scratch scripts (kept on disk, not tracked)
39
+ oneoff/
40
+
dynbem-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kristof
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ recursive-include dynbem *.py
5
+ recursive-include rotors *.yaml *.csv
6
+
7
+ prune Research
8
+ prune tests
9
+ prune envelope
10
+ prune rotors/*/Research
11
+ prune out
12
+ prune .github
13
+ prune oneoff
14
+ prune dist
15
+ prune build
16
+ prune .venv
17
+ prune .pytest_cache
18
+ prune __pycache__
19
+
20
+ exclude CLAUDE.md
21
+ exclude run_map.cmd
22
+ exclude setup.cmd
23
+ exclude requirements.txt
24
+ exclude *.cmd
25
+
26
+ global-exclude *.pyc
27
+ global-exclude __pycache__
28
+ global-exclude .DS_Store
dynbem-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,513 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynbem
3
+ Version: 0.1.0
4
+ Summary: Blade-element momentum rotor aerodynamics with dynamic inflow (Pitt-Peters and Øye), covering the full operating envelope from helicopter hover through climb, descent, VRS, autorotation, and wind-turbine extraction in a single code path.
5
+ Author: Kristof
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mcroomp/dynbem
8
+ Project-URL: Repository, https://github.com/mcroomp/dynbem
9
+ Project-URL: Issues, https://github.com/mcroomp/dynbem/issues
10
+ Keywords: rotor,aerodynamics,BEM,blade-element-momentum,dynamic-inflow,Pitt-Peters,Oye,helicopter,wind-turbine,autorotation,VRS
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Physics
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy>=1.24
22
+ Requires-Dist: numba>=0.58
23
+ Provides-Extra: yaml
24
+ Requires-Dist: PyYAML>=6; extra == "yaml"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest; extra == "dev"
27
+ Requires-Dist: pytest-timeout; extra == "dev"
28
+ Requires-Dist: PyYAML>=6; extra == "dev"
29
+ Requires-Dist: build; extra == "dev"
30
+ Requires-Dist: twine; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # dynbem
34
+
35
+ **Dynamic blade-element momentum rotor aerodynamics — helicopter and
36
+ wind-turbine modes in one code path.**
37
+
38
+ `dynbem` is a Python rotor-aerodynamics library built around a multi-element
39
+ blade-element-momentum (BEM) solver coupled to dynamic-inflow models. It is
40
+ designed to be numerically valid across the **full operating envelope** —
41
+ helicopter hover, axial climb, axial descent, vortex-ring state (VRS),
42
+ windmill-brake state (WBS), autorotation, and wind-turbine power extraction
43
+ — without switching equations or sign conventions between regimes.
44
+
45
+ Two dynamic-inflow models are provided:
46
+
47
+ - **Pitt-Peters** (three-state global ν₀/ν_s/ν_c) — Numba-JIT-compiled,
48
+ with the Peters L-matrix, Glauert wake-skew via the off-diagonal
49
+ coupling, and the Leishman empirical VRS polynomial baked into the
50
+ uniform-inflow state.
51
+ - **Øye 2-stage annular** — per-annulus filtered momentum inflow (the
52
+ OpenFAST DBEMT formulation), independent across radii and numerically
53
+ stable at high advance ratios where Pitt-Peters becomes stiff.
54
+
55
+ Both models share a tabulated polar interpolator and a common BEM ψ-loop
56
+ kernel, and they plug into the same `AeroBase` interface. The package also
57
+ includes a flight-envelope sweep driver (`envelope/compute_map.py`), a
58
+ cyclic-trim solver, a point-mass + cyclic-pitch attitude simulator, and a
59
+ test suite that validates against NACA TN-2474 (Castles & Gray) and Peters'
60
+ own Nikolsky-lecture data.
61
+
62
+ Coordinates are NED throughout; rotor rotation is CCW-from-above
63
+ (American helicopter convention).
64
+
65
+ ## Install
66
+
67
+ The package is set up with a standard `pyproject.toml` and can be installed
68
+ into any environment:
69
+
70
+ ```
71
+ pip install -e .
72
+ ```
73
+
74
+ For the bundled `.venv` on Windows, run `setup.cmd` from the repo root —
75
+ it creates `.venv\`, upgrades pip, and installs `requirements.txt`.
76
+ Activate with `.venv\Scripts\activate`, or invoke directly via
77
+ `.venv\Scripts\python` / `.venv\Scripts\pytest`.
78
+
79
+ ## Usage
80
+
81
+ ```python
82
+ import numpy as np
83
+ import dynbem
84
+
85
+ defn = dynbem.rotor_definition.load("rotors/castles_gray_6ft/rotor.yaml")
86
+ model = dynbem.create_aero(defn, model="pitt_peters_jit") # or "pitt_peters", "oye", "bem"
87
+ state = model.initial_rotor_state()
88
+
89
+ inputs = dynbem.RotorInputs(
90
+ collective_rad=0.14,
91
+ tilt_lon=0.0, tilt_lat=0.0, # swashplate (helicopter-standard signs)
92
+ R_hub=np.eye(3),
93
+ v_hub_world=np.zeros(3),
94
+ wind_world=np.zeros(3),
95
+ t=0.0,
96
+ )
97
+ result, derivative = model.compute_forces(inputs, state)
98
+ # result.F_world, result.M_orbital, result.M_spin, result.Q_spin
99
+ # derivative is a RotorState with d/dt of every state field
100
+ ```
101
+
102
+ `dynbem/__init__.py` lists the full public surface — models (`BEMModel`,
103
+ `PittPetersModel`, `PittPetersModelJIT`, the `create_aero` factory),
104
+ inputs/outputs (`RotorInputs`, `AeroResult`), state types
105
+ (`QuasiStaticRotorState`, `PittPetersRotorState`), polars (`AirfoilPolar`,
106
+ `LinearPolar`), rotor-definition types (`RotorDefinition`,
107
+ `BladeGeometry`, `AirfoilProperties`, `ControlProperties`, etc.), and the
108
+ `vrs_lambda1` helper. Cyclic mapping (`tilt_lon`/`tilt_lat` →
109
+ blade-pitch coefficients) lives in [dynbem/cyclic.py](dynbem/cyclic.py).
110
+
111
+ ## Flight envelope sweep
112
+
113
+ ```
114
+ run_map.cmd # quick grid, saves to out\map.npz, plots to out\
115
+ run_map.cmd --full # full grid
116
+ python -m envelope.compute_map --help
117
+ ```
118
+
119
+ ## Tests
120
+
121
+ ```
122
+ .venv\Scripts\pytest
123
+ ```
124
+
125
+ The `tests/` directory contains unit tests, validation scripts against
126
+ published rotor data, and end-to-end force-balance / frame-transform
127
+ checks. Key files:
128
+
129
+ - `test_bem.py`, `test_bem_components.py` — Level-1 BEM unit tests
130
+ (Prandtl tip/hub loss, momentum-BEM convergence, interface).
131
+ - `test_castles_gray.py` — full Castles-Gray (NACA TN-2474) hover CT/CQ,
132
+ WBS, and autorotation sweep against the paper's Table I data.
133
+ - `test_pitt_peters.py`, `test_pitt_peters_jit.py` — Pitt-Peters
134
+ hover/VRS/WBS validation; JIT-vs-reference numerical agreement.
135
+ - `test_cyclic.py` — helicopter-standard cyclic sign convention on all
136
+ three models, plus Pitt-Peters inflow-dynamics response to cyclic.
137
+ - `test_force_balance.py` — hub-frame transform (`R_hub` tilt → force
138
+ vector tilt), hover collective sweep against vehicle weight,
139
+ translating-flight + wind, and end-to-end swashplate phase mapping.
140
+ - `val_step*.py`, `validate_table_i.py` — standalone validation
141
+ scripts (not part of the pytest run; invoked manually).
142
+
143
+ ---
144
+
145
+ ## Coordinate system — NED
146
+
147
+ This project uses **NED (North-East-Down)** throughout, without exception:
148
+
149
+ - X = North, Y = East, Z = Down
150
+ - Gravity acts in the **+Z** direction
151
+ - Rotor thrust (upward lift) is **negative Z** in world frame: `F_world[2] < 0`
152
+ - Wind blowing upward (driving a flying turbine) is **negative Z** in world frame
153
+ - `R_hub` rotates from hub frame → NED world frame
154
+
155
+ ### Reading literature — coordinate trap
156
+
157
+ Most helicopter and wind-turbine literature uses one of:
158
+ - **SAE / helicopter**: X forward, Y right, Z down (body frame, not world NED)
159
+ - **Wind-turbine (IEC 61400)**: X downwind, Y lateral, Z up (**ENU-like**)
160
+ - **Aeronautics (NED)**: X North, Y East, Z Down
161
+
162
+ When adapting equations or sign conventions from papers, always check
163
+ which frame the authors use. Windmill-brake-state and axial-induction
164
+ literature (Glauert, Buhl) often defines positive inflow **upward**
165
+ (opposing thrust), which is **negative Z** here. Flip signs accordingly.
166
+
167
+ ### Inflow sign convention (NED)
168
+
169
+ For a rotor disk lying in the XY-plane (hub pointing down):
170
+
171
+ - `lambda` (inflow ratio) is positive when flow passes through the disk
172
+ from above (downward, +Z direction), i.e. in **normal rotor mode**
173
+ (helicopter hover).
174
+ - In **windmill / autorotation mode** the wind drives flow upward (−Z),
175
+ so `lambda` is **negative** when the rotor is in energy-harvesting mode.
176
+ - Collective pitch `theta_0 > 0` pitches blade leading edge up
177
+ (toward −Z thrust).
178
+
179
+ ---
180
+
181
+ ## Implementation roadmap
182
+
183
+ The model is built in phases from simple to state-of-the-art, each a
184
+ drop-in upgrade behind the same `AeroBase` interface.
185
+
186
+ ### Level 1 — Multi-element quasi-static BEM ✅ DONE
187
+
188
+ - Multi-element BEM loop (radial quadrature over `n_elements` annuli)
189
+ - Hover-safe inflow iteration on `λ_r` (not wind-turbine induction
190
+ factor `a`)
191
+ - Per-element Prandtl **tip + hub** loss `F = F_tip · F_hub` (both
192
+ factors exported from `dynbem.bem`)
193
+ - Glauert / Buhl Windmill Brake State correction (quadratic root
194
+ selection)
195
+ - Forward-flight ψ-loop: per-azimuth blade pitch (cyclic), tangential
196
+ wind projection (advancing/retreating), and in-plane hub moment
197
+ accumulation (M_orbital)
198
+ - `dω/dt = (Q_aero + Q_motor) / I_ode` — rotor speed integrated as ODE
199
+ state
200
+ - `dψ/dt = ω` — spin angle integrated as ODE state
201
+ - Returns `QuasiStaticRotorState` derivative
202
+ - **Validation target**: Caradonna-Tung rotor (NASA TM-81232, 1981) —
203
+ 2-blade NACA 0012, CT vs collective at 5°/8°/12°
204
+ (see `Research/CaradonnaTung/` for CT tables and test notes)
205
+
206
+ ### Level 2 — Pitt-Peters 3-state dynamic inflow ✅ DONE
207
+
208
+ - `PittPetersModel` (numpy) and `PittPetersModelJIT` (Numba-compiled, same
209
+ physics) in `dynbem/pitt_peters.py` / `dynbem/pitt_peters_jit.py`
210
+ - Prescribed-inflow blade element loop with per-element Prandtl tip + hub
211
+ loss; blade sees `λ_total = λ_0 + v_climb/ΩR` (induced state +
212
+ freestream), so WBS and autorotation work correctly
213
+ - Pitt-Peters ODE in matrix form (Peters 2009 Eq 7, hub axes):
214
+ `[M] dλ/dt + V·[L]⁻¹ λ = forcing` with
215
+ `M = diag(8/(3π), 16/(45π), 16/(45π))` → `τ_0 = 8R/(3πV_T)`,
216
+ `τ_cs = 16R/(45πV_T)`.
217
+ - Steady-state targets follow the canonical L matrix (Peters Eq 10) with
218
+ X = tan(χ/2), translated to our ψ=0-at-+X convention:
219
+ ```
220
+ λ_0_ss = C_T/(2·µ_T) + (15π·X/64) · C_M_hub / µ_T
221
+ λ_c_ss = −(15π·X/64) · C_T + 4·cos(χ)/(1+cos χ) · C_M_hub) / µ_T
222
+ λ_s_ss = 4/(1+cos χ) · C_L_hub / µ_T
223
+ ```
224
+ The `−(15π·X/64)·C_T` cross-coupling in λ_c_ss is the Pitt-Peters term
225
+ that produces Glauert wake-skew naturally from thrust forcing — no
226
+ closed-form Glauert tilt needed.
227
+ - Cyclic input (`tilt_lon`, `tilt_lat`) wired through both models:
228
+ blade pitch `θ(ψ) = collective + θ_1c·cos(ψ) + θ_1s·sin(ψ)` with
229
+ helicopter-standard signs (`tilt_lon > 0` → nose-down,
230
+ `tilt_lat > 0` → roll right). See `dynbem/cyclic.py` and CLAUDE.md.
231
+ - In-plane hub moments returned via `AeroResult.M_orbital`
232
+ (`Mx_hub, My_hub` accumulated in the ψ-loop) — needed for cyclic to
233
+ produce vehicle attitude response in the outer loop.
234
+ - **VRS empirical correction** (Leishman 2000, fit to Castles-Gray
235
+ data): in 0 < λ₂ < 2,
236
+ `λ_0_ss` comes from the polynomial
237
+ `λ₁/V_h = 1 + 1.125λ₂ − 1.372λ₂² + 1.718λ₂³ − 0.655λ₂⁴`
238
+ rather than momentum theory, preventing the Level-1 CT blow-up in VRS.
239
+ Cross-coupling is also skipped in the VRS regime.
240
+ - **Canonical reference**: Peters, D.A. (2009), "How Dynamic Inflow
241
+ Survives in the Competitive World of Rotorcraft Aerodynamics: The
242
+ Alexander Nikolsky Honorary Lecture," *JAHS* 54(1):011001. PDF and
243
+ extraction notes in `Research/Peters_Nikolsky_2008/`.
244
+ - **Validation**: `tests/test_pitt_peters.py` — hover CT vs Level-1
245
+ BEM, VRS no-blow-up, WBS autorotation sign, first-order inflow lag.
246
+ `tests/test_cyclic.py` — helicopter-standard cyclic signs on all
247
+ three models, inflow dynamics under hover + cyclic.
248
+ - **Known limitations**:
249
+ - VRS CT still rises to ~2× nominal in deep VRS (λ₂ ≈ 1.5–2) at
250
+ fixed θ; real rotor stays near nominal (paper: θ barely adjusts).
251
+ The Leishman polynomial shifts the operating point but doesn't
252
+ fully suppress it.
253
+ - Autorotation torque crossing at V/ΩR ≈ 0.14 vs paper's 0.083.
254
+ - Mass-flow scaling uses `µ_T = √(µ²+λ²)` (classical Glauert) rather
255
+ than Peters' Eq 8 `V = (µ²+(λ+ν)(λ+2ν))/√(µ²+(λ+ν)²)`. They agree
256
+ in high-speed forward flight but differ by 2× in hover. Switching
257
+ would need validation against a hover dataset.
258
+ - Wind-axis rotation of the L-matrix is NOT applied; oblique flight
259
+ `µ_y ≠ 0` is approximate. Exact for axial and pure-longitudinal
260
+ flight. A previous implementation was reverted because it
261
+ destabilised the tethered-rotor envelope — see CLAUDE.md.
262
+
263
+ ### Level 2 alt — Øye 2-stage annular dynamic inflow ✅ DONE
264
+
265
+ - `OyeBEMModel` in `dynbem/oye.py` (Numba-compiled ψ-loop)
266
+ - **Annulus-local** inflow: each radial annulus has its own pair of
267
+ first-order lag filters `(W_int, W)` chasing the quasi-steady
268
+ momentum target `W_qs`. No global L-matrix; no λ_c/λ_s harmonic
269
+ states.
270
+ - Two time constants per annulus (Øye 1990, OpenFAST AD Theory §6.3.4):
271
+ ```
272
+ τ₁ = 1.1 / (1 − 1.3·min(a, 0.5)) · R / V_∞
273
+ τ₂(r) = (0.39 − 0.26·(r/R)²) · τ₁
274
+ τ₁·dW_int/dt + W_int = W_qs + k·τ₁·dW_qs/dt
275
+ τ₂·dW/dt + W = W_int
276
+ ```
277
+ with empirical coupling `k = 0.6`. DBEMT_Mod=1 equivalent
278
+ (`dW_qs/dt = 0` across each outer step — exact for envelope sweeps).
279
+ - W_qs per annulus from Glauert momentum balance using rotor-mean
280
+ `µ_T = V_T / Ω·R`: `W_qs[i] = dCT/dx[i] / (4·x[i]·µ_T)`.
281
+ - Same VRS override (Leishman polynomial) as Pitt-Peters for
282
+ `0 < V_descent/V_h < 2` — applied uniformly across annuli.
283
+ - Same cyclic-pitch wiring (`tilt_lon` / `tilt_lat` → per-ψ blade
284
+ pitch) and same in-plane hub moments returned via `M_orbital`.
285
+ - **Why this alongside Pitt-Peters**: Pitt-Peters' L-matrix couples
286
+ thrust + hub moments back into all three inflow harmonics
287
+ globally, which produces a stiff BEM-driven feedback at high
288
+ advance ratios and in descent + edgewise wind. Øye's annulus-local
289
+ filters are independent → no feedback loop → numerically stable in
290
+ the same regimes that needed adaptive time-stepping with
291
+ Pitt-Peters. OpenFAST's DBEMT uses the same Øye-style formulation
292
+ for this reason.
293
+ - **Trade-off**: no harmonic inflow states means the inflow doesn't
294
+ develop a `λ_c`-like tilt in response to cyclic pitching moments,
295
+ so `tests/test_cyclic.py::test_cyclic_inflow_reduces_hub_moment`
296
+ (which checks PP's specific feedback mechanism) doesn't apply.
297
+ Cyclic *control* still works (hub moments respond correctly to
298
+ swashplate inputs), but cyclic *inflow feedback* is absent.
299
+ - **Validation**: `tests/test_oye.py` — hover CT vs Pitt-Peters (5%
300
+ match at moderate thrust), climb-vs-hover induction sign, finite-τ
301
+ lag response, descent + edgewise wind convergence where Pitt-Peters
302
+ was numerically stiff.
303
+ - **References**:
304
+ - Øye, S. (1990). A simple vortex model. IEA Symposium.
305
+ - Snel, H. & Schepers, J.G. (1995). Joint investigation of dynamic
306
+ inflow effects. ECN.
307
+ - OpenFAST AeroDyn Theory v3.5, §6.3.4 (DBEMT).
308
+
309
+ ### Level 3 — Peters-He finite-state dynamic inflow (state of the art)
310
+
311
+ - 9-state (or higher-order) Peters-He inflow model
312
+ - Requires new `PetersHeRotorState` dataclass
313
+ - Captures higher harmonics of the inflow distribution
314
+ - Best accuracy for maneuvering flight and aeroelastic coupling
315
+ - **Validation target**: Caradonna-Tung unsteady / forward-flight data
316
+
317
+ ### Forward flight (applies to all levels) — implemented
318
+
319
+ - Oblique inflow: advance ratio `µ = V_edge / (Ω·R)` ≠ 0
320
+ - Blade azimuth-dependent velocity in the BEM loop (`n_psi=36` stations
321
+ by default, triggered when `µ > 0.01`, cyclic input is nonzero, or
322
+ cyclic inflow state is nonzero)
323
+ - In-plane hub moments `Mx_hub`, `My_hub` returned in `AeroResult.M_orbital`
324
+ - Pitt-Peters L matrix off-diagonal `−L_off·C_T` produces Glauert
325
+ wake-skew naturally from thrust forcing (exact for axial and
326
+ pure-longitudinal flight; approximate for oblique `µ_y ≠ 0`)
327
+
328
+ ---
329
+
330
+ ## BEM solver design — critical notes
331
+
332
+ ### Hover-safe inflow iteration
333
+
334
+ The standard wind-turbine BEM uses the induction factor `a = v_i / V_inf`,
335
+ which **collapses to zero in hover** (`V_inf = 0`). This code instead
336
+ iterates on the **total inflow ratio** `λ_r = v_a / (Ω·R)`, where `v_a`
337
+ is the total axial velocity at the disk (external freestream + induced).
338
+
339
+ The combined momentum-BEM equation at each annulus is:
340
+
341
+ k·(λ_r² + x²) = λ_r·(λ_r − λ_c)
342
+
343
+ where `k = σ_r·cn / (8·F)`, `x = r/R`, and `λ_c = v_climb / (Ω·R)`.
344
+
345
+ This quadratic is solved per iteration step; `v_climb = 0` in hover is
346
+ handled naturally (gives the standard hover solution
347
+ `λ_r = x·sqrt(k/(1−k))`).
348
+
349
+ ### v_climb sign convention (internal BEM)
350
+
351
+ `v_climb = dot(v_rel_world, hub_axis_ned)` (no negation):
352
+
353
+ - `v_climb > 0`: air flows **downward** through disk (helicopter climb /
354
+ normal inflow)
355
+ - `v_climb = 0`: hover
356
+ - `v_climb < 0`: air flows **upward** through disk (autorotation /
357
+ flying wind turbine)
358
+
359
+ ### Root selection in the momentum-BEM quadratic
360
+
361
+ The quadratic has two roots. Selection is by operating mode:
362
+
363
+ - Helicopter / hover (`λ_c ≥ 0`): take the **positive** root (`λ_r > 0`)
364
+ - Turbine / autorotation (`λ_c < 0`): take the **negative** root
365
+ (`λ_r < 0`)
366
+
367
+ ### Autorotation torque sign
368
+
369
+ In autorotation (upward wind, `λ_c < 0`):
370
+ - `λ_r < 0` → `φ < 0` → `ct = cl·sin(φ) − cd·cos(φ) < 0` → `Q_total < 0`
371
+ - `d_omega = (−Q_total + Q_motor) / I` → positive angular acceleration ✓
372
+
373
+ In powered/hover mode (`λ_c ≥ 0`):
374
+ - `Q_total > 0` (aerodynamic drag on rotor) → `d_omega < 0` without
375
+ motor torque ✓
376
+
377
+ ### Force direction
378
+
379
+ `F_world = −T_total · hub_axis_ned`
380
+
381
+ `T_total` is always positive for a rotor generating lift (cn > 0 in
382
+ both modes). With `hub_axis_ned = [0, 0, 1]` for a level rotor:
383
+ `F_world[2] = −T_total < 0` (upward). ✓
384
+
385
+ ---
386
+
387
+ ## Pitt-Peters design notes (`dynbem/pitt_peters.py`)
388
+
389
+ ### State interpretation
390
+
391
+ `λ_0` (and `λ_c`, `λ_s`) is the **induced** inflow ratio `v_i / (ΩR)`,
392
+ not the total inflow. The total axial flow seen by each blade element
393
+ is:
394
+
395
+ λ_total = λ_0 + λ_climb where λ_climb = v_climb / (ΩR) < 0 in descent
396
+
397
+ This must be computed inside the blade element loop — **do not pass
398
+ only `λ_0`**. Without the freestream term the blade never sees
399
+ net-upward flow in WBS, so CQ never goes negative and autorotation is
400
+ suppressed entirely.
401
+
402
+ ### VRS polynomial sign convention
403
+
404
+ The Leishman (2000) polynomial uses descent-positive
405
+ λ₂ = V_descent / V_h:
406
+
407
+ λ₁/V_h = 1 + 1.125·λ₂ − 1.372·λ₂² + 1.718·λ₂³ − 0.655·λ₂⁴
408
+
409
+ This is NOT the form with coefficients
410
+ (−1.125, −1.372, −1.718, −0.655), which applies when the argument is
411
+ V_climb/V_h (negative for descent). The two forms are equivalent;
412
+ this code uses descent-positive throughout.
413
+
414
+ ### V_T floor
415
+
416
+ `V_T = |v_climb + v_0|` → 0 in the middle of VRS (upward freestream ≈
417
+ downward induced). A floor of `1e-2 · max(ΩR, 1)` prevents
418
+ `τ_0 → ∞` and division by zero. This is physically reasonable:
419
+ `τ_0 → large` in VRS is correct (slow, unsteady response), and the
420
+ exact floor value doesn't matter for stability.
421
+
422
+ ### Why CT still rises in deep VRS
423
+
424
+ At λ₂ ≈ 1.5, the Leishman polynomial gives
425
+ `λ_0_ss ≈ 2 · V_h/ΩR`. Combined with `λ_climb ≈ −1.5 · V_h/ΩR`,
426
+ the net blade inflow `λ_total ≈ 0.5 · V_h/ΩR` is less than hover, so
427
+ AoA increases and CT rises. The real VRS has recirculating wakes that
428
+ further restrict net throughflow; the 1-D polynomial captures the mean
429
+ induced velocity but not the 3-D blockage. This is a known limitation
430
+ of all momentum-based VRS models.
431
+
432
+ ---
433
+
434
+ ## Øye design notes (`dynbem/oye.py`)
435
+
436
+ ### State interpretation
437
+
438
+ `W[i]` and `W_int[i]` are induced inflow ratios `v_i / (Ω·R)` **per
439
+ annulus**, not global harmonics. The total axial flow at annulus `i`
440
+ seen by the blade is `λ_total[i] = λ_climb + W[i]` (compare with
441
+ Pitt-Peters' `λ_total = λ_climb + λ_0 + x·(λ_c·cos ψ + λ_s·sin ψ)`).
442
+
443
+ `W` is what the blade actually reads in the ψ-loop. `W_int` is the
444
+ intermediate filter stage between the quasi-steady target `W_qs[i]`
445
+ and `W`. Both arrays have length `n_elements`.
446
+
447
+ ### Quasi-steady target
448
+
449
+ `W_qs[i]` is solved per annulus from Glauert momentum balance using
450
+ the rotor-mean `µ_T = V_T / Ω·R`:
451
+
452
+ W_qs[i] = dCT/dx[i] / (4·x[i]·µ_T)
453
+ where V_T = √(v_edge² + (v_climb + v_0_mean)²)
454
+
455
+ This linear (in `W_qs`) form is what Pitt-Peters effectively uses in
456
+ its aggregate `λ_0_ss = T / (2ρA·V_T·ΩR)`. The pure axial-momentum
457
+ form `4·x·λ_r·W = dCT/dx` is unstable in forward flight (small λ_r in
458
+ descent makes W blow up) and was rejected during development.
459
+
460
+ ### Why no L matrix
461
+
462
+ Annulus-local: each `W[i]` evolves independently, driven only by
463
+ `W_qs[i]` from its own annulus. Cross-annulus coupling happens only
464
+ through the rotor-mean `µ_T` in the τ formulas and `V_h` in the VRS
465
+ override. There's no analogue of Pitt-Peters' `−L_off·C_T` term that
466
+ feeds total thrust into the cyclic harmonics, so no BEM-driven
467
+ feedback loop and no associated stiffness — at the cost of not
468
+ modelling cyclic inflow harmonics at all.
469
+
470
+ ### Time constants
471
+
472
+ `τ₁` is rotor-mean (depends on `a_avg`, not per-annulus); `τ₂(r)`
473
+ varies with radius. With `dt = 5 ms` and a 1 m rotor at `V_∞ ~ 10 m/s`,
474
+ `τ₁ ~ 0.1 s` and `τ₂ ~ 0.04 s` — both well above the envelope's outer
475
+ `dt`, so the semi-implicit Euler in `envelope/point_mass.py` is gentle
476
+ damping at most.
477
+
478
+ ### Cyclic input
479
+
480
+ Cyclic pitch flows through the same `cyclic_coeffs` → `θ(ψ) =
481
+ collective + θ_1c·cos ψ + θ_1s·sin ψ` path as Pitt-Peters; the ψ-loop
482
+ produces correct hub moments. What's *missing* compared to
483
+ Pitt-Peters: the cyclic-driven hub moment doesn't develop a
484
+ counter-acting inflow harmonic (no `λ_c`/`λ_s` states), so the
485
+ steady-state moment is over-predicted vs Pitt-Peters at hover.
486
+ Cyclic *control* (sign and order-of-magnitude) is right; cyclic
487
+ *inflow damping* is absent.
488
+
489
+ ---
490
+
491
+ ## Research sources
492
+
493
+ Extracted tables and figures from primary literature live under
494
+ `Research/`. Each paper subfolder uses the convention
495
+ `page_NN_<description>.md` so extractions trace back to their source
496
+ page image.
497
+
498
+ - **CaradonnaTung/** — NASA TM-81232 (1981). 2-blade NACA 0012 hover
499
+ CT data at θc = 5°/8°/12°. Primary BEM validation source. No CP /
500
+ torque data.
501
+ - **Buhl_NREL_TP500_36834/** — NREL TP-500-36834 (2005). Windmill
502
+ Brake State correction extending Glauert. Used for the WBS
503
+ quadratic.
504
+ - **Castles_TN2474/** — NACA TN-2474 (Castles & Gray, 1951). Induced
505
+ velocity in hover/descent — experimental basis for the Leishman VRS
506
+ polynomial.
507
+ - **Harrington_TN2318/** — NACA TN-2318 (Harrington, 1951). Hover
508
+ CT vs CP polars for two full-scale rotors. Candidate dataset for
509
+ CP-CT polar validation.
510
+
511
+ ## License
512
+
513
+ MIT — see `LICENSE`.