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.
- dynbem-0.1.0/.gitignore +40 -0
- dynbem-0.1.0/LICENSE +21 -0
- dynbem-0.1.0/MANIFEST.in +28 -0
- dynbem-0.1.0/PKG-INFO +513 -0
- dynbem-0.1.0/README.md +481 -0
- dynbem-0.1.0/dynbem/__init__.py +212 -0
- dynbem-0.1.0/dynbem/_bem_common.py +143 -0
- dynbem-0.1.0/dynbem/bem.py +401 -0
- dynbem-0.1.0/dynbem/cyclic.py +59 -0
- dynbem-0.1.0/dynbem/oye.py +392 -0
- dynbem-0.1.0/dynbem/pitt_peters.py +395 -0
- dynbem-0.1.0/dynbem/pitt_peters_jit.py +307 -0
- dynbem-0.1.0/dynbem/polar.py +121 -0
- dynbem-0.1.0/dynbem/rotor_definition.py +283 -0
- dynbem-0.1.0/dynbem/rotor_state.py +178 -0
- dynbem-0.1.0/dynbem/trim.py +278 -0
- dynbem-0.1.0/dynbem.egg-info/PKG-INFO +513 -0
- dynbem-0.1.0/dynbem.egg-info/SOURCES.txt +25 -0
- dynbem-0.1.0/dynbem.egg-info/dependency_links.txt +1 -0
- dynbem-0.1.0/dynbem.egg-info/requires.txt +12 -0
- dynbem-0.1.0/dynbem.egg-info/top_level.txt +1 -0
- dynbem-0.1.0/pyproject.toml +45 -0
- dynbem-0.1.0/rotors/beaupoil_2026/rotor.yaml +75 -0
- dynbem-0.1.0/rotors/beaupoil_2026/sg6040_re500k.csv +149 -0
- dynbem-0.1.0/rotors/castles_gray_6ft/naca0015_ncrit5_re200k.csv +163 -0
- dynbem-0.1.0/rotors/castles_gray_6ft/rotor.yaml +32 -0
- dynbem-0.1.0/setup.cfg +4 -0
dynbem-0.1.0/.gitignore
ADDED
|
@@ -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.
|
dynbem-0.1.0/MANIFEST.in
ADDED
|
@@ -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`.
|