pyflightstream 0.2.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.
Files changed (109) hide show
  1. pyflightstream-0.2.0/LICENSE +21 -0
  2. pyflightstream-0.2.0/PKG-INFO +88 -0
  3. pyflightstream-0.2.0/README.md +55 -0
  4. pyflightstream-0.2.0/pyproject.toml +80 -0
  5. pyflightstream-0.2.0/setup.cfg +4 -0
  6. pyflightstream-0.2.0/src/pyflightstream/__init__.py +27 -0
  7. pyflightstream-0.2.0/src/pyflightstream/cases/__init__.py +332 -0
  8. pyflightstream-0.2.0/src/pyflightstream/cases/matrix_legacy.py +337 -0
  9. pyflightstream-0.2.0/src/pyflightstream/commands/__init__.py +544 -0
  10. pyflightstream-0.2.0/src/pyflightstream/commands/_meta.yaml +18 -0
  11. pyflightstream-0.2.0/src/pyflightstream/commands/actuators.yaml +167 -0
  12. pyflightstream-0.2.0/src/pyflightstream/commands/advanced_settings.yaml +136 -0
  13. pyflightstream-0.2.0/src/pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
  14. pyflightstream-0.2.0/src/pyflightstream/commands/boundary_conditions.yaml +153 -0
  15. pyflightstream-0.2.0/src/pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
  16. pyflightstream-0.2.0/src/pyflightstream/commands/coordinate_systems.yaml +123 -0
  17. pyflightstream-0.2.0/src/pyflightstream/commands/file_io.yaml +82 -0
  18. pyflightstream-0.2.0/src/pyflightstream/commands/mesh_import_export.yaml +72 -0
  19. pyflightstream-0.2.0/src/pyflightstream/commands/motion_definitions.yaml +185 -0
  20. pyflightstream-0.2.0/src/pyflightstream/commands/probe_points.yaml +116 -0
  21. pyflightstream-0.2.0/src/pyflightstream/commands/runtime_settings.yaml +160 -0
  22. pyflightstream-0.2.0/src/pyflightstream/commands/scenes.yaml +14 -0
  23. pyflightstream-0.2.0/src/pyflightstream/commands/script_controls.yaml +39 -0
  24. pyflightstream-0.2.0/src/pyflightstream/commands/simulation_controls.yaml +30 -0
  25. pyflightstream-0.2.0/src/pyflightstream/commands/solver_analysis.yaml +118 -0
  26. pyflightstream-0.2.0/src/pyflightstream/commands/solver_export.yaml +138 -0
  27. pyflightstream-0.2.0/src/pyflightstream/commands/solver_initialization.yaml +95 -0
  28. pyflightstream-0.2.0/src/pyflightstream/commands/solver_settings.yaml +120 -0
  29. pyflightstream-0.2.0/src/pyflightstream/commands/streamlines.yaml +63 -0
  30. pyflightstream-0.2.0/src/pyflightstream/commands/surface_sections.yaml +145 -0
  31. pyflightstream-0.2.0/src/pyflightstream/commands/sweeper.yaml +115 -0
  32. pyflightstream-0.2.0/src/pyflightstream/commands/unsteady_solver.yaml +68 -0
  33. pyflightstream-0.2.0/src/pyflightstream/commands/volume_sections.yaml +148 -0
  34. pyflightstream-0.2.0/src/pyflightstream/farfield/__init__.py +613 -0
  35. pyflightstream-0.2.0/src/pyflightstream/files/__init__.py +375 -0
  36. pyflightstream-0.2.0/src/pyflightstream/fsi/__init__.py +57 -0
  37. pyflightstream-0.2.0/src/pyflightstream/fsi/beam.py +417 -0
  38. pyflightstream-0.2.0/src/pyflightstream/fsi/centrifugal.py +418 -0
  39. pyflightstream-0.2.0/src/pyflightstream/fsi/cli.py +206 -0
  40. pyflightstream-0.2.0/src/pyflightstream/fsi/config.py +316 -0
  41. pyflightstream-0.2.0/src/pyflightstream/fsi/driver.py +501 -0
  42. pyflightstream-0.2.0/src/pyflightstream/fsi/kinematics.py +153 -0
  43. pyflightstream-0.2.0/src/pyflightstream/fsi/loads.py +740 -0
  44. pyflightstream-0.2.0/src/pyflightstream/fsi/nodes.py +463 -0
  45. pyflightstream-0.2.0/src/pyflightstream/fsi/state.py +181 -0
  46. pyflightstream-0.2.0/src/pyflightstream/post/__init__.py +18 -0
  47. pyflightstream-0.2.0/src/pyflightstream/post/writers.py +195 -0
  48. pyflightstream-0.2.0/src/pyflightstream/probes/__init__.py +453 -0
  49. pyflightstream-0.2.0/src/pyflightstream/probes/geometry.py +281 -0
  50. pyflightstream-0.2.0/src/pyflightstream/probes/planar.py +477 -0
  51. pyflightstream-0.2.0/src/pyflightstream/qa/__init__.py +59 -0
  52. pyflightstream-0.2.0/src/pyflightstream/qa/cli.py +364 -0
  53. pyflightstream-0.2.0/src/pyflightstream/qa/compat.py +285 -0
  54. pyflightstream-0.2.0/src/pyflightstream/qa/drift.py +406 -0
  55. pyflightstream-0.2.0/src/pyflightstream/qa/geometry.py +421 -0
  56. pyflightstream-0.2.0/src/pyflightstream/qa/physics.py +1744 -0
  57. pyflightstream-0.2.0/src/pyflightstream/qa/probes.py +1023 -0
  58. pyflightstream-0.2.0/src/pyflightstream/qa/references/PHY-01.yaml +38 -0
  59. pyflightstream-0.2.0/src/pyflightstream/qa/references/PHY-02.yaml +28 -0
  60. pyflightstream-0.2.0/src/pyflightstream/qa/references/PHY-05.yaml +29 -0
  61. pyflightstream-0.2.0/src/pyflightstream/qa/references/PHY-06.yaml +90 -0
  62. pyflightstream-0.2.0/src/pyflightstream/qa/references/SMI-01.yaml +28 -0
  63. pyflightstream-0.2.0/src/pyflightstream/qa/references/SMI-02.yaml +28 -0
  64. pyflightstream-0.2.0/src/pyflightstream/qa/specs.py +1405 -0
  65. pyflightstream-0.2.0/src/pyflightstream/reference.py +465 -0
  66. pyflightstream-0.2.0/src/pyflightstream/results/__init__.py +547 -0
  67. pyflightstream-0.2.0/src/pyflightstream/run/__init__.py +677 -0
  68. pyflightstream-0.2.0/src/pyflightstream/script/__init__.py +474 -0
  69. pyflightstream-0.2.0/src/pyflightstream/script/helpers.py +844 -0
  70. pyflightstream-0.2.0/src/pyflightstream/versions.py +191 -0
  71. pyflightstream-0.2.0/src/pyflightstream.egg-info/PKG-INFO +88 -0
  72. pyflightstream-0.2.0/src/pyflightstream.egg-info/SOURCES.txt +107 -0
  73. pyflightstream-0.2.0/src/pyflightstream.egg-info/dependency_links.txt +1 -0
  74. pyflightstream-0.2.0/src/pyflightstream.egg-info/entry_points.txt +3 -0
  75. pyflightstream-0.2.0/src/pyflightstream.egg-info/requires.txt +25 -0
  76. pyflightstream-0.2.0/src/pyflightstream.egg-info/top_level.txt +1 -0
  77. pyflightstream-0.2.0/tests/test_cases.py +104 -0
  78. pyflightstream-0.2.0/tests/test_command_db.py +264 -0
  79. pyflightstream-0.2.0/tests/test_farfield.py +181 -0
  80. pyflightstream-0.2.0/tests/test_files.py +144 -0
  81. pyflightstream-0.2.0/tests/test_fsi_beam.py +117 -0
  82. pyflightstream-0.2.0/tests/test_fsi_centrifugal.py +108 -0
  83. pyflightstream-0.2.0/tests/test_fsi_cli.py +80 -0
  84. pyflightstream-0.2.0/tests/test_fsi_config.py +85 -0
  85. pyflightstream-0.2.0/tests/test_fsi_driver.py +260 -0
  86. pyflightstream-0.2.0/tests/test_fsi_kinematics.py +211 -0
  87. pyflightstream-0.2.0/tests/test_fsi_loads.py +234 -0
  88. pyflightstream-0.2.0/tests/test_fsi_sources.py +102 -0
  89. pyflightstream-0.2.0/tests/test_fsi_wing.py +123 -0
  90. pyflightstream-0.2.0/tests/test_house_style.py +54 -0
  91. pyflightstream-0.2.0/tests/test_matrix_legacy.py +106 -0
  92. pyflightstream-0.2.0/tests/test_package_imports.py +36 -0
  93. pyflightstream-0.2.0/tests/test_post_writers.py +57 -0
  94. pyflightstream-0.2.0/tests/test_probes.py +124 -0
  95. pyflightstream-0.2.0/tests/test_probes_geometry.py +154 -0
  96. pyflightstream-0.2.0/tests/test_probes_planar.py +145 -0
  97. pyflightstream-0.2.0/tests/test_qa_compat.py +169 -0
  98. pyflightstream-0.2.0/tests/test_qa_drift.py +83 -0
  99. pyflightstream-0.2.0/tests/test_qa_geometry.py +126 -0
  100. pyflightstream-0.2.0/tests/test_qa_physics.py +329 -0
  101. pyflightstream-0.2.0/tests/test_qa_probes.py +309 -0
  102. pyflightstream-0.2.0/tests/test_reference.py +93 -0
  103. pyflightstream-0.2.0/tests/test_results.py +139 -0
  104. pyflightstream-0.2.0/tests/test_run.py +78 -0
  105. pyflightstream-0.2.0/tests/test_run_campaign.py +256 -0
  106. pyflightstream-0.2.0/tests/test_run_preproc.py +70 -0
  107. pyflightstream-0.2.0/tests/test_script.py +294 -0
  108. pyflightstream-0.2.0/tests/test_script_helpers.py +251 -0
  109. pyflightstream-0.2.0/tests/test_versions.py +58 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geovana Neves
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,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyflightstream
3
+ Version: 0.2.0
4
+ Summary: Version-aware, didactic Python driver for the FlightStream panel-method solver
5
+ Author: Geovana Neves
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/nevesgeovana/pyflightstream
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pandas
13
+ Requires-Dist: pydantic
14
+ Requires-Dist: pyyaml
15
+ Requires-Dist: xarray
16
+ Provides-Extra: plot
17
+ Requires-Dist: matplotlib; extra == "plot"
18
+ Provides-Extra: geom
19
+ Requires-Dist: trimesh; extra == "geom"
20
+ Requires-Dist: rtree; extra == "geom"
21
+ Requires-Dist: scipy; extra == "geom"
22
+ Provides-Extra: fsi
23
+ Requires-Dist: PyNiteFEA>=3.0; extra == "fsi"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: ruff; extra == "dev"
27
+ Requires-Dist: pre-commit; extra == "dev"
28
+ Requires-Dist: mkdocs-material; extra == "dev"
29
+ Requires-Dist: mkdocstrings[python]; extra == "dev"
30
+ Requires-Dist: mkdocs-gen-files; extra == "dev"
31
+ Requires-Dist: mkdocs-literate-nav; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # pyflightstream
35
+
36
+ Version-aware, didactic Python driver for the FlightStream panel-method solver.
37
+ Successor of the author's legacy research scripts. MIT licensed.
38
+
39
+ Status: pre-alpha, milestone M0 (repository skeleton). The package installs and
40
+ imports, but no functionality ships yet. See the milestone plan below.
41
+
42
+ ## Why this package
43
+
44
+ FlightStream is scripted through an ASCII command file whose commands change
45
+ between solver versions, and not every change reaches the changelog. This
46
+ package makes the FlightStream version an explicit input: every command it
47
+ emits is validated against a per-version command database, and old versions
48
+ are only ever added, never dropped.
49
+
50
+ ## What is each folder?
51
+
52
+ | Folder | Purpose in plain language |
53
+ |---|---|
54
+ | `src/pyflightstream/` | The package itself, one subpackage per pipeline stage |
55
+ | `src/pyflightstream/commands/` | The command database: what exists in which FlightStream version, with manual page citations |
56
+ | `tests/` | Tier 1 tests, runnable anywhere, no FlightStream needed |
57
+ | `reports/` | Committed evidence from licensed machines: which commands actually work (compat) and physics regression results |
58
+ | `docs/` | Documentation source (mkdocs) |
59
+ | `examples/` | Runnable example scripts in percent format |
60
+ | `.claude/skills/` | Maintenance procedures (version updates, command additions, QA runs, releases) |
61
+ | `_private/` | Local only, never committed: FlightStream manuals and research geometry |
62
+
63
+ ## Supported FlightStream versions
64
+
65
+ Planned at launch: 26.000, 26.100, 26.120 (canonical 26.XXX scheme; the last
66
+ digit indexes vendor hotfix builds). The ordered list in
67
+ `src/pyflightstream/commands/_meta.yaml` is the only ordering authority.
68
+
69
+ ## Development setup
70
+
71
+ ```
72
+ pip install -e .[dev]
73
+ pre-commit install
74
+ pytest
75
+ ```
76
+
77
+ Tier 2 (command validity probes) and Tier 3 (physics regression) require a
78
+ local FlightStream license and are documented in CONTRIBUTING.md.
79
+
80
+ ## Milestones
81
+
82
+ M0 skeleton (this) > M1 command database > M2 builder, runner, parser >
83
+ M3 compat report > M4 physics cases > M5 docs and example > v0.1.0 (private).
84
+
85
+ ## License
86
+
87
+ MIT. Contributions must be original or MIT-compatible; code derived from the
88
+ AGPL pyFlightscript package is not accepted. See CONTRIBUTING.md.
@@ -0,0 +1,55 @@
1
+ # pyflightstream
2
+
3
+ Version-aware, didactic Python driver for the FlightStream panel-method solver.
4
+ Successor of the author's legacy research scripts. MIT licensed.
5
+
6
+ Status: pre-alpha, milestone M0 (repository skeleton). The package installs and
7
+ imports, but no functionality ships yet. See the milestone plan below.
8
+
9
+ ## Why this package
10
+
11
+ FlightStream is scripted through an ASCII command file whose commands change
12
+ between solver versions, and not every change reaches the changelog. This
13
+ package makes the FlightStream version an explicit input: every command it
14
+ emits is validated against a per-version command database, and old versions
15
+ are only ever added, never dropped.
16
+
17
+ ## What is each folder?
18
+
19
+ | Folder | Purpose in plain language |
20
+ |---|---|
21
+ | `src/pyflightstream/` | The package itself, one subpackage per pipeline stage |
22
+ | `src/pyflightstream/commands/` | The command database: what exists in which FlightStream version, with manual page citations |
23
+ | `tests/` | Tier 1 tests, runnable anywhere, no FlightStream needed |
24
+ | `reports/` | Committed evidence from licensed machines: which commands actually work (compat) and physics regression results |
25
+ | `docs/` | Documentation source (mkdocs) |
26
+ | `examples/` | Runnable example scripts in percent format |
27
+ | `.claude/skills/` | Maintenance procedures (version updates, command additions, QA runs, releases) |
28
+ | `_private/` | Local only, never committed: FlightStream manuals and research geometry |
29
+
30
+ ## Supported FlightStream versions
31
+
32
+ Planned at launch: 26.000, 26.100, 26.120 (canonical 26.XXX scheme; the last
33
+ digit indexes vendor hotfix builds). The ordered list in
34
+ `src/pyflightstream/commands/_meta.yaml` is the only ordering authority.
35
+
36
+ ## Development setup
37
+
38
+ ```
39
+ pip install -e .[dev]
40
+ pre-commit install
41
+ pytest
42
+ ```
43
+
44
+ Tier 2 (command validity probes) and Tier 3 (physics regression) require a
45
+ local FlightStream license and are documented in CONTRIBUTING.md.
46
+
47
+ ## Milestones
48
+
49
+ M0 skeleton (this) > M1 command database > M2 builder, runner, parser >
50
+ M3 compat report > M4 physics cases > M5 docs and example > v0.1.0 (private).
51
+
52
+ ## License
53
+
54
+ MIT. Contributions must be original or MIT-compatible; code derived from the
55
+ AGPL pyFlightscript package is not accepted. See CONTRIBUTING.md.
@@ -0,0 +1,80 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyflightstream"
7
+ version = "0.2.0"
8
+ description = "Version-aware, didactic Python driver for the FlightStream panel-method solver"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "Geovana Neves"}]
12
+ requires-python = ">=3.11"
13
+ # xarray confirmed as a runtime dependency by the author's far-field
14
+ # extraction instruction of 2026-07-21 (PLN-006 closed): the farfield
15
+ # ledgers live on labeled (station, r, psi) arrays.
16
+ dependencies = [
17
+ "numpy",
18
+ "pandas",
19
+ "pydantic",
20
+ "pyyaml",
21
+ "xarray",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ plot = ["matplotlib"]
26
+ # Geometry gate of the probe planner: containment culling and
27
+ # distance-to-surface queries (license evidence reports/RPT-003).
28
+ geom = ["trimesh", "rtree", "scipy"]
29
+ # FSI structural coupling (M6, DLV-007). PyNiteFEA is MIT; license
30
+ # evidence: reports/RPT-002_pynitefea-license_2026-07-21.md (NFR-02).
31
+ fsi = ["PyNiteFEA>=3.0"]
32
+ dev = [
33
+ "pytest",
34
+ "ruff",
35
+ "pre-commit",
36
+ "mkdocs-material",
37
+ "mkdocstrings[python]",
38
+ "mkdocs-gen-files",
39
+ "mkdocs-literate-nav",
40
+ ]
41
+
42
+ [project.scripts]
43
+ pyfs-qa = "pyflightstream.qa.cli:main"
44
+ pyfs-fsi = "pyflightstream.fsi.cli:main"
45
+
46
+ [project.urls]
47
+ Repository = "https://github.com/nevesgeovana/pyflightstream"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.setuptools.package-data]
53
+ "pyflightstream.commands" = ["*.yaml"]
54
+ "pyflightstream.qa" = ["references/*.yaml"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ markers = [
59
+ "needs_flightstream: requires a local FlightStream installation and license",
60
+ "validity: tier 2 per-command probe tests",
61
+ "physics: tier 3 physics regression matrix (slow)",
62
+ ]
63
+ addopts = "-m 'not needs_flightstream'"
64
+
65
+ [tool.ruff]
66
+ line-length = 100
67
+ target-version = "py311"
68
+ src = ["src"]
69
+
70
+ [tool.ruff.lint]
71
+ select = ["E", "F", "W", "I", "UP", "B", "N", "D"]
72
+ # N803/N806 exempted so aerodynamic symbols (CL, CDi, J, alpha_deg) keep
73
+ # their standard names (SAD Section 12).
74
+ ignore = ["N803", "N806"]
75
+
76
+ [tool.ruff.lint.pydocstyle]
77
+ convention = "numpy"
78
+
79
+ [tool.ruff.lint.per-file-ignores]
80
+ "tests/*" = ["D"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ """pyflightstream: version-aware, didactic Python driver for FlightStream.
2
+
3
+ The package automates the FlightStream panel-method solver through its ASCII
4
+ scripting interface. The FlightStream version is an explicit input: every
5
+ command emitted is validated against the per-version command database in
6
+ ``pyflightstream.commands``.
7
+
8
+ Pipeline layers, dependencies flowing strictly downward:
9
+
10
+ - ``versions``: canonical 26.XXX version identifiers and ordering.
11
+ - ``commands``: the command database and per-version registry.
12
+ - ``script``: the validating ASCII script builder.
13
+ - ``results``: anchor-based parsers for solver output files.
14
+ - ``cases``: simulation and campaign definitions.
15
+ - ``run`` and ``files``: execution, run manifest, managed file layout.
16
+ - ``post``: results into engineering data (sweep assembly, PLTET, exports).
17
+ - ``qa``: probe harness and physics regression tooling.
18
+ - ``fsi``: seam for the future fluid-structure interaction coupling.
19
+
20
+ Milestone M0: skeleton only; functionality arrives from milestone M1 on.
21
+ """
22
+
23
+ __version__ = "0.0.1.dev0"
24
+
25
+ from pyflightstream.reference import help # noqa: E402
26
+
27
+ __all__ = ["__version__", "help"]
@@ -0,0 +1,332 @@
1
+ """Simulation and campaign definitions.
2
+
3
+ Pipeline role: describes what to run. A :class:`SimCase` (identified
4
+ by ``sim_id``) is one solver configuration with its sweep; a
5
+ :class:`Campaign` groups cases with the FlightStream version and the
6
+ executable path, both required and explicit: nothing is read from
7
+ environment variables or guessed (SAD Section 5). Native persistence
8
+ is ``campaign.toml``; the legacy pipe-delimited ``matriz.fs`` matrix
9
+ of the predecessor scripts will be read unchanged, forever, by the
10
+ legacy reader (FR-10, next step of milestone M2).
11
+
12
+ Script recipes are explicitly imported functions satisfying the
13
+ :class:`ScriptRecipe` protocol: ``build(case, script) -> None``. The
14
+ campaign loop specializes the case per sweep point (filling
15
+ :attr:`SimCase.point`) and the recipe translates it into script
16
+ emissions, usually through the curated helpers. Recipe references are
17
+ ``"package.module:function"`` strings, replacing the historical
18
+ import-by-number system (PP-7, FR-12).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import tomllib
24
+ from collections.abc import Callable, Iterator
25
+ from importlib import import_module
26
+ from pathlib import Path
27
+ from typing import Literal, Protocol, runtime_checkable
28
+
29
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
30
+
31
+ from pyflightstream.script import Script
32
+ from pyflightstream.versions import resolve
33
+
34
+ _TAG_PREFIXES = (("alpha", "a"), ("beta", "b"), ("advance_ratio", "j"))
35
+
36
+
37
+ @runtime_checkable
38
+ class ScriptRecipe(Protocol):
39
+ """A function that turns one case point into script emissions.
40
+
41
+ Implementations receive the per-point specialized case (the
42
+ campaign loop fills :attr:`SimCase.point` and stages the geometry)
43
+ and an empty :class:`~pyflightstream.script.Script` bound to the
44
+ campaign's FlightStream version; they emit the whole script,
45
+ usually through the curated helpers. Output files must use paths
46
+ relative to the execution directory, so the collected evidence
47
+ stays inside the managed simulation folder.
48
+ """
49
+
50
+ def __call__(self, case: SimCase, script: Script) -> None:
51
+ """Emit the complete script for one case point."""
52
+ ...
53
+
54
+
55
+ class SweepAxis(BaseModel):
56
+ """The sweep of one case: which axis varies and its values.
57
+
58
+ Attributes
59
+ ----------
60
+ type : str
61
+ ``alpha`` (angle of attack, deg), ``beta`` (side slip, deg),
62
+ ``alpha_beta`` (paired values), or ``advance_ratio``
63
+ (propeller advance ratio J, dimensionless).
64
+ values : list
65
+ Axis values; for ``alpha_beta`` each entry is an
66
+ ``[alpha, beta]`` pair in deg.
67
+ """
68
+
69
+ model_config = ConfigDict(extra="forbid")
70
+
71
+ type: Literal["alpha", "beta", "alpha_beta", "advance_ratio"]
72
+ values: list[float] | list[tuple[float, float]]
73
+
74
+ @model_validator(mode="after")
75
+ def _values_match_the_axis_type(self) -> SweepAxis:
76
+ pairs = self.type == "alpha_beta"
77
+ for value in self.values:
78
+ if pairs != isinstance(value, tuple):
79
+ expected = "[alpha, beta] pairs" if pairs else "scalar values"
80
+ raise ValueError(f"a {self.type} sweep takes {expected}, got {value!r}")
81
+ return self
82
+
83
+ def points(self) -> Iterator[dict[str, float]]:
84
+ """Iterate the sweep as named point coordinates.
85
+
86
+ Yields
87
+ ------
88
+ dict of str to float
89
+ One mapping per point, keyed ``alpha``, ``beta``, or
90
+ ``advance_ratio`` (both keys for ``alpha_beta``).
91
+ """
92
+ for value in self.values:
93
+ if self.type == "alpha_beta":
94
+ alpha, beta = value
95
+ yield {"alpha": alpha, "beta": beta}
96
+ else:
97
+ yield {self.type: value}
98
+
99
+
100
+ def point_tag(point: dict[str, float]) -> str:
101
+ """Return the stable file-name tag of one sweep point.
102
+
103
+ The tag encodes the point coordinates in a fixed axis order with
104
+ signed fixed-width values, for example ``a+02.0_b+00.0``; it names
105
+ the generated script and ends the ``run_id``.
106
+
107
+ Parameters
108
+ ----------
109
+ point : dict of str to float
110
+ Point coordinates as produced by :meth:`SweepAxis.points`.
111
+ """
112
+ parts = [f"{prefix}{point[axis]:+05.1f}" for axis, prefix in _TAG_PREFIXES if axis in point]
113
+ if not parts:
114
+ raise ValueError(f"point {point!r} has no known axis (alpha, beta, advance_ratio)")
115
+ return "_".join(parts)
116
+
117
+
118
+ class ReferenceData(BaseModel):
119
+ """Reference quantities for coefficient normalization.
120
+
121
+ Attributes
122
+ ----------
123
+ area : float
124
+ Reference area S_ref in simulation length units squared.
125
+ length : float
126
+ Reference length L_ref in simulation length units.
127
+ velocity : float, optional
128
+ Reference velocity in m/s; None lets the recipe default it to
129
+ the free-stream velocity (steady runs) or a characteristic
130
+ velocity such as the rotor tip speed (SRC-003 p.201).
131
+ """
132
+
133
+ model_config = ConfigDict(extra="forbid")
134
+
135
+ area: float
136
+ length: float
137
+ velocity: float | None = None
138
+
139
+
140
+ class SolverSettings(BaseModel):
141
+ """Solver runtime settings of one case.
142
+
143
+ Field names match the keyword arguments of
144
+ :func:`pyflightstream.script.helpers.solver_settings`, so recipes
145
+ can forward them directly.
146
+
147
+ Attributes
148
+ ----------
149
+ iterations : int
150
+ Solver iteration limit.
151
+ convergence : float
152
+ Residual threshold declaring convergence (SRC-003 p.200).
153
+ forced_iterations : bool, optional
154
+ Run the full iteration count regardless of convergence.
155
+ boundary_layer : str, optional
156
+ ``LAMINAR``, ``TRANSITIONAL``, or ``TURBULENT``.
157
+ viscous_coupling : bool, optional
158
+ Couple the boundary layer model to the potential solution.
159
+ max_threads : int, optional
160
+ Parallel core count.
161
+ timeout_s : float, optional
162
+ Wall-clock limit for one point's solver process; enforced by
163
+ the executor, not by FlightStream.
164
+ """
165
+
166
+ model_config = ConfigDict(extra="forbid")
167
+
168
+ iterations: int = 500
169
+ convergence: float = 1e-5
170
+ forced_iterations: bool | None = None
171
+ boundary_layer: str | None = None
172
+ viscous_coupling: bool | None = None
173
+ max_threads: int | None = None
174
+ timeout_s: float | None = None
175
+
176
+
177
+ class SimCase(BaseModel):
178
+ """One solver configuration with its sweep (SAD Section 5).
179
+
180
+ Attributes
181
+ ----------
182
+ sim_id : str
183
+ Case identity; also names the managed folder
184
+ ``sims/sim_<sim_id>``.
185
+ aircraft : str
186
+ Aircraft or configuration name.
187
+ description : str
188
+ Free-text description.
189
+ reynolds : float, optional
190
+ Chord Reynolds number of the condition.
191
+ mach : float, optional
192
+ Free-stream Mach number.
193
+ velocity : float, optional
194
+ Free-stream velocity in m/s.
195
+ geometry : str, optional
196
+ Path of the simulation (.fsm) file; the campaign loop stages
197
+ it into ``inputs/`` and rewrites this field to the staged
198
+ copy, so recipes OPEN exactly what the manifest hashed.
199
+ sweep : SweepAxis
200
+ The sweep of this case.
201
+ reference : ReferenceData, optional
202
+ Coefficient normalization references.
203
+ solver : SolverSettings
204
+ Runtime settings; defaults apply when omitted.
205
+ recipe : str
206
+ Script recipe reference, ``"package.module:function"``, or a
207
+ name registered with the campaign loop.
208
+ variables : dict
209
+ Free per-case variables for the recipe (strings, numbers, or
210
+ booleans), for example a symmetry declaration.
211
+ outputs : list of str
212
+ Output files the recipe's script exports, relative to the
213
+ execution directory; the loop collects them into ``raw/`` and
214
+ a missing one marks the point FAILED_INCOMPLETE_OUTPUT.
215
+ point : dict of str to float
216
+ The current sweep point; filled by the campaign loop before
217
+ the recipe builds, empty on the authored case.
218
+ """
219
+
220
+ model_config = ConfigDict(extra="forbid")
221
+
222
+ sim_id: str
223
+ aircraft: str
224
+ description: str = ""
225
+ reynolds: float | None = None
226
+ mach: float | None = None
227
+ velocity: float | None = None
228
+ geometry: str | None = None
229
+ sweep: SweepAxis
230
+ reference: ReferenceData | None = None
231
+ solver: SolverSettings = Field(default_factory=SolverSettings)
232
+ recipe: str
233
+ variables: dict[str, str | float | int | bool] = Field(default_factory=dict)
234
+ outputs: list[str] = Field(default_factory=list)
235
+ point: dict[str, float] = Field(default_factory=dict)
236
+
237
+
238
+ class Campaign(BaseModel):
239
+ """A named group of cases bound to one FlightStream installation.
240
+
241
+ Attributes
242
+ ----------
243
+ name : str
244
+ Campaign name; prefixes every ``run_id``.
245
+ fs_version : str
246
+ FlightStream version, canonical or alias; validated against
247
+ the registered versions at load time, resolved to canonical in
248
+ the manifest.
249
+ fs_exe : str
250
+ Explicit path of the FlightStream executable; existence is
251
+ checked by the executor at construction, not here, so a
252
+ campaign file can be authored away from the licensed machine.
253
+ sims : list of SimCase
254
+ The cases of the campaign.
255
+ """
256
+
257
+ model_config = ConfigDict(extra="forbid")
258
+
259
+ name: str
260
+ fs_version: str
261
+ fs_exe: str
262
+ sims: list[SimCase]
263
+
264
+ @field_validator("fs_version")
265
+ @classmethod
266
+ def _version_is_registered(cls, value: str) -> str:
267
+ resolve(value)
268
+ return value
269
+
270
+
271
+ def load_campaign(path: str | Path) -> Campaign:
272
+ """Load and validate a ``campaign.toml`` file.
273
+
274
+ The file holds one ``[campaign]`` table (name, fs_version,
275
+ fs_exe) and one ``[[sim]]`` array entry per case, as in SAD
276
+ Section 5.
277
+
278
+ Parameters
279
+ ----------
280
+ path : str or Path
281
+ Location of the TOML file.
282
+
283
+ Returns
284
+ -------
285
+ Campaign
286
+ Validated campaign; version aliases are checked against the
287
+ registered versions immediately, so a typo fails at load
288
+ time, not at the first point.
289
+ """
290
+ with open(path, "rb") as handle:
291
+ data = tomllib.load(handle)
292
+ if "campaign" not in data:
293
+ raise ValueError(
294
+ f"{path} has no [campaign] table; campaign.toml needs [campaign] with "
295
+ "name, fs_version, and fs_exe, plus one [[sim]] entry per case"
296
+ )
297
+ return Campaign(**data["campaign"], sims=data.get("sim", []))
298
+
299
+
300
+ def resolve_recipe(reference: str) -> Callable[[SimCase, Script], None]:
301
+ """Import the recipe function a reference string names.
302
+
303
+ Parameters
304
+ ----------
305
+ reference : str
306
+ ``"package.module:function"``; the module must be importable
307
+ and the attribute callable. Explicit references replace the
308
+ historical import-by-number system (PP-7, FR-12).
309
+
310
+ Returns
311
+ -------
312
+ callable
313
+ The recipe function, satisfying :class:`ScriptRecipe`.
314
+ """
315
+ module_name, separator, function_name = reference.partition(":")
316
+ if not separator or not module_name or not function_name:
317
+ raise ValueError(
318
+ f"recipe reference {reference!r} is not of the form 'package.module:function'"
319
+ )
320
+ try:
321
+ module = import_module(module_name)
322
+ except ImportError as error:
323
+ raise ValueError(
324
+ f"recipe module {module_name!r} cannot be imported: {error}. Recipes are "
325
+ "explicitly imported functions; check the module path and the environment."
326
+ ) from error
327
+ recipe = getattr(module, function_name, None)
328
+ if not callable(recipe):
329
+ raise ValueError(
330
+ f"recipe {reference!r} does not name a callable in {module_name!r}; found {recipe!r}"
331
+ )
332
+ return recipe