pyflightstream 0.2.0__py3-none-any.whl
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.
- pyflightstream/__init__.py +27 -0
- pyflightstream/cases/__init__.py +332 -0
- pyflightstream/cases/matrix_legacy.py +337 -0
- pyflightstream/commands/__init__.py +544 -0
- pyflightstream/commands/_meta.yaml +18 -0
- pyflightstream/commands/actuators.yaml +167 -0
- pyflightstream/commands/advanced_settings.yaml +136 -0
- pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
- pyflightstream/commands/boundary_conditions.yaml +153 -0
- pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
- pyflightstream/commands/coordinate_systems.yaml +123 -0
- pyflightstream/commands/file_io.yaml +82 -0
- pyflightstream/commands/mesh_import_export.yaml +72 -0
- pyflightstream/commands/motion_definitions.yaml +185 -0
- pyflightstream/commands/probe_points.yaml +116 -0
- pyflightstream/commands/runtime_settings.yaml +160 -0
- pyflightstream/commands/scenes.yaml +14 -0
- pyflightstream/commands/script_controls.yaml +39 -0
- pyflightstream/commands/simulation_controls.yaml +30 -0
- pyflightstream/commands/solver_analysis.yaml +118 -0
- pyflightstream/commands/solver_export.yaml +138 -0
- pyflightstream/commands/solver_initialization.yaml +95 -0
- pyflightstream/commands/solver_settings.yaml +120 -0
- pyflightstream/commands/streamlines.yaml +63 -0
- pyflightstream/commands/surface_sections.yaml +145 -0
- pyflightstream/commands/sweeper.yaml +115 -0
- pyflightstream/commands/unsteady_solver.yaml +68 -0
- pyflightstream/commands/volume_sections.yaml +148 -0
- pyflightstream/farfield/__init__.py +613 -0
- pyflightstream/files/__init__.py +375 -0
- pyflightstream/fsi/__init__.py +57 -0
- pyflightstream/fsi/beam.py +417 -0
- pyflightstream/fsi/centrifugal.py +418 -0
- pyflightstream/fsi/cli.py +206 -0
- pyflightstream/fsi/config.py +316 -0
- pyflightstream/fsi/driver.py +501 -0
- pyflightstream/fsi/kinematics.py +153 -0
- pyflightstream/fsi/loads.py +740 -0
- pyflightstream/fsi/nodes.py +463 -0
- pyflightstream/fsi/state.py +181 -0
- pyflightstream/post/__init__.py +18 -0
- pyflightstream/post/writers.py +195 -0
- pyflightstream/probes/__init__.py +453 -0
- pyflightstream/probes/geometry.py +281 -0
- pyflightstream/probes/planar.py +477 -0
- pyflightstream/qa/__init__.py +59 -0
- pyflightstream/qa/cli.py +364 -0
- pyflightstream/qa/compat.py +285 -0
- pyflightstream/qa/drift.py +406 -0
- pyflightstream/qa/geometry.py +421 -0
- pyflightstream/qa/physics.py +1744 -0
- pyflightstream/qa/probes.py +1023 -0
- pyflightstream/qa/references/PHY-01.yaml +38 -0
- pyflightstream/qa/references/PHY-02.yaml +28 -0
- pyflightstream/qa/references/PHY-05.yaml +29 -0
- pyflightstream/qa/references/PHY-06.yaml +90 -0
- pyflightstream/qa/references/SMI-01.yaml +28 -0
- pyflightstream/qa/references/SMI-02.yaml +28 -0
- pyflightstream/qa/specs.py +1405 -0
- pyflightstream/reference.py +465 -0
- pyflightstream/results/__init__.py +547 -0
- pyflightstream/run/__init__.py +677 -0
- pyflightstream/script/__init__.py +474 -0
- pyflightstream/script/helpers.py +844 -0
- pyflightstream/versions.py +191 -0
- pyflightstream-0.2.0.dist-info/METADATA +88 -0
- pyflightstream-0.2.0.dist-info/RECORD +71 -0
- pyflightstream-0.2.0.dist-info/WHEEL +5 -0
- pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
- pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
- pyflightstream-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""FSI configuration schema, validation, and round-trip IO.
|
|
2
|
+
|
|
3
|
+
Pipeline role: `config.json` is the single per-run configuration of
|
|
4
|
+
the structural coupling executable (DLV-007 Section 5). It is staged
|
|
5
|
+
into the run folder and hashed; every convergence-log row carries the
|
|
6
|
+
hash (FSI-R15) so any later result is traceable to its exact
|
|
7
|
+
configuration, the same discipline as the run manifest (FR-19).
|
|
8
|
+
|
|
9
|
+
All geometry lives in the rotating blade frame: the spanwise axis
|
|
10
|
+
points from root to tip along the pitch axis, the chordwise axis
|
|
11
|
+
points toward the leading edge, and the normal axis completes the
|
|
12
|
+
right-handed triad. Per-station distributions are sampled at
|
|
13
|
+
``station_radii_m`` and interpolated linearly in between.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_STATION_ARRAY_FIELDS = (
|
|
26
|
+
"chord_m",
|
|
27
|
+
"mass_per_length_kg_per_m",
|
|
28
|
+
"inertia_major_kg_m",
|
|
29
|
+
"inertia_minor_kg_m",
|
|
30
|
+
"bending_stiffness_n_m2",
|
|
31
|
+
"torsion_stiffness_n_m2",
|
|
32
|
+
"elastic_axis_offset_chordwise_m",
|
|
33
|
+
"elastic_axis_offset_normal_m",
|
|
34
|
+
"cg_offset_chordwise_m",
|
|
35
|
+
"cg_offset_normal_m",
|
|
36
|
+
"geometric_pitch_deg",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BladeProperties(BaseModel):
|
|
41
|
+
"""Per-station structural distributions of one blade.
|
|
42
|
+
|
|
43
|
+
All blades share these distributions; node sets and solves are
|
|
44
|
+
replicated per blade (FSI-R08). Every list is sampled at
|
|
45
|
+
``station_radii_m`` and must have the same length.
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
station_radii_m : list of float
|
|
50
|
+
Radial stations measured from the rotation axis along the
|
|
51
|
+
pitch axis [m]; strictly increasing; the first entry is the
|
|
52
|
+
root (clamp) station and the last is the tip.
|
|
53
|
+
chord_m : list of float
|
|
54
|
+
Local chord [m]; sets the lever arm of the leading-edge and
|
|
55
|
+
trailing-edge twist-encoding nodes (DLV-007 Section 4.4).
|
|
56
|
+
mass_per_length_kg_per_m : list of float
|
|
57
|
+
Running mass mu(r) [kg/m]; source of the centrifugal tension.
|
|
58
|
+
inertia_major_kg_m : list of float
|
|
59
|
+
Sectional mass moment of inertia per unit length about the
|
|
60
|
+
section major principal axis, I1(r) [kg m].
|
|
61
|
+
inertia_minor_kg_m : list of float
|
|
62
|
+
Sectional mass moment of inertia per unit length about the
|
|
63
|
+
section minor principal axis, I2(r) [kg m]. The difference
|
|
64
|
+
I1 - I2 drives the propeller moment.
|
|
65
|
+
bending_stiffness_n_m2 : list of float
|
|
66
|
+
Flapwise bending stiffness EI(r) [N m^2].
|
|
67
|
+
torsion_stiffness_n_m2 : list of float
|
|
68
|
+
Torsional stiffness GJ(r) [N m^2].
|
|
69
|
+
elastic_axis_offset_chordwise_m, elastic_axis_offset_normal_m : list of float
|
|
70
|
+
Offset e(r) from the pitch axis to the elastic axis [m], in
|
|
71
|
+
section-plane components (chordwise positive toward the
|
|
72
|
+
leading edge, normal completing the triad). Loads are exported
|
|
73
|
+
about the pitch axis and transferred to the elastic axis with
|
|
74
|
+
this offset (FSI-R04), so refining the elastic axis estimate
|
|
75
|
+
never touches the FlightStream setup.
|
|
76
|
+
cg_offset_chordwise_m, cg_offset_normal_m : list of float
|
|
77
|
+
Offset from the elastic axis to the sectional center of
|
|
78
|
+
gravity [m], section-plane components. Enters the centrifugal
|
|
79
|
+
terms and creates the bend-twist coupling.
|
|
80
|
+
geometric_pitch_deg : list of float
|
|
81
|
+
Built-in geometric pitch distribution [deg] about the pitch
|
|
82
|
+
axis; the total pitch is geometric plus elastic twist.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(extra="forbid")
|
|
86
|
+
|
|
87
|
+
station_radii_m: list[float] = Field(min_length=2)
|
|
88
|
+
chord_m: list[float]
|
|
89
|
+
mass_per_length_kg_per_m: list[float]
|
|
90
|
+
inertia_major_kg_m: list[float]
|
|
91
|
+
inertia_minor_kg_m: list[float]
|
|
92
|
+
bending_stiffness_n_m2: list[float]
|
|
93
|
+
torsion_stiffness_n_m2: list[float]
|
|
94
|
+
elastic_axis_offset_chordwise_m: list[float]
|
|
95
|
+
elastic_axis_offset_normal_m: list[float]
|
|
96
|
+
cg_offset_chordwise_m: list[float]
|
|
97
|
+
cg_offset_normal_m: list[float]
|
|
98
|
+
geometric_pitch_deg: list[float]
|
|
99
|
+
|
|
100
|
+
@field_validator("station_radii_m")
|
|
101
|
+
@classmethod
|
|
102
|
+
def _radii_increase(cls, radii: list[float]) -> list[float]:
|
|
103
|
+
"""Reject stations that do not march from root to tip."""
|
|
104
|
+
if radii[0] < 0.0:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"the root station sits at a negative radius, which has no "
|
|
107
|
+
f"physical meaning on a rotating blade (got {radii[0]} m)"
|
|
108
|
+
)
|
|
109
|
+
for inboard, outboard in zip(radii, radii[1:], strict=False):
|
|
110
|
+
if outboard <= inboard:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"station radii must strictly increase from root to tip; "
|
|
113
|
+
f"{outboard} m does not lie outboard of {inboard} m"
|
|
114
|
+
)
|
|
115
|
+
return radii
|
|
116
|
+
|
|
117
|
+
@field_validator("chord_m", "mass_per_length_kg_per_m")
|
|
118
|
+
@classmethod
|
|
119
|
+
def _strictly_positive(cls, values: list[float]) -> list[float]:
|
|
120
|
+
"""Reject zero or negative chord and running mass values."""
|
|
121
|
+
if any(v <= 0.0 for v in values):
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"chord and running mass must be positive at every station; a "
|
|
124
|
+
"zero or negative value describes a section that does not exist"
|
|
125
|
+
)
|
|
126
|
+
return values
|
|
127
|
+
|
|
128
|
+
@field_validator("bending_stiffness_n_m2", "torsion_stiffness_n_m2")
|
|
129
|
+
@classmethod
|
|
130
|
+
def _stiff_enough_to_solve(cls, values: list[float]) -> list[float]:
|
|
131
|
+
"""Zero stiffness makes the static beam solve singular."""
|
|
132
|
+
if any(v <= 0.0 for v in values):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"EI and GJ must be positive at every station; a zero or "
|
|
135
|
+
"negative stiffness makes the static solve k theta = M singular"
|
|
136
|
+
)
|
|
137
|
+
return values
|
|
138
|
+
|
|
139
|
+
@field_validator("inertia_major_kg_m", "inertia_minor_kg_m")
|
|
140
|
+
@classmethod
|
|
141
|
+
def _inertia_nonnegative(cls, values: list[float]) -> list[float]:
|
|
142
|
+
"""Mass moments of inertia are nonnegative by definition."""
|
|
143
|
+
if any(v < 0.0 for v in values):
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"sectional mass moments of inertia are nonnegative by "
|
|
146
|
+
"definition; a negative I1 or I2 is a sign or unit error"
|
|
147
|
+
)
|
|
148
|
+
return values
|
|
149
|
+
|
|
150
|
+
@model_validator(mode="after")
|
|
151
|
+
def _consistent_station_count(self) -> "BladeProperties":
|
|
152
|
+
"""Every distribution must be sampled at every station."""
|
|
153
|
+
n = len(self.station_radii_m)
|
|
154
|
+
for name in _STATION_ARRAY_FIELDS:
|
|
155
|
+
m = len(getattr(self, name))
|
|
156
|
+
if m != n:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"distribution '{name}' has {m} entries but there are "
|
|
159
|
+
f"{n} radial stations; every per-station list must be "
|
|
160
|
+
"sampled at exactly the stations of station_radii_m"
|
|
161
|
+
)
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PhaseSchedule(BaseModel):
|
|
166
|
+
"""Parameters of the four-phase coupling driver (DLV-007 Section 4.5).
|
|
167
|
+
|
|
168
|
+
Attributes
|
|
169
|
+
----------
|
|
170
|
+
wake_development_revolutions : float
|
|
171
|
+
Phase 1 length [revolutions]: zero displacements are written
|
|
172
|
+
while the wake develops on the rigid blade.
|
|
173
|
+
coupling_relaxation : float
|
|
174
|
+
Relaxation factor lambda of phases 2 and 3, in (0, 1]. The
|
|
175
|
+
update is d_new = d_old + lambda (d_calc - d_old) (FSI-R07).
|
|
176
|
+
Phase 4 records instantaneous loads with lambda = 1 by design:
|
|
177
|
+
relaxing there would low-pass exactly the 1P amplitude and
|
|
178
|
+
phase being measured.
|
|
179
|
+
averaging_window_revolutions : float
|
|
180
|
+
Load-averaging window of phases 2 and 3 [revolutions].
|
|
181
|
+
tip_twist_tolerance_deg : float
|
|
182
|
+
Phase 3 convergence: tip elastic twist change per revolution
|
|
183
|
+
below this value [deg] (together with the revolution-averaged
|
|
184
|
+
CT stability judged from the convergence log, FSI-R09).
|
|
185
|
+
recording_revolutions : float
|
|
186
|
+
Phase 4 length [revolutions] recording theta(r, psi) per blade.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
model_config = ConfigDict(extra="forbid")
|
|
190
|
+
|
|
191
|
+
wake_development_revolutions: float = Field(default=1.0, gt=0.0)
|
|
192
|
+
coupling_relaxation: float = Field(default=0.4, gt=0.0, le=1.0)
|
|
193
|
+
averaging_window_revolutions: float = Field(default=1.0, gt=0.0)
|
|
194
|
+
tip_twist_tolerance_deg: float = Field(default=0.05, gt=0.0)
|
|
195
|
+
recording_revolutions: float = Field(default=1.0, gt=0.0)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class FsiConfig(BaseModel):
|
|
199
|
+
"""Complete per-run configuration of the coupling executable.
|
|
200
|
+
|
|
201
|
+
Attributes
|
|
202
|
+
----------
|
|
203
|
+
blade_count : int
|
|
204
|
+
Number of blades; node sets, section groups, and solves are
|
|
205
|
+
replicated per blade (FSI-R08).
|
|
206
|
+
omega_rad_per_s : float
|
|
207
|
+
Rotor angular speed Omega [rad/s], constant over the run;
|
|
208
|
+
source of the centrifugal tension and the propeller moment.
|
|
209
|
+
time_increment_s : float or None
|
|
210
|
+
Unsteady time step of the coupled run [s]. The loads export
|
|
211
|
+
header prints the increment with three decimals only (RPT-006:
|
|
212
|
+
0.003525 prints as .004, skewing the revolution bookkeeping),
|
|
213
|
+
so a configured value takes precedence in the driver's phase
|
|
214
|
+
schedule, with the printed value cross-checked against it at
|
|
215
|
+
print precision. None falls back to the printed value.
|
|
216
|
+
blade : BladeProperties
|
|
217
|
+
Shared per-station structural distributions.
|
|
218
|
+
stiffness_scale_factor : float
|
|
219
|
+
Multiplier applied to EI and GJ at solve time. The near-rigid
|
|
220
|
+
regression of the coupled pilot (WP7) scales a synthetic blade
|
|
221
|
+
stiff with this knob instead of editing distributions.
|
|
222
|
+
node_offset_chord_fraction : float
|
|
223
|
+
Chord fraction of the leading-edge and trailing-edge node
|
|
224
|
+
offsets from the elastic axis used to encode twist as
|
|
225
|
+
differential translations (DLV-007 Section 4.4).
|
|
226
|
+
phases : PhaseSchedule
|
|
227
|
+
Driver phase parameters.
|
|
228
|
+
node_map_file : str
|
|
229
|
+
Name, inside the run folder, of the node ordering map written
|
|
230
|
+
by the node generator; the same generator emits the node file
|
|
231
|
+
imported into FlightStream, keeping a single source of truth
|
|
232
|
+
for the FSIDisp ordering (FSI-R14).
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
model_config = ConfigDict(extra="forbid")
|
|
236
|
+
|
|
237
|
+
blade_count: int = Field(ge=1)
|
|
238
|
+
omega_rad_per_s: float = Field(ge=0.0)
|
|
239
|
+
time_increment_s: float | None = Field(default=None, gt=0.0)
|
|
240
|
+
blade: BladeProperties
|
|
241
|
+
stiffness_scale_factor: float = Field(default=1.0, gt=0.0)
|
|
242
|
+
node_offset_chord_fraction: float = Field(default=0.25, gt=0.0, le=0.5)
|
|
243
|
+
phases: PhaseSchedule = Field(default_factory=PhaseSchedule)
|
|
244
|
+
node_map_file: str = "fsi_node_map.json"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def frame_embedding(cfg: FsiConfig) -> str:
|
|
248
|
+
"""Return the geometric embedding of the blade section frame.
|
|
249
|
+
|
|
250
|
+
``"rotor_frame"`` for a spinning blade (RPT-006 finding 3: the
|
|
251
|
+
import frame is rotor-axis X, in-plane Y, span Z, and the section
|
|
252
|
+
chordwise/normal axes rotate with the local blade angle, taken
|
|
253
|
+
from ``geometric_pitch_deg``); ``"section_frame"`` at Omega zero,
|
|
254
|
+
where the import frame is the section frame itself (the wing
|
|
255
|
+
case). One rule shared by the node generator and the loads
|
|
256
|
+
projection, so both sides of the interface always agree.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
cfg : FsiConfig
|
|
261
|
+
Validated configuration.
|
|
262
|
+
"""
|
|
263
|
+
return "rotor_frame" if cfg.omega_rad_per_s > 0.0 else "section_frame"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def load_config(path: str | Path) -> FsiConfig:
|
|
267
|
+
"""Load and validate a ``config.json``.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
path : str or Path
|
|
272
|
+
JSON file with the :class:`FsiConfig` fields.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
FsiConfig
|
|
277
|
+
Validated configuration.
|
|
278
|
+
"""
|
|
279
|
+
path = Path(path)
|
|
280
|
+
cfg = FsiConfig.model_validate_json(path.read_text(encoding="utf-8"))
|
|
281
|
+
logger.info("loaded FSI config %s (hash %s)", path, config_hash(cfg))
|
|
282
|
+
return cfg
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def dump_config(cfg: FsiConfig, path: str | Path) -> None:
|
|
286
|
+
"""Write a configuration as pretty-printed JSON.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
cfg : FsiConfig
|
|
291
|
+
Configuration to persist.
|
|
292
|
+
path : str or Path
|
|
293
|
+
Destination file; overwritten if present.
|
|
294
|
+
"""
|
|
295
|
+
Path(path).write_text(cfg.model_dump_json(indent=2) + "\n", encoding="utf-8")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def config_hash(cfg: FsiConfig) -> str:
|
|
299
|
+
"""Return the canonical sha256 hash of a configuration.
|
|
300
|
+
|
|
301
|
+
The hash is computed over the JSON serialization with sorted keys
|
|
302
|
+
and no whitespace, so it is independent of field order and
|
|
303
|
+
formatting. Every convergence-log row carries it (FSI-R15).
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
cfg : FsiConfig
|
|
308
|
+
Configuration to hash.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
str
|
|
313
|
+
Hex sha256 digest.
|
|
314
|
+
"""
|
|
315
|
+
canonical = json.dumps(cfg.model_dump(mode="json"), sort_keys=True, separators=(",", ":"))
|
|
316
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|