aerorare-studio 0.1.1__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.
- aerorare_studio-0.1.1/LICENSE +21 -0
- aerorare_studio-0.1.1/PKG-INFO +172 -0
- aerorare_studio-0.1.1/README.md +134 -0
- aerorare_studio-0.1.1/aerorare/__init__.py +25 -0
- aerorare_studio-0.1.1/aerorare/analysis.py +152 -0
- aerorare_studio-0.1.1/aerorare/archive.py +146 -0
- aerorare_studio-0.1.1/aerorare/atmosphere.py +120 -0
- aerorare_studio-0.1.1/aerorare/blmesh.py +188 -0
- aerorare_studio-0.1.1/aerorare/boundaries.py +122 -0
- aerorare_studio-0.1.1/aerorare/cad.py +169 -0
- aerorare_studio-0.1.1/aerorare/cli.py +1200 -0
- aerorare_studio-0.1.1/aerorare/freestream.py +102 -0
- aerorare_studio-0.1.1/aerorare/gas.py +95 -0
- aerorare_studio-0.1.1/aerorare/geometry.py +301 -0
- aerorare_studio-0.1.1/aerorare/geometry3d.py +427 -0
- aerorare_studio-0.1.1/aerorare/gui.py +1352 -0
- aerorare_studio-0.1.1/aerorare/hpc.py +209 -0
- aerorare_studio-0.1.1/aerorare/meshing.py +330 -0
- aerorare_studio-0.1.1/aerorare/postprocess.py +350 -0
- aerorare_studio-0.1.1/aerorare/project.py +214 -0
- aerorare_studio-0.1.1/aerorare/regime.py +98 -0
- aerorare_studio-0.1.1/aerorare/report.py +425 -0
- aerorare_studio-0.1.1/aerorare/results.py +389 -0
- aerorare_studio-0.1.1/aerorare/runner.py +148 -0
- aerorare_studio-0.1.1/aerorare/sparta.py +530 -0
- aerorare_studio-0.1.1/aerorare/su2.py +217 -0
- aerorare_studio-0.1.1/aerorare/tools.py +72 -0
- aerorare_studio-0.1.1/aerorare/valcases.py +302 -0
- aerorare_studio-0.1.1/aerorare/validation.py +169 -0
- aerorare_studio-0.1.1/aerorare_studio.egg-info/PKG-INFO +172 -0
- aerorare_studio-0.1.1/aerorare_studio.egg-info/SOURCES.txt +43 -0
- aerorare_studio-0.1.1/aerorare_studio.egg-info/dependency_links.txt +1 -0
- aerorare_studio-0.1.1/aerorare_studio.egg-info/entry_points.txt +2 -0
- aerorare_studio-0.1.1/aerorare_studio.egg-info/requires.txt +18 -0
- aerorare_studio-0.1.1/aerorare_studio.egg-info/top_level.txt +1 -0
- aerorare_studio-0.1.1/pyproject.toml +48 -0
- aerorare_studio-0.1.1/setup.cfg +4 -0
- aerorare_studio-0.1.1/tests/test_3d_hpc.py +247 -0
- aerorare_studio-0.1.1/tests/test_analysis.py +162 -0
- aerorare_studio-0.1.1/tests/test_archive.py +89 -0
- aerorare_studio-0.1.1/tests/test_core.py +399 -0
- aerorare_studio-0.1.1/tests/test_repair.py +86 -0
- aerorare_studio-0.1.1/tests/test_results.py +179 -0
- aerorare_studio-0.1.1/tests/test_results3d_report.py +179 -0
- aerorare_studio-0.1.1/tests/test_valcases.py +91 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nsclk
|
|
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,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aerorare-studio
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Open-source CFD workbench for hypersonic, reentry and rarefied-flow analysis (SU2 + SPARTA + Gmsh + ParaView orchestration)
|
|
5
|
+
Author: nsclk
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/nsclk/aerorare-studio
|
|
8
|
+
Project-URL: Changelog, https://github.com/nsclk/aerorare-studio/blob/main/DEVELOPMENT.md
|
|
9
|
+
Keywords: cfd,hypersonic,dsmc,su2,sparta,reentry,rarefied-gas,aerothermodynamics
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.20
|
|
25
|
+
Requires-Dist: PyYAML>=5.4
|
|
26
|
+
Provides-Extra: mesh
|
|
27
|
+
Requires-Dist: gmsh>=4.11; extra == "mesh"
|
|
28
|
+
Provides-Extra: cad
|
|
29
|
+
Requires-Dist: cadquery>=2.4; extra == "cad"
|
|
30
|
+
Provides-Extra: gui
|
|
31
|
+
Requires-Dist: PySide6>=6.5; extra == "gui"
|
|
32
|
+
Requires-Dist: vtk<9.5,>=9.2; extra == "gui"
|
|
33
|
+
Provides-Extra: results
|
|
34
|
+
Requires-Dist: vtk<9.5,>=9.2; extra == "results"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# AeroRare Studio
|
|
40
|
+
|
|
41
|
+
Open-source CFD workbench for **hypersonic**, **atmospheric reentry** and
|
|
42
|
+
**rarefied-flow / DSMC** analysis. AeroRare Studio is not a solver: it
|
|
43
|
+
orchestrates [SU2](https://su2code.github.io/) (continuum CFD),
|
|
44
|
+
[SPARTA](https://sparta.github.io/) (DSMC), [Gmsh](https://gmsh.info/)
|
|
45
|
+
(meshing) and ParaView/VTK (post-processing) behind a single physics-first
|
|
46
|
+
workflow. You describe the physical problem; the studio generates geometry,
|
|
47
|
+
meshes, solver input files, run commands and reports.
|
|
48
|
+
|
|
49
|
+
The full product concept is described (in Turkish) in
|
|
50
|
+
[`docs/hyperrare_studio_spec.md`](docs/hyperrare_studio_spec.md). This package is the
|
|
51
|
+
engine layer: every screen of the planned GUI maps onto a module here.
|
|
52
|
+
|
|
53
|
+
## Toolchain status (this machine)
|
|
54
|
+
|
|
55
|
+
All five external tools are installed and detected by `aerorare tools`:
|
|
56
|
+
SU2 8.0.0, SPARTA (24 Sep 2025, built from `../sparta-master`), Gmsh 4.14,
|
|
57
|
+
ParaView 5.11.2 (`pvpython` in the app bundle), Open MPI 5.0.8. Python side:
|
|
58
|
+
CadQuery 2.5.2 (bundled OCCT 7.7 kernel via OCP) and VTK 9.3.1; OCCT 7.9.3
|
|
59
|
+
(brew) and FreeCAD 1.x (`/Applications/FreeCAD.app`) for CAD work.
|
|
60
|
+
|
|
61
|
+
## Status — v0.1.0 (engine + CLI)
|
|
62
|
+
|
|
63
|
+
| Concept (spec §) | Module | State |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| System check (§2) | `aerorare/tools.py` | ✅ detects SU2, SPARTA, Gmsh, ParaView, MPI |
|
|
66
|
+
| Project Setup (§3) | `aerorare/project.py` | ✅ project.yaml, folder layout, run versioning |
|
|
67
|
+
| Geometry Studio (§4) | `aerorare/geometry.py` | ✅ parametric 2D/axisymmetric templates (blunt cone, capsule, sphere, cylinder, wedge, flat plate); CAD import planned |
|
|
68
|
+
| Geometry Health Check (§5) | `Contour.health_check` | ✅ basic checks |
|
|
69
|
+
| Boundary Assignment (§6) | `aerorare/boundaries.py` | ✅ types, auto-detection, consistency check, colour legend |
|
|
70
|
+
| Physics Wizard (§7) | `aerorare/freestream.py`, `aerorare/atmosphere.py`, `aerorare/gas.py` | ✅ US76 atmosphere, altitude & direct modes, gas models |
|
|
71
|
+
| Regime Selector (§8) | `aerorare/regime.py` | ✅ Kn bands, solver recommendation, override warnings |
|
|
72
|
+
| Mesh Studio (§9) | `aerorare/meshing.py`, `aerorare/sparta.py` | ✅ mesh plan + Gmsh .geo + SU2 mesh; DSMC grid plan w/ λ checks |
|
|
73
|
+
| Solver Setup (§10) | `aerorare/su2.py`, `aerorare/sparta.py` | ✅ SU2 .cfg and SPARTA input generation |
|
|
74
|
+
| Case Validation (§11) | `aerorare/validation.py` | ✅ Ready/Warning/Error gate |
|
|
75
|
+
| Run Monitor (§12) | `aerorare/runner.py` | ✅ `aerorare run`: MPI launch + live residual/particle tracking |
|
|
76
|
+
| Results Viewer (§13) | `aerorare/postprocess.py` | ✅ pvpython offscreen contours (percentile-clamped, body-framed) + dependency-free SVG wall/convergence plots |
|
|
77
|
+
| Comparison Studio (§14) | `results.compare_surfaces` | ✅ common arc-length resampling, % differences, overlay plots |
|
|
78
|
+
| Common Result Model (§15) | `aerorare/results.py` | ✅ SU2 CSV/VTU + SPARTA dump parsers → shared surface/volume model |
|
|
79
|
+
| Report Generator (§16) | `aerorare/report.py` | ✅ Markdown case report |
|
|
80
|
+
| 3D geometries (§4) | `aerorare/geometry3d.py` | ✅ CadQuery solids (sphere, blunt cone, capsule, cone-cylinder), revolve-from-contour, STL export, watertight check |
|
|
81
|
+
| CAD import (§4.2) | `aerorare/cad.py` | ✅ STEP/IGES/BREP/STL/OBJ, unit scaling, health report |
|
|
82
|
+
| SPARTA 3D | `sparta.write_surface_file_3d` etc. | ✅ triangle surfaces + 3D grid plan + input (verified with a live 3D run) |
|
|
83
|
+
| SSH / SLURM environments (§12) | `aerorare/hpc.py` | ✅ `aerorare env` + remote stage/submit/fetch, sbatch generation |
|
|
84
|
+
| Desktop GUI | `aerorare/gui.py` | ✅ MVP: PySide6, software-rendered geometry viewport, clickable boundary assignment (§6 colour legend), workflow sidebar, live run log, results gallery (`aerorare gui`) |
|
|
85
|
+
| SU2 NEMO (§7.2) | `su2.py` (`--thermal two-temperature`) | ✅ AIR-5 two-temperature configs patterned on SU2's nonequilibrium test cases; NEMO_EULER verified converging on SU2 8.0.0 |
|
|
86
|
+
| 3D SU2 mesh from STEP | `meshing.py` (`MeshPlan3D`) | ✅ gmsh OpenCASCADE boolean (STEP body − sphere farfield), body/farfield markers, distance-field refinement; verified with a live 3D SU2 run |
|
|
87
|
+
| 3D boundary-layer inflation | `blmesh.py` (`generate --bl`) | ✅ prism layers via gmsh `extrudeBoundaryLayer` (smooth bodies; sharp edges fall back to isotropic); hybrid prism+tet mesh verified in SU2 |
|
|
88
|
+
| 3D results (§13–15) | `results.py` 3D loaders | ✅ meridional collapse (x, r → s) + azimuthal averaging, slice/surface pvpython renders, SU2-vs-SPARTA comparison on the meridian |
|
|
89
|
+
| HTML/PDF reports (§16) | `report.py` (`report --format html/pdf`) | ✅ dependency-free MD→HTML with figures embedded (base64/inline SVG); PDF via Qt QPdfWriter |
|
|
90
|
+
| Geometry auto-repair (§5) | `geometry3d.repair_mesh` (`new --repair`) | ✅ duplicate removal, winding fix, small-hole fill |
|
|
91
|
+
| Docker environment (§12) | `hpc.docker_command` (`env --kind docker`) | ✅ containerised runs with live monitoring (command-generation tested; docker not installed here) |
|
|
92
|
+
| Restart flows | `generate --restart` | ✅ SU2 RESTART_SOL + staged 2nd-order upgrade; SPARTA read_restart continuation (verified live) |
|
|
93
|
+
| Analysis tools (§13.3) | `analysis.py` (`aerorare analyze`) | ✅ stagnation region, shock stand-off vs Billig, centerline probes |
|
|
94
|
+
| Mesh Studio GUI | `gui.py` Mesh page | ✅ plan editor with persisted overrides + live residual/particle charts |
|
|
95
|
+
|
|
96
|
+
Development history and roadmap: [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cd aerorare_studio
|
|
102
|
+
python3 -m pip install -e ".[dev]" # engine + tests
|
|
103
|
+
# optional external tools:
|
|
104
|
+
# gmsh -> mesh generation (brew install gmsh / pip install gmsh)
|
|
105
|
+
# SU2 -> continuum solver (https://su2code.github.io/download.html)
|
|
106
|
+
# SPARTA -> DSMC solver (https://sparta.github.io/download.html)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Building SPARTA from source (macOS)
|
|
110
|
+
|
|
111
|
+
The vendored tree in `../sparta-master` builds with CMake; clang/libc++
|
|
112
|
+
needs the C++11 hash-map flag:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd ../sparta-master && mkdir -p build install && cd build
|
|
116
|
+
cmake -DCMAKE_INSTALL_PREFIX=../install -DBUILD_MPI=ON \
|
|
117
|
+
-DCMAKE_CXX_FLAGS="-DSPARTA_UNORDERED_MAP" ../cmake
|
|
118
|
+
make -j8 && make install
|
|
119
|
+
ln -sf "$PWD/../install/bin/spa_" /opt/homebrew/bin/spa_
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The studio locates SPARTA's species/VSS data files (`air.species`,
|
|
123
|
+
`air.vss`, ...) next to the executable or via `$SPARTA_DATA_DIR`, and
|
|
124
|
+
copies them into each run directory.
|
|
125
|
+
|
|
126
|
+
### Doc-compliance notes (validated against the vendored sources)
|
|
127
|
+
|
|
128
|
+
* **SPARTA** — generated inputs follow the manual and the `examples/axi`
|
|
129
|
+
case: `boundary oo ar pp` + box `ylo=0` + `global ... weight cell radius`
|
|
130
|
+
for axisymmetric models (manual §6.2); surface files obey the `read_surf`
|
|
131
|
+
right-hand orientation rule (outward normal `N = z × (p2−p1)`) and the
|
|
132
|
+
watertight rule (endpoints exactly on a box edge — near-axis points are
|
|
133
|
+
snapped to `y=0`); open profiles ending off-axis are auto-closed at the
|
|
134
|
+
base. Dump columns use `xlo/ylo/xhi/yhi` (there is no `xc/yc`).
|
|
135
|
+
* **SU2** — every emitted option exists in `SU2-master/config_template.cfg`;
|
|
136
|
+
viscous cases set `INIT_OPTION= TD_CONDITIONS`, `FREESTREAM_OPTION=
|
|
137
|
+
TEMPERATURE_FS` plus `REYNOLDS_NUMBER` (required by SU2 8.x for
|
|
138
|
+
NAVIER_STOKES). Verified by running SU2_CFD 8.0.0 on a generated case.
|
|
139
|
+
* **Gmsh** — `.geo` scripts use OpenCASCADE factory + `Physical Curve`
|
|
140
|
+
groups whose names become SU2 markers via `gmsh -2 -format su2`.
|
|
141
|
+
Verified with Gmsh 4.14 (markers arrive in the `.su2` mesh intact).
|
|
142
|
+
|
|
143
|
+
## Quick start — Mach 15 blunt-body reentry at 80 km
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
aerorare tools # check SU2/SPARTA/Gmsh/MPI
|
|
147
|
+
aerorare new reentry --template blunt-cone \
|
|
148
|
+
--param nose_radius=0.05 --param half_angle_deg=15 --param length=0.3
|
|
149
|
+
aerorare physics reentry --mach 15 --altitude 80000 --wall-temp 1000
|
|
150
|
+
aerorare regime reentry # -> slip regime, su2+sparta
|
|
151
|
+
aerorare generate reentry --quick # coarse preview setup (drop --quick for production)
|
|
152
|
+
aerorare validate reentry # pre-run gate
|
|
153
|
+
aerorare run reentry --ranks 4 # SU2 + SPARTA with live monitoring
|
|
154
|
+
aerorare results reentry # contour PNGs, wall SVGs, SU2-vs-SPARTA comparison
|
|
155
|
+
aerorare report reentry # reports/case_report.md
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
With the full toolchain the quick demo takes ~20 s of solver time and
|
|
159
|
+
produces, under `results/run_NNN/`: ParaView Mach/pressure/temperature/
|
|
160
|
+
density contours framed on the body, SU2 wall pressure / heat-flux / Cp and
|
|
161
|
+
convergence plots, SPARTA wall distributions, overlaid SU2-vs-SPARTA
|
|
162
|
+
comparison charts and a `comparison.md` with mean/max percentage
|
|
163
|
+
differences.
|
|
164
|
+
|
|
165
|
+
Everything needed to reproduce a case lives in `project.yaml`; each
|
|
166
|
+
`generate` allocates a fresh `solvers/run_NNN/` so earlier setups are kept.
|
|
167
|
+
|
|
168
|
+
## Tests
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python3 -m pytest tests/ -q
|
|
172
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# AeroRare Studio
|
|
2
|
+
|
|
3
|
+
Open-source CFD workbench for **hypersonic**, **atmospheric reentry** and
|
|
4
|
+
**rarefied-flow / DSMC** analysis. AeroRare Studio is not a solver: it
|
|
5
|
+
orchestrates [SU2](https://su2code.github.io/) (continuum CFD),
|
|
6
|
+
[SPARTA](https://sparta.github.io/) (DSMC), [Gmsh](https://gmsh.info/)
|
|
7
|
+
(meshing) and ParaView/VTK (post-processing) behind a single physics-first
|
|
8
|
+
workflow. You describe the physical problem; the studio generates geometry,
|
|
9
|
+
meshes, solver input files, run commands and reports.
|
|
10
|
+
|
|
11
|
+
The full product concept is described (in Turkish) in
|
|
12
|
+
[`docs/hyperrare_studio_spec.md`](docs/hyperrare_studio_spec.md). This package is the
|
|
13
|
+
engine layer: every screen of the planned GUI maps onto a module here.
|
|
14
|
+
|
|
15
|
+
## Toolchain status (this machine)
|
|
16
|
+
|
|
17
|
+
All five external tools are installed and detected by `aerorare tools`:
|
|
18
|
+
SU2 8.0.0, SPARTA (24 Sep 2025, built from `../sparta-master`), Gmsh 4.14,
|
|
19
|
+
ParaView 5.11.2 (`pvpython` in the app bundle), Open MPI 5.0.8. Python side:
|
|
20
|
+
CadQuery 2.5.2 (bundled OCCT 7.7 kernel via OCP) and VTK 9.3.1; OCCT 7.9.3
|
|
21
|
+
(brew) and FreeCAD 1.x (`/Applications/FreeCAD.app`) for CAD work.
|
|
22
|
+
|
|
23
|
+
## Status — v0.1.0 (engine + CLI)
|
|
24
|
+
|
|
25
|
+
| Concept (spec §) | Module | State |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| System check (§2) | `aerorare/tools.py` | ✅ detects SU2, SPARTA, Gmsh, ParaView, MPI |
|
|
28
|
+
| Project Setup (§3) | `aerorare/project.py` | ✅ project.yaml, folder layout, run versioning |
|
|
29
|
+
| Geometry Studio (§4) | `aerorare/geometry.py` | ✅ parametric 2D/axisymmetric templates (blunt cone, capsule, sphere, cylinder, wedge, flat plate); CAD import planned |
|
|
30
|
+
| Geometry Health Check (§5) | `Contour.health_check` | ✅ basic checks |
|
|
31
|
+
| Boundary Assignment (§6) | `aerorare/boundaries.py` | ✅ types, auto-detection, consistency check, colour legend |
|
|
32
|
+
| Physics Wizard (§7) | `aerorare/freestream.py`, `aerorare/atmosphere.py`, `aerorare/gas.py` | ✅ US76 atmosphere, altitude & direct modes, gas models |
|
|
33
|
+
| Regime Selector (§8) | `aerorare/regime.py` | ✅ Kn bands, solver recommendation, override warnings |
|
|
34
|
+
| Mesh Studio (§9) | `aerorare/meshing.py`, `aerorare/sparta.py` | ✅ mesh plan + Gmsh .geo + SU2 mesh; DSMC grid plan w/ λ checks |
|
|
35
|
+
| Solver Setup (§10) | `aerorare/su2.py`, `aerorare/sparta.py` | ✅ SU2 .cfg and SPARTA input generation |
|
|
36
|
+
| Case Validation (§11) | `aerorare/validation.py` | ✅ Ready/Warning/Error gate |
|
|
37
|
+
| Run Monitor (§12) | `aerorare/runner.py` | ✅ `aerorare run`: MPI launch + live residual/particle tracking |
|
|
38
|
+
| Results Viewer (§13) | `aerorare/postprocess.py` | ✅ pvpython offscreen contours (percentile-clamped, body-framed) + dependency-free SVG wall/convergence plots |
|
|
39
|
+
| Comparison Studio (§14) | `results.compare_surfaces` | ✅ common arc-length resampling, % differences, overlay plots |
|
|
40
|
+
| Common Result Model (§15) | `aerorare/results.py` | ✅ SU2 CSV/VTU + SPARTA dump parsers → shared surface/volume model |
|
|
41
|
+
| Report Generator (§16) | `aerorare/report.py` | ✅ Markdown case report |
|
|
42
|
+
| 3D geometries (§4) | `aerorare/geometry3d.py` | ✅ CadQuery solids (sphere, blunt cone, capsule, cone-cylinder), revolve-from-contour, STL export, watertight check |
|
|
43
|
+
| CAD import (§4.2) | `aerorare/cad.py` | ✅ STEP/IGES/BREP/STL/OBJ, unit scaling, health report |
|
|
44
|
+
| SPARTA 3D | `sparta.write_surface_file_3d` etc. | ✅ triangle surfaces + 3D grid plan + input (verified with a live 3D run) |
|
|
45
|
+
| SSH / SLURM environments (§12) | `aerorare/hpc.py` | ✅ `aerorare env` + remote stage/submit/fetch, sbatch generation |
|
|
46
|
+
| Desktop GUI | `aerorare/gui.py` | ✅ MVP: PySide6, software-rendered geometry viewport, clickable boundary assignment (§6 colour legend), workflow sidebar, live run log, results gallery (`aerorare gui`) |
|
|
47
|
+
| SU2 NEMO (§7.2) | `su2.py` (`--thermal two-temperature`) | ✅ AIR-5 two-temperature configs patterned on SU2's nonequilibrium test cases; NEMO_EULER verified converging on SU2 8.0.0 |
|
|
48
|
+
| 3D SU2 mesh from STEP | `meshing.py` (`MeshPlan3D`) | ✅ gmsh OpenCASCADE boolean (STEP body − sphere farfield), body/farfield markers, distance-field refinement; verified with a live 3D SU2 run |
|
|
49
|
+
| 3D boundary-layer inflation | `blmesh.py` (`generate --bl`) | ✅ prism layers via gmsh `extrudeBoundaryLayer` (smooth bodies; sharp edges fall back to isotropic); hybrid prism+tet mesh verified in SU2 |
|
|
50
|
+
| 3D results (§13–15) | `results.py` 3D loaders | ✅ meridional collapse (x, r → s) + azimuthal averaging, slice/surface pvpython renders, SU2-vs-SPARTA comparison on the meridian |
|
|
51
|
+
| HTML/PDF reports (§16) | `report.py` (`report --format html/pdf`) | ✅ dependency-free MD→HTML with figures embedded (base64/inline SVG); PDF via Qt QPdfWriter |
|
|
52
|
+
| Geometry auto-repair (§5) | `geometry3d.repair_mesh` (`new --repair`) | ✅ duplicate removal, winding fix, small-hole fill |
|
|
53
|
+
| Docker environment (§12) | `hpc.docker_command` (`env --kind docker`) | ✅ containerised runs with live monitoring (command-generation tested; docker not installed here) |
|
|
54
|
+
| Restart flows | `generate --restart` | ✅ SU2 RESTART_SOL + staged 2nd-order upgrade; SPARTA read_restart continuation (verified live) |
|
|
55
|
+
| Analysis tools (§13.3) | `analysis.py` (`aerorare analyze`) | ✅ stagnation region, shock stand-off vs Billig, centerline probes |
|
|
56
|
+
| Mesh Studio GUI | `gui.py` Mesh page | ✅ plan editor with persisted overrides + live residual/particle charts |
|
|
57
|
+
|
|
58
|
+
Development history and roadmap: [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd aerorare_studio
|
|
64
|
+
python3 -m pip install -e ".[dev]" # engine + tests
|
|
65
|
+
# optional external tools:
|
|
66
|
+
# gmsh -> mesh generation (brew install gmsh / pip install gmsh)
|
|
67
|
+
# SU2 -> continuum solver (https://su2code.github.io/download.html)
|
|
68
|
+
# SPARTA -> DSMC solver (https://sparta.github.io/download.html)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Building SPARTA from source (macOS)
|
|
72
|
+
|
|
73
|
+
The vendored tree in `../sparta-master` builds with CMake; clang/libc++
|
|
74
|
+
needs the C++11 hash-map flag:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd ../sparta-master && mkdir -p build install && cd build
|
|
78
|
+
cmake -DCMAKE_INSTALL_PREFIX=../install -DBUILD_MPI=ON \
|
|
79
|
+
-DCMAKE_CXX_FLAGS="-DSPARTA_UNORDERED_MAP" ../cmake
|
|
80
|
+
make -j8 && make install
|
|
81
|
+
ln -sf "$PWD/../install/bin/spa_" /opt/homebrew/bin/spa_
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The studio locates SPARTA's species/VSS data files (`air.species`,
|
|
85
|
+
`air.vss`, ...) next to the executable or via `$SPARTA_DATA_DIR`, and
|
|
86
|
+
copies them into each run directory.
|
|
87
|
+
|
|
88
|
+
### Doc-compliance notes (validated against the vendored sources)
|
|
89
|
+
|
|
90
|
+
* **SPARTA** — generated inputs follow the manual and the `examples/axi`
|
|
91
|
+
case: `boundary oo ar pp` + box `ylo=0` + `global ... weight cell radius`
|
|
92
|
+
for axisymmetric models (manual §6.2); surface files obey the `read_surf`
|
|
93
|
+
right-hand orientation rule (outward normal `N = z × (p2−p1)`) and the
|
|
94
|
+
watertight rule (endpoints exactly on a box edge — near-axis points are
|
|
95
|
+
snapped to `y=0`); open profiles ending off-axis are auto-closed at the
|
|
96
|
+
base. Dump columns use `xlo/ylo/xhi/yhi` (there is no `xc/yc`).
|
|
97
|
+
* **SU2** — every emitted option exists in `SU2-master/config_template.cfg`;
|
|
98
|
+
viscous cases set `INIT_OPTION= TD_CONDITIONS`, `FREESTREAM_OPTION=
|
|
99
|
+
TEMPERATURE_FS` plus `REYNOLDS_NUMBER` (required by SU2 8.x for
|
|
100
|
+
NAVIER_STOKES). Verified by running SU2_CFD 8.0.0 on a generated case.
|
|
101
|
+
* **Gmsh** — `.geo` scripts use OpenCASCADE factory + `Physical Curve`
|
|
102
|
+
groups whose names become SU2 markers via `gmsh -2 -format su2`.
|
|
103
|
+
Verified with Gmsh 4.14 (markers arrive in the `.su2` mesh intact).
|
|
104
|
+
|
|
105
|
+
## Quick start — Mach 15 blunt-body reentry at 80 km
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
aerorare tools # check SU2/SPARTA/Gmsh/MPI
|
|
109
|
+
aerorare new reentry --template blunt-cone \
|
|
110
|
+
--param nose_radius=0.05 --param half_angle_deg=15 --param length=0.3
|
|
111
|
+
aerorare physics reentry --mach 15 --altitude 80000 --wall-temp 1000
|
|
112
|
+
aerorare regime reentry # -> slip regime, su2+sparta
|
|
113
|
+
aerorare generate reentry --quick # coarse preview setup (drop --quick for production)
|
|
114
|
+
aerorare validate reentry # pre-run gate
|
|
115
|
+
aerorare run reentry --ranks 4 # SU2 + SPARTA with live monitoring
|
|
116
|
+
aerorare results reentry # contour PNGs, wall SVGs, SU2-vs-SPARTA comparison
|
|
117
|
+
aerorare report reentry # reports/case_report.md
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
With the full toolchain the quick demo takes ~20 s of solver time and
|
|
121
|
+
produces, under `results/run_NNN/`: ParaView Mach/pressure/temperature/
|
|
122
|
+
density contours framed on the body, SU2 wall pressure / heat-flux / Cp and
|
|
123
|
+
convergence plots, SPARTA wall distributions, overlaid SU2-vs-SPARTA
|
|
124
|
+
comparison charts and a `comparison.md` with mean/max percentage
|
|
125
|
+
differences.
|
|
126
|
+
|
|
127
|
+
Everything needed to reproduce a case lives in `project.yaml`; each
|
|
128
|
+
`generate` allocates a fresh `solvers/run_NNN/` so earlier setups are kept.
|
|
129
|
+
|
|
130
|
+
## Tests
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python3 -m pytest tests/ -q
|
|
134
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""AeroRare Studio — open-source CFD workbench for hypersonic, reentry and
|
|
2
|
+
rarefied-flow analysis.
|
|
3
|
+
|
|
4
|
+
AeroRare Studio is not a solver. It orchestrates SU2 (continuum CFD),
|
|
5
|
+
SPARTA (DSMC), Gmsh (meshing) and ParaView/VTK (post-processing) behind a
|
|
6
|
+
single physics-first interface: the user defines the physical problem and
|
|
7
|
+
the studio generates geometry, meshes, solver input files, run commands and
|
|
8
|
+
reports.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.1"
|
|
12
|
+
|
|
13
|
+
from .atmosphere import atmosphere_at
|
|
14
|
+
from .freestream import Freestream
|
|
15
|
+
from .regime import FlowRegime, classify_regime
|
|
16
|
+
from .project import Project
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"atmosphere_at",
|
|
20
|
+
"Freestream",
|
|
21
|
+
"FlowRegime",
|
|
22
|
+
"classify_regime",
|
|
23
|
+
"Project",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""One-click analysis tools (spec §13.3): stagnation region, shock
|
|
2
|
+
stand-off distance and centerline probes.
|
|
3
|
+
|
|
4
|
+
All functions work on the common result model (SurfaceData/VolumeData),
|
|
5
|
+
so they apply to SU2 and SPARTA solutions alike.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Dict, Optional
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .freestream import Freestream
|
|
17
|
+
from .results import SurfaceData, VolumeData
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StagnationResult:
|
|
22
|
+
"""Wall quantities at and near the stagnation point (s = 0)."""
|
|
23
|
+
|
|
24
|
+
solver: str
|
|
25
|
+
x: float
|
|
26
|
+
y: float
|
|
27
|
+
pressure: Optional[float]
|
|
28
|
+
heat_flux: Optional[float]
|
|
29
|
+
peak_pressure: Optional[float]
|
|
30
|
+
peak_pressure_s: Optional[float]
|
|
31
|
+
peak_heat_flux: Optional[float]
|
|
32
|
+
peak_heat_flux_s: Optional[float]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stagnation_analysis(surface: SurfaceData) -> StagnationResult:
|
|
36
|
+
"""Values at the stagnation point plus surface peaks and their
|
|
37
|
+
arc-length locations."""
|
|
38
|
+
i0 = int(np.argmin(surface.s))
|
|
39
|
+
|
|
40
|
+
def at0(name):
|
|
41
|
+
v = surface.fields.get(name)
|
|
42
|
+
return float(v[i0]) if v is not None else None
|
|
43
|
+
|
|
44
|
+
def peak(name):
|
|
45
|
+
v = surface.fields.get(name)
|
|
46
|
+
if v is None:
|
|
47
|
+
return None, None
|
|
48
|
+
j = int(np.argmax(v))
|
|
49
|
+
return float(v[j]), float(surface.s[j])
|
|
50
|
+
|
|
51
|
+
pk_p, pk_p_s = peak("pressure")
|
|
52
|
+
pk_q, pk_q_s = peak("heat_flux")
|
|
53
|
+
return StagnationResult(
|
|
54
|
+
solver=surface.solver,
|
|
55
|
+
x=float(surface.x[i0]), y=float(surface.y[i0]),
|
|
56
|
+
pressure=at0("pressure"), heat_flux=at0("heat_flux"),
|
|
57
|
+
peak_pressure=pk_p, peak_pressure_s=pk_p_s,
|
|
58
|
+
peak_heat_flux=pk_q, peak_heat_flux_s=pk_q_s)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def centerline_profile(volume: VolumeData, x_nose: float,
|
|
62
|
+
upstream_length: float, band: float,
|
|
63
|
+
n_samples: int = 200) -> Dict[str, np.ndarray]:
|
|
64
|
+
"""Field profiles along the stagnation line: points within ``band`` of
|
|
65
|
+
the axis (y ≈ 0, and z ≈ 0 for 3D volumes), upstream of the nose,
|
|
66
|
+
bin-averaged onto a uniform x grid. Returns {'x': ..., field: ...}."""
|
|
67
|
+
mask = np.abs(volume.y) <= band
|
|
68
|
+
if volume.z is not None:
|
|
69
|
+
mask &= np.abs(volume.z) <= band
|
|
70
|
+
x = volume.x
|
|
71
|
+
mask &= (x >= x_nose - upstream_length) & (x <= x_nose)
|
|
72
|
+
if not np.any(mask):
|
|
73
|
+
raise ValueError("No volume samples on the stagnation line — "
|
|
74
|
+
"increase the search band")
|
|
75
|
+
|
|
76
|
+
xs = x[mask]
|
|
77
|
+
edges = np.linspace(xs.min(), xs.max(), n_samples + 1)
|
|
78
|
+
idx = np.clip(np.digitize(xs, edges) - 1, 0, n_samples - 1)
|
|
79
|
+
counts = np.bincount(idx, minlength=n_samples)
|
|
80
|
+
keep = counts > 0
|
|
81
|
+
out: Dict[str, np.ndarray] = {
|
|
82
|
+
"x": (0.5 * (edges[:-1] + edges[1:]))[keep]}
|
|
83
|
+
for name, values in volume.fields.items():
|
|
84
|
+
sums = np.bincount(idx, weights=values[mask], minlength=n_samples)
|
|
85
|
+
out[name] = sums[keep] / counts[keep]
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ShockStandoff:
|
|
91
|
+
"""Measured stand-off distance with the Billig correlation reference."""
|
|
92
|
+
|
|
93
|
+
distance: float # m
|
|
94
|
+
x_shock: float # m
|
|
95
|
+
x_nose: float # m
|
|
96
|
+
detection_field: str
|
|
97
|
+
billig_distance: Optional[float] # m, sphere correlation, None if n/a
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def ratio_to_billig(self) -> Optional[float]:
|
|
101
|
+
if not self.billig_distance:
|
|
102
|
+
return None
|
|
103
|
+
return self.distance / self.billig_distance
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def billig_standoff_sphere(nose_radius: float, mach: float) -> float:
|
|
107
|
+
"""Billig (1967) bow-shock stand-off correlation for a sphere:
|
|
108
|
+
delta / R = 0.143 * exp(3.24 / M^2)."""
|
|
109
|
+
return nose_radius * 0.143 * math.exp(3.24 / mach ** 2)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def billig_standoff_cylinder(radius: float, mach: float) -> float:
|
|
113
|
+
"""Billig (1967) bow-shock stand-off correlation for a circular
|
|
114
|
+
cylinder in crossflow: delta / R = 0.386 * exp(4.67 / M^2)."""
|
|
115
|
+
return radius * 0.386 * math.exp(4.67 / mach ** 2)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def shock_standoff(volume: VolumeData, freestream: Freestream,
|
|
119
|
+
x_nose: float, body_length: float,
|
|
120
|
+
nose_radius: Optional[float] = None,
|
|
121
|
+
band: Optional[float] = None) -> ShockStandoff:
|
|
122
|
+
"""Locate the bow shock on the stagnation line and measure its
|
|
123
|
+
stand-off distance from the nose.
|
|
124
|
+
|
|
125
|
+
The shock is taken as the upstream-most centerline sample where the
|
|
126
|
+
pressure rises above 2x freestream (or, without a pressure field,
|
|
127
|
+
where Mach drops below 90% of the freestream value).
|
|
128
|
+
"""
|
|
129
|
+
band = band if band is not None else 0.03 * body_length
|
|
130
|
+
profile = centerline_profile(volume, x_nose,
|
|
131
|
+
upstream_length=1.5 * body_length,
|
|
132
|
+
band=band)
|
|
133
|
+
x = profile["x"]
|
|
134
|
+
|
|
135
|
+
if "pressure" in profile:
|
|
136
|
+
field = "pressure"
|
|
137
|
+
crossed = profile[field] > 2.0 * freestream.pressure
|
|
138
|
+
elif "mach" in profile:
|
|
139
|
+
field = "mach"
|
|
140
|
+
crossed = profile[field] < 0.9 * freestream.mach
|
|
141
|
+
else:
|
|
142
|
+
raise ValueError("Centerline profile has neither pressure nor Mach")
|
|
143
|
+
|
|
144
|
+
if not np.any(crossed):
|
|
145
|
+
raise ValueError("No shock detected on the stagnation line "
|
|
146
|
+
f"(field '{field}')")
|
|
147
|
+
x_shock = float(x[crossed].min())
|
|
148
|
+
billig = (billig_standoff_sphere(nose_radius, freestream.mach)
|
|
149
|
+
if nose_radius else None)
|
|
150
|
+
return ShockStandoff(distance=x_nose - x_shock, x_shock=x_shock,
|
|
151
|
+
x_nose=x_nose, detection_field=field,
|
|
152
|
+
billig_distance=billig)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Run archive (spec §2) — history and comparison of a project's runs.
|
|
2
|
+
|
|
3
|
+
Every ``solvers/run_NNN`` directory is summarised into a RunRecord:
|
|
4
|
+
which solvers were set up, whether they finished or failed, headline
|
|
5
|
+
metrics (final SU2 residual and drag, last SPARTA step and particle
|
|
6
|
+
count) and whether the run continued from a restart. The records feed
|
|
7
|
+
the ``aerorare runs`` table and the GUI comparison view.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Optional
|
|
18
|
+
|
|
19
|
+
from .project import Project
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RunRecord:
|
|
24
|
+
name: str
|
|
25
|
+
path: Path
|
|
26
|
+
created: Optional[datetime] = None
|
|
27
|
+
solvers: List[str] = field(default_factory=list)
|
|
28
|
+
restarted: bool = False
|
|
29
|
+
su2_status: str = "—" # finished | failed | no log | —
|
|
30
|
+
su2_final_rms: Optional[float] = None
|
|
31
|
+
su2_final_cd: Optional[float] = None
|
|
32
|
+
sparta_status: str = "—"
|
|
33
|
+
sparta_last_step: Optional[int] = None
|
|
34
|
+
sparta_particles: Optional[int] = None
|
|
35
|
+
has_results: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_SPARTA_STATS = re.compile(r"^\s*(\d+)\s+[\d.eE+-]+\s+(\d+)\s+\d+\s+\d+")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _su2_metrics(run_dir: Path, record: RunRecord) -> None:
|
|
42
|
+
log = run_dir / "su2.log"
|
|
43
|
+
if log.exists():
|
|
44
|
+
tail = log.read_text(errors="replace")[-4000:]
|
|
45
|
+
if "Exit Success" in tail:
|
|
46
|
+
record.su2_status = "finished"
|
|
47
|
+
elif "Error" in tail:
|
|
48
|
+
record.su2_status = "failed"
|
|
49
|
+
else:
|
|
50
|
+
record.su2_status = "interrupted"
|
|
51
|
+
elif (run_dir / "case.cfg").exists():
|
|
52
|
+
record.su2_status = "not run"
|
|
53
|
+
|
|
54
|
+
history = run_dir / "history.csv"
|
|
55
|
+
if not history.exists():
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
with open(history, newline="") as fh:
|
|
59
|
+
reader = csv.reader(fh)
|
|
60
|
+
header = [h.strip().strip('"') for h in next(reader)]
|
|
61
|
+
last = None
|
|
62
|
+
for row in reader:
|
|
63
|
+
if row:
|
|
64
|
+
last = row
|
|
65
|
+
if last:
|
|
66
|
+
cols = {name: float(v) for name, v in zip(header, last)}
|
|
67
|
+
record.su2_final_rms = cols.get("rms[Rho]")
|
|
68
|
+
record.su2_final_cd = cols.get("CD")
|
|
69
|
+
except (ValueError, StopIteration):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sparta_metrics(run_dir: Path, record: RunRecord) -> None:
|
|
74
|
+
log = run_dir / "sparta.log"
|
|
75
|
+
if not log.exists():
|
|
76
|
+
log = run_dir / "log.sparta"
|
|
77
|
+
if log.exists():
|
|
78
|
+
text = log.read_text(errors="replace")
|
|
79
|
+
record.sparta_status = ("finished" if "Loop time" in text else
|
|
80
|
+
"interrupted")
|
|
81
|
+
last = None
|
|
82
|
+
for line in text.splitlines():
|
|
83
|
+
m = _SPARTA_STATS.match(line)
|
|
84
|
+
if m:
|
|
85
|
+
last = m
|
|
86
|
+
if last:
|
|
87
|
+
record.sparta_last_step = int(last.group(1))
|
|
88
|
+
record.sparta_particles = int(last.group(2))
|
|
89
|
+
elif (run_dir / "in.case").exists():
|
|
90
|
+
record.sparta_status = "not run"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def collect_runs(project: Project) -> List[RunRecord]:
|
|
94
|
+
"""RunRecords for every solvers/run_NNN directory, oldest first."""
|
|
95
|
+
records: List[RunRecord] = []
|
|
96
|
+
solvers_dir = project.root / "solvers"
|
|
97
|
+
for run_dir in sorted(solvers_dir.glob("run_*")):
|
|
98
|
+
if not run_dir.is_dir():
|
|
99
|
+
continue
|
|
100
|
+
record = RunRecord(name=run_dir.name, path=run_dir)
|
|
101
|
+
record.created = datetime.fromtimestamp(run_dir.stat().st_mtime)
|
|
102
|
+
|
|
103
|
+
cfg = run_dir / "case.cfg"
|
|
104
|
+
if cfg.exists():
|
|
105
|
+
record.solvers.append("su2")
|
|
106
|
+
if "RESTART_SOL= YES" in cfg.read_text(errors="replace"):
|
|
107
|
+
record.restarted = True
|
|
108
|
+
inp = run_dir / "in.case"
|
|
109
|
+
if inp.exists():
|
|
110
|
+
record.solvers.append("sparta")
|
|
111
|
+
if "read_restart" in inp.read_text(errors="replace"):
|
|
112
|
+
record.restarted = True
|
|
113
|
+
|
|
114
|
+
_su2_metrics(run_dir, record)
|
|
115
|
+
_sparta_metrics(run_dir, record)
|
|
116
|
+
record.has_results = (project.root / "results" / run_dir.name).is_dir()
|
|
117
|
+
records.append(record)
|
|
118
|
+
return records
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _fmt(value, spec: str = "") -> str:
|
|
122
|
+
if value is None:
|
|
123
|
+
return "—"
|
|
124
|
+
return format(value, spec) if spec else str(value)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def runs_table(records: List[RunRecord]) -> str:
|
|
128
|
+
"""Markdown comparison table across runs (also readable as plain text)."""
|
|
129
|
+
header = ("| Run | Date | Solvers | Restart | SU2 status | rms[Rho] | CD "
|
|
130
|
+
"| SPARTA status | Step | Particles | Results |")
|
|
131
|
+
sep = "|" + "---|" * 11
|
|
132
|
+
rows = [header, sep]
|
|
133
|
+
for r in records:
|
|
134
|
+
rows.append(
|
|
135
|
+
f"| {r.name} "
|
|
136
|
+
f"| {r.created.strftime('%Y-%m-%d %H:%M') if r.created else '—'} "
|
|
137
|
+
f"| {'+'.join(r.solvers) or '—'} "
|
|
138
|
+
f"| {'yes' if r.restarted else '—'} "
|
|
139
|
+
f"| {r.su2_status} "
|
|
140
|
+
f"| {_fmt(r.su2_final_rms, '.3f')} "
|
|
141
|
+
f"| {_fmt(r.su2_final_cd, '.5f')} "
|
|
142
|
+
f"| {r.sparta_status} "
|
|
143
|
+
f"| {_fmt(r.sparta_last_step)} "
|
|
144
|
+
f"| {_fmt(r.sparta_particles, ',')} "
|
|
145
|
+
f"| {'yes' if r.has_results else '—'} |")
|
|
146
|
+
return "\n".join(rows)
|