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.
Files changed (45) hide show
  1. aerorare_studio-0.1.1/LICENSE +21 -0
  2. aerorare_studio-0.1.1/PKG-INFO +172 -0
  3. aerorare_studio-0.1.1/README.md +134 -0
  4. aerorare_studio-0.1.1/aerorare/__init__.py +25 -0
  5. aerorare_studio-0.1.1/aerorare/analysis.py +152 -0
  6. aerorare_studio-0.1.1/aerorare/archive.py +146 -0
  7. aerorare_studio-0.1.1/aerorare/atmosphere.py +120 -0
  8. aerorare_studio-0.1.1/aerorare/blmesh.py +188 -0
  9. aerorare_studio-0.1.1/aerorare/boundaries.py +122 -0
  10. aerorare_studio-0.1.1/aerorare/cad.py +169 -0
  11. aerorare_studio-0.1.1/aerorare/cli.py +1200 -0
  12. aerorare_studio-0.1.1/aerorare/freestream.py +102 -0
  13. aerorare_studio-0.1.1/aerorare/gas.py +95 -0
  14. aerorare_studio-0.1.1/aerorare/geometry.py +301 -0
  15. aerorare_studio-0.1.1/aerorare/geometry3d.py +427 -0
  16. aerorare_studio-0.1.1/aerorare/gui.py +1352 -0
  17. aerorare_studio-0.1.1/aerorare/hpc.py +209 -0
  18. aerorare_studio-0.1.1/aerorare/meshing.py +330 -0
  19. aerorare_studio-0.1.1/aerorare/postprocess.py +350 -0
  20. aerorare_studio-0.1.1/aerorare/project.py +214 -0
  21. aerorare_studio-0.1.1/aerorare/regime.py +98 -0
  22. aerorare_studio-0.1.1/aerorare/report.py +425 -0
  23. aerorare_studio-0.1.1/aerorare/results.py +389 -0
  24. aerorare_studio-0.1.1/aerorare/runner.py +148 -0
  25. aerorare_studio-0.1.1/aerorare/sparta.py +530 -0
  26. aerorare_studio-0.1.1/aerorare/su2.py +217 -0
  27. aerorare_studio-0.1.1/aerorare/tools.py +72 -0
  28. aerorare_studio-0.1.1/aerorare/valcases.py +302 -0
  29. aerorare_studio-0.1.1/aerorare/validation.py +169 -0
  30. aerorare_studio-0.1.1/aerorare_studio.egg-info/PKG-INFO +172 -0
  31. aerorare_studio-0.1.1/aerorare_studio.egg-info/SOURCES.txt +43 -0
  32. aerorare_studio-0.1.1/aerorare_studio.egg-info/dependency_links.txt +1 -0
  33. aerorare_studio-0.1.1/aerorare_studio.egg-info/entry_points.txt +2 -0
  34. aerorare_studio-0.1.1/aerorare_studio.egg-info/requires.txt +18 -0
  35. aerorare_studio-0.1.1/aerorare_studio.egg-info/top_level.txt +1 -0
  36. aerorare_studio-0.1.1/pyproject.toml +48 -0
  37. aerorare_studio-0.1.1/setup.cfg +4 -0
  38. aerorare_studio-0.1.1/tests/test_3d_hpc.py +247 -0
  39. aerorare_studio-0.1.1/tests/test_analysis.py +162 -0
  40. aerorare_studio-0.1.1/tests/test_archive.py +89 -0
  41. aerorare_studio-0.1.1/tests/test_core.py +399 -0
  42. aerorare_studio-0.1.1/tests/test_repair.py +86 -0
  43. aerorare_studio-0.1.1/tests/test_results.py +179 -0
  44. aerorare_studio-0.1.1/tests/test_results3d_report.py +179 -0
  45. 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)