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,375 @@
|
|
|
1
|
+
"""Managed run file layout and the campaign manifest.
|
|
2
|
+
|
|
3
|
+
Pipeline role: owns where run files live. Folder layout, staging of solver
|
|
4
|
+
inputs, collection of outputs, and archiving are managed by the package,
|
|
5
|
+
not by the user: folder identity mistakes were a recurring failure mode in
|
|
6
|
+
the predecessor toolchain. Run identity lives in the manifest
|
|
7
|
+
(``runs.json``), never in folder names; folder names are generated,
|
|
8
|
+
English, and stable, and are never parsed for meaning (SAD Section 6).
|
|
9
|
+
|
|
10
|
+
The managed layout under a user-chosen campaign root:
|
|
11
|
+
|
|
12
|
+
- ``runs.json``: the authoritative manifest, one record per executed
|
|
13
|
+
point.
|
|
14
|
+
- ``sims/sim_<sim_id>/``: per-simulation folder with ``inputs/``
|
|
15
|
+
(staged copies with recorded sha256), ``scripts/`` (generated script
|
|
16
|
+
text per point), ``raw/`` (solver outputs as produced), and
|
|
17
|
+
``parsed/`` (typed extracts).
|
|
18
|
+
- ``archive/``: zipped completed simulations, manifest-driven.
|
|
19
|
+
|
|
20
|
+
Archiving and cleaning refuse to act when the manifest is missing or
|
|
21
|
+
does not record the target simulation, so file management can never
|
|
22
|
+
destroy an unrecorded run.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import enum
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import zipfile
|
|
33
|
+
from collections.abc import Sequence
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
37
|
+
|
|
38
|
+
_SIM_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")
|
|
39
|
+
_SIM_SUBDIRS = ("inputs", "scripts", "raw", "parsed")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WorkspaceError(RuntimeError):
|
|
43
|
+
"""A file-management operation was refused or impossible.
|
|
44
|
+
|
|
45
|
+
The refusals protect run evidence: archiving or cleaning without a
|
|
46
|
+
manifest record would destroy a run the manifest cannot account
|
|
47
|
+
for, and collection of a declared output that the solver never
|
|
48
|
+
produced points at an incomplete run.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RunStatus(enum.StrEnum):
|
|
53
|
+
"""Terminal status of one executed campaign point (SAD Section 7).
|
|
54
|
+
|
|
55
|
+
Every executed point lands in exactly one of these; a silent skip
|
|
56
|
+
is structurally impossible in the campaign loop.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
CONVERGED = "CONVERGED"
|
|
60
|
+
COMPLETED_MAX_ITER = "COMPLETED_MAX_ITER"
|
|
61
|
+
FAILED_EXECUTION = "FAILED_EXECUTION"
|
|
62
|
+
FAILED_SCRIPT = "FAILED_SCRIPT"
|
|
63
|
+
FAILED_INCOMPLETE_OUTPUT = "FAILED_INCOMPLETE_OUTPUT"
|
|
64
|
+
FAILED_DIVERGED = "FAILED_DIVERGED"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RunRecord(BaseModel):
|
|
68
|
+
"""One manifest record: a single executed campaign point.
|
|
69
|
+
|
|
70
|
+
The record plus the staged inputs reproduce the run (NFR-07).
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
run_id : str
|
|
75
|
+
Unique identity of the executed point, for example
|
|
76
|
+
``"campaign/sim_9001/a+02.0_b+00.0"``; the manifest rejects
|
|
77
|
+
duplicates.
|
|
78
|
+
sim_id : str
|
|
79
|
+
Simulation identity; ties the record to ``sims/sim_<sim_id>``.
|
|
80
|
+
point : dict of str to float
|
|
81
|
+
Sweep point coordinates, for example alpha and beta in deg.
|
|
82
|
+
fs_version_requested : str
|
|
83
|
+
Canonical FlightStream version the script was built for.
|
|
84
|
+
fs_version_reported : str, optional
|
|
85
|
+
Version printed in the solver outputs; filled by the parsers
|
|
86
|
+
and cross-checked against the requested one (FR-18).
|
|
87
|
+
fs_build : str, optional
|
|
88
|
+
Build string reported by the solver, when available.
|
|
89
|
+
package_version : str
|
|
90
|
+
pyflightstream version that produced the run.
|
|
91
|
+
script_sha256 : str
|
|
92
|
+
Hash of the executed script text.
|
|
93
|
+
inputs_sha256 : dict of str to str
|
|
94
|
+
Hash per staged input file name, recorded at staging time.
|
|
95
|
+
raw_flag : bool
|
|
96
|
+
True when the script used the ``raw()`` escape hatch and its
|
|
97
|
+
content bypassed database validation (FR-07).
|
|
98
|
+
status : RunStatus
|
|
99
|
+
Terminal status of the point.
|
|
100
|
+
iterations : int, optional
|
|
101
|
+
Solver iterations reached, when parsed.
|
|
102
|
+
residual : float, optional
|
|
103
|
+
Final residual, when parsed.
|
|
104
|
+
wall_time_s : float, optional
|
|
105
|
+
Wall-clock duration of the solver process in seconds.
|
|
106
|
+
outputs : list of str
|
|
107
|
+
Collected output files, relative to the simulation folder
|
|
108
|
+
(for example ``"raw/loads.txt"``).
|
|
109
|
+
error : str, optional
|
|
110
|
+
Error text for failed points.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
model_config = ConfigDict(extra="forbid")
|
|
114
|
+
|
|
115
|
+
run_id: str
|
|
116
|
+
sim_id: str
|
|
117
|
+
point: dict[str, float] = Field(default_factory=dict)
|
|
118
|
+
fs_version_requested: str
|
|
119
|
+
fs_version_reported: str | None = None
|
|
120
|
+
fs_build: str | None = None
|
|
121
|
+
package_version: str
|
|
122
|
+
script_sha256: str
|
|
123
|
+
inputs_sha256: dict[str, str] = Field(default_factory=dict)
|
|
124
|
+
raw_flag: bool
|
|
125
|
+
status: RunStatus
|
|
126
|
+
iterations: int | None = None
|
|
127
|
+
residual: float | None = None
|
|
128
|
+
wall_time_s: float | None = None
|
|
129
|
+
outputs: list[str] = Field(default_factory=list)
|
|
130
|
+
error: str | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _sha256(path: Path) -> str:
|
|
134
|
+
digest = hashlib.sha256()
|
|
135
|
+
with open(path, "rb") as handle:
|
|
136
|
+
for block in iter(lambda: handle.read(65536), b""):
|
|
137
|
+
digest.update(block)
|
|
138
|
+
return digest.hexdigest()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CampaignWorkspace:
|
|
142
|
+
"""The managed folder layout of one campaign root.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
root : str or Path
|
|
147
|
+
User-chosen campaign root; everything below it is managed by
|
|
148
|
+
this class and never hand-built.
|
|
149
|
+
|
|
150
|
+
Attributes
|
|
151
|
+
----------
|
|
152
|
+
root : Path
|
|
153
|
+
The campaign root.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, root: str | Path):
|
|
157
|
+
self.root = Path(root)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def manifest_path(self) -> Path:
|
|
161
|
+
"""Location of the authoritative manifest, ``runs.json``."""
|
|
162
|
+
return self.root / "runs.json"
|
|
163
|
+
|
|
164
|
+
def sim_dir(self, sim_id: str) -> Path:
|
|
165
|
+
"""Return the managed folder of one simulation.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
sim_id : str
|
|
170
|
+
Simulation identity; letters, digits, underscore, and
|
|
171
|
+
hyphen only, so the derived folder name is stable and
|
|
172
|
+
portable (NFR-10).
|
|
173
|
+
"""
|
|
174
|
+
if not _SIM_ID_PATTERN.match(sim_id):
|
|
175
|
+
raise WorkspaceError(
|
|
176
|
+
f"sim_id {sim_id!r} cannot name a managed folder: use letters, digits, "
|
|
177
|
+
"underscore, or hyphen. Folder names derive from sim_id and must stay "
|
|
178
|
+
"stable and portable; identity lives in the manifest, not in names."
|
|
179
|
+
)
|
|
180
|
+
return self.root / "sims" / f"sim_{sim_id}"
|
|
181
|
+
|
|
182
|
+
def create_sim(self, sim_id: str) -> Path:
|
|
183
|
+
"""Create the managed subfolders of one simulation and return its path.
|
|
184
|
+
|
|
185
|
+
Creates ``inputs/``, ``scripts/``, ``raw/``, and ``parsed/``;
|
|
186
|
+
existing folders are kept, so the call is idempotent.
|
|
187
|
+
"""
|
|
188
|
+
sim = self.sim_dir(sim_id)
|
|
189
|
+
for name in _SIM_SUBDIRS:
|
|
190
|
+
(sim / name).mkdir(parents=True, exist_ok=True)
|
|
191
|
+
return sim
|
|
192
|
+
|
|
193
|
+
def stage_inputs(self, sim_id: str, sources: Sequence[str | Path]) -> dict[str, str]:
|
|
194
|
+
"""Copy input files into ``inputs/`` and record their hashes.
|
|
195
|
+
|
|
196
|
+
Staging happens before execution so the manifest can tie the
|
|
197
|
+
run to the exact input content (NFR-07).
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
sim_id : str
|
|
202
|
+
Target simulation.
|
|
203
|
+
sources : sequence of str or Path
|
|
204
|
+
Files to copy; each must exist.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
dict of str to str
|
|
209
|
+
sha256 per staged file name, ready for
|
|
210
|
+
:attr:`RunRecord.inputs_sha256`.
|
|
211
|
+
"""
|
|
212
|
+
sim = self.create_sim(sim_id)
|
|
213
|
+
hashes: dict[str, str] = {}
|
|
214
|
+
for source in sources:
|
|
215
|
+
origin = Path(source)
|
|
216
|
+
if not origin.is_file():
|
|
217
|
+
raise WorkspaceError(
|
|
218
|
+
f"cannot stage {origin}: the file does not exist. Staging copies "
|
|
219
|
+
"inputs before execution so the manifest records what actually ran."
|
|
220
|
+
)
|
|
221
|
+
target = sim / "inputs" / origin.name
|
|
222
|
+
shutil.copy2(origin, target)
|
|
223
|
+
hashes[origin.name] = _sha256(target)
|
|
224
|
+
return hashes
|
|
225
|
+
|
|
226
|
+
def write_script(self, sim_id: str, name: str, text: str) -> tuple[Path, str]:
|
|
227
|
+
"""Write one generated script into ``scripts/`` and hash it.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
sim_id : str
|
|
232
|
+
Target simulation.
|
|
233
|
+
name : str
|
|
234
|
+
Script file name, for example ``"a+02.0_b+00.0.txt"``.
|
|
235
|
+
text : str
|
|
236
|
+
Rendered script text from the builder.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
Path
|
|
241
|
+
Location of the written script.
|
|
242
|
+
str
|
|
243
|
+
sha256 of the written text, for
|
|
244
|
+
:attr:`RunRecord.script_sha256`.
|
|
245
|
+
"""
|
|
246
|
+
sim = self.create_sim(sim_id)
|
|
247
|
+
target = sim / "scripts" / name
|
|
248
|
+
target.write_text(text, encoding="utf-8")
|
|
249
|
+
return target, _sha256(target)
|
|
250
|
+
|
|
251
|
+
def collect_outputs(self, sim_id: str, produced: Sequence[str | Path]) -> list[str]:
|
|
252
|
+
"""Move declared solver outputs into ``raw/``.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
sim_id : str
|
|
257
|
+
Target simulation.
|
|
258
|
+
produced : sequence of str or Path
|
|
259
|
+
Output files the run declared it would produce, wherever
|
|
260
|
+
the script wrote them.
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
list of str
|
|
265
|
+
Collected names relative to the simulation folder
|
|
266
|
+
(``"raw/<name>"``), ready for :attr:`RunRecord.outputs`.
|
|
267
|
+
|
|
268
|
+
Raises
|
|
269
|
+
------
|
|
270
|
+
WorkspaceError
|
|
271
|
+
If a declared output does not exist; the campaign loop
|
|
272
|
+
turns this into FAILED_INCOMPLETE_OUTPUT, never into a
|
|
273
|
+
silently shorter output set.
|
|
274
|
+
"""
|
|
275
|
+
sim = self.create_sim(sim_id)
|
|
276
|
+
missing = [str(path) for path in produced if not Path(path).is_file()]
|
|
277
|
+
if missing:
|
|
278
|
+
raise WorkspaceError(
|
|
279
|
+
f"declared outputs were not produced: {', '.join(missing)}. A missing "
|
|
280
|
+
"declared output marks the point FAILED_INCOMPLETE_OUTPUT; outputs are "
|
|
281
|
+
"never silently dropped."
|
|
282
|
+
)
|
|
283
|
+
collected: list[str] = []
|
|
284
|
+
for path in produced:
|
|
285
|
+
origin = Path(path)
|
|
286
|
+
shutil.move(str(origin), sim / "raw" / origin.name)
|
|
287
|
+
collected.append(f"raw/{origin.name}")
|
|
288
|
+
return collected
|
|
289
|
+
|
|
290
|
+
def read_manifest(self) -> list[RunRecord]:
|
|
291
|
+
"""Read and validate every record of ``runs.json``.
|
|
292
|
+
|
|
293
|
+
Returns an empty list when the manifest does not exist yet.
|
|
294
|
+
"""
|
|
295
|
+
if not self.manifest_path.is_file():
|
|
296
|
+
return []
|
|
297
|
+
entries = json.loads(self.manifest_path.read_text(encoding="utf-8"))
|
|
298
|
+
return [RunRecord.model_validate(entry) for entry in entries]
|
|
299
|
+
|
|
300
|
+
def append_record(self, record: RunRecord) -> None:
|
|
301
|
+
"""Append one record to the manifest, atomically.
|
|
302
|
+
|
|
303
|
+
The manifest is rewritten through a temporary file and an
|
|
304
|
+
atomic replace, so a crash never leaves it half-written; a
|
|
305
|
+
duplicate ``run_id`` is rejected because the manifest is the
|
|
306
|
+
run identity (PP-6).
|
|
307
|
+
"""
|
|
308
|
+
records = self.read_manifest()
|
|
309
|
+
if any(existing.run_id == record.run_id for existing in records):
|
|
310
|
+
raise WorkspaceError(
|
|
311
|
+
f"run_id {record.run_id!r} is already in the manifest; run identity "
|
|
312
|
+
"must be unique. Use a new run_id or archive the campaign first."
|
|
313
|
+
)
|
|
314
|
+
records.append(record)
|
|
315
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
payload = json.dumps([entry.model_dump(mode="json") for entry in records], indent=2)
|
|
317
|
+
temporary = self.manifest_path.with_suffix(".json.tmp")
|
|
318
|
+
temporary.write_text(payload + "\n", encoding="utf-8")
|
|
319
|
+
temporary.replace(self.manifest_path)
|
|
320
|
+
|
|
321
|
+
def archive_sim(self, sim_id: str) -> Path:
|
|
322
|
+
"""Zip one recorded simulation into ``archive/`` and remove its folder.
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
Path
|
|
327
|
+
Location of the written zip file.
|
|
328
|
+
|
|
329
|
+
Raises
|
|
330
|
+
------
|
|
331
|
+
WorkspaceError
|
|
332
|
+
If the manifest is missing, does not record ``sim_id``, or
|
|
333
|
+
the simulation folder does not exist: file management
|
|
334
|
+
never destroys an unrecorded run.
|
|
335
|
+
"""
|
|
336
|
+
sim = self._recorded_sim(sim_id, operation="archive")
|
|
337
|
+
archive_dir = self.root / "archive"
|
|
338
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
339
|
+
target = archive_dir / f"sim_{sim_id}.zip"
|
|
340
|
+
with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as bundle:
|
|
341
|
+
for path in sorted(sim.rglob("*")):
|
|
342
|
+
if path.is_file():
|
|
343
|
+
bundle.write(path, path.relative_to(sim))
|
|
344
|
+
shutil.rmtree(sim)
|
|
345
|
+
return target
|
|
346
|
+
|
|
347
|
+
def clean_sim(self, sim_id: str) -> None:
|
|
348
|
+
"""Remove one recorded simulation folder without archiving it.
|
|
349
|
+
|
|
350
|
+
Raises
|
|
351
|
+
------
|
|
352
|
+
WorkspaceError
|
|
353
|
+
Same refusals as :meth:`archive_sim`.
|
|
354
|
+
"""
|
|
355
|
+
sim = self._recorded_sim(sim_id, operation="clean")
|
|
356
|
+
shutil.rmtree(sim)
|
|
357
|
+
|
|
358
|
+
def _recorded_sim(self, sim_id: str, operation: str) -> Path:
|
|
359
|
+
sim = self.sim_dir(sim_id)
|
|
360
|
+
if not self.manifest_path.is_file():
|
|
361
|
+
raise WorkspaceError(
|
|
362
|
+
f"refusing to {operation} sim_{sim_id}: no manifest (runs.json) exists "
|
|
363
|
+
"in this campaign root. Without the manifest the folder content cannot "
|
|
364
|
+
"be accounted for, and file management never destroys an unrecorded run."
|
|
365
|
+
)
|
|
366
|
+
if not any(record.sim_id == sim_id for record in self.read_manifest()):
|
|
367
|
+
raise WorkspaceError(
|
|
368
|
+
f"refusing to {operation} sim_{sim_id}: the manifest has no record of "
|
|
369
|
+
"this simulation, so its folder would be destroyed unaccounted."
|
|
370
|
+
)
|
|
371
|
+
if not sim.is_dir():
|
|
372
|
+
raise WorkspaceError(
|
|
373
|
+
f"cannot {operation} sim_{sim_id}: the folder {sim} does not exist."
|
|
374
|
+
)
|
|
375
|
+
return sim
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Fluid-structure interaction coupling for rotating blades.
|
|
2
|
+
|
|
3
|
+
Pipeline role: this subpackage implements the external structural
|
|
4
|
+
executable of the FlightStream Aeroelastic Toolbox loop (M6, DLV-007;
|
|
5
|
+
FR-23a). Per coupling call FlightStream exports sectional loads on
|
|
6
|
+
user-defined surface sections; the executable reads them together with
|
|
7
|
+
its configuration and persisted state, solves one beam per blade,
|
|
8
|
+
converts the solution to nodal translations, and writes the
|
|
9
|
+
displacement file back for the solver to deform the mesh. All exchange
|
|
10
|
+
happens in the rotating blade frames; this package never handles
|
|
11
|
+
azimuth or global-frame transforms (FSI-R02).
|
|
12
|
+
|
|
13
|
+
The structural backend is PyNite (PyPI distribution ``PyNiteFEA``,
|
|
14
|
+
import name ``Pynite``), pulled in only by the optional ``[fsi]``
|
|
15
|
+
extra; importing :mod:`pyflightstream.fsi` itself stays dependency
|
|
16
|
+
free so the core package works without the extra installed.
|
|
17
|
+
|
|
18
|
+
The evidence status of the structural model is recorded in DLV-007
|
|
19
|
+
Section 2: formulas live in small isolated functions with their source
|
|
20
|
+
cited in the docstring, so a later primary-source correction stays a
|
|
21
|
+
localized change.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Protocol, runtime_checkable
|
|
26
|
+
|
|
27
|
+
from pyflightstream.fsi.config import FsiConfig, config_hash, load_config
|
|
28
|
+
|
|
29
|
+
__all__ = ["FsiConfig", "StructuralSolver", "config_hash", "load_config"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class StructuralSolver(Protocol):
|
|
34
|
+
"""Contract of one structural coupling call (HND-008 loop).
|
|
35
|
+
|
|
36
|
+
FlightStream calls the executable between coupling iterations; the
|
|
37
|
+
executable is stateless per call (FSI-R01) and all state persists
|
|
38
|
+
in files inside the run folder. An implementation reads the
|
|
39
|
+
exported sectional loads, ``config.json`` and ``state.json`` from
|
|
40
|
+
``run_dir``, solves the structure, and writes ``FSIDisp.txt`` plus
|
|
41
|
+
the updated ``state.json`` atomically (FSI-R13).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def step(self, run_dir: Path) -> None:
|
|
45
|
+
"""Execute one coupling call inside ``run_dir``.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
run_dir : Path
|
|
50
|
+
Run folder managed by :mod:`pyflightstream.files` (FR-28)
|
|
51
|
+
containing the FlightStream loads export, ``config.json``
|
|
52
|
+
and ``state.json``. The written ``FSIDisp.txt`` carries one
|
|
53
|
+
translation vector (dx, dy, dz) per structural node, in
|
|
54
|
+
meters, in the rotating blade frame, in exactly the node
|
|
55
|
+
order imported into FlightStream (FSI-R14).
|
|
56
|
+
"""
|
|
57
|
+
...
|