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.
Files changed (71) hide show
  1. pyflightstream/__init__.py +27 -0
  2. pyflightstream/cases/__init__.py +332 -0
  3. pyflightstream/cases/matrix_legacy.py +337 -0
  4. pyflightstream/commands/__init__.py +544 -0
  5. pyflightstream/commands/_meta.yaml +18 -0
  6. pyflightstream/commands/actuators.yaml +167 -0
  7. pyflightstream/commands/advanced_settings.yaml +136 -0
  8. pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
  9. pyflightstream/commands/boundary_conditions.yaml +153 -0
  10. pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
  11. pyflightstream/commands/coordinate_systems.yaml +123 -0
  12. pyflightstream/commands/file_io.yaml +82 -0
  13. pyflightstream/commands/mesh_import_export.yaml +72 -0
  14. pyflightstream/commands/motion_definitions.yaml +185 -0
  15. pyflightstream/commands/probe_points.yaml +116 -0
  16. pyflightstream/commands/runtime_settings.yaml +160 -0
  17. pyflightstream/commands/scenes.yaml +14 -0
  18. pyflightstream/commands/script_controls.yaml +39 -0
  19. pyflightstream/commands/simulation_controls.yaml +30 -0
  20. pyflightstream/commands/solver_analysis.yaml +118 -0
  21. pyflightstream/commands/solver_export.yaml +138 -0
  22. pyflightstream/commands/solver_initialization.yaml +95 -0
  23. pyflightstream/commands/solver_settings.yaml +120 -0
  24. pyflightstream/commands/streamlines.yaml +63 -0
  25. pyflightstream/commands/surface_sections.yaml +145 -0
  26. pyflightstream/commands/sweeper.yaml +115 -0
  27. pyflightstream/commands/unsteady_solver.yaml +68 -0
  28. pyflightstream/commands/volume_sections.yaml +148 -0
  29. pyflightstream/farfield/__init__.py +613 -0
  30. pyflightstream/files/__init__.py +375 -0
  31. pyflightstream/fsi/__init__.py +57 -0
  32. pyflightstream/fsi/beam.py +417 -0
  33. pyflightstream/fsi/centrifugal.py +418 -0
  34. pyflightstream/fsi/cli.py +206 -0
  35. pyflightstream/fsi/config.py +316 -0
  36. pyflightstream/fsi/driver.py +501 -0
  37. pyflightstream/fsi/kinematics.py +153 -0
  38. pyflightstream/fsi/loads.py +740 -0
  39. pyflightstream/fsi/nodes.py +463 -0
  40. pyflightstream/fsi/state.py +181 -0
  41. pyflightstream/post/__init__.py +18 -0
  42. pyflightstream/post/writers.py +195 -0
  43. pyflightstream/probes/__init__.py +453 -0
  44. pyflightstream/probes/geometry.py +281 -0
  45. pyflightstream/probes/planar.py +477 -0
  46. pyflightstream/qa/__init__.py +59 -0
  47. pyflightstream/qa/cli.py +364 -0
  48. pyflightstream/qa/compat.py +285 -0
  49. pyflightstream/qa/drift.py +406 -0
  50. pyflightstream/qa/geometry.py +421 -0
  51. pyflightstream/qa/physics.py +1744 -0
  52. pyflightstream/qa/probes.py +1023 -0
  53. pyflightstream/qa/references/PHY-01.yaml +38 -0
  54. pyflightstream/qa/references/PHY-02.yaml +28 -0
  55. pyflightstream/qa/references/PHY-05.yaml +29 -0
  56. pyflightstream/qa/references/PHY-06.yaml +90 -0
  57. pyflightstream/qa/references/SMI-01.yaml +28 -0
  58. pyflightstream/qa/references/SMI-02.yaml +28 -0
  59. pyflightstream/qa/specs.py +1405 -0
  60. pyflightstream/reference.py +465 -0
  61. pyflightstream/results/__init__.py +547 -0
  62. pyflightstream/run/__init__.py +677 -0
  63. pyflightstream/script/__init__.py +474 -0
  64. pyflightstream/script/helpers.py +844 -0
  65. pyflightstream/versions.py +191 -0
  66. pyflightstream-0.2.0.dist-info/METADATA +88 -0
  67. pyflightstream-0.2.0.dist-info/RECORD +71 -0
  68. pyflightstream-0.2.0.dist-info/WHEEL +5 -0
  69. pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
  70. pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
  71. 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()