Skip to content

Commit 9b39783

Browse files
sksatclaude
andcommitted
[add] PSAS example: dual-deployment recovery-zone comparison
Add a dual-deployment variant of the PSAS flight to show why high-power rockets stage their recovery. config-dualdeploy.toml flies the same rocket with a drogue at apogee + the main at 450 m AGL (using the new multi-stage parachute support). Because the rocket spends most of the descent under the small fast drogue and only opens the main low down, it drifts far less: the nominal recovery zone shrinks from ~10 km (single main, crosses US-20) to ~3 km (stays local). recovery-map.py overlays both on the launch-site map (recovery-map.png). - config-dualdeploy.toml: dual-deploy recovery, nominal only, wind swept. - recovery-map.py: single-main vs dual-deploy nominal zone on the OSM map. - run.sh: also runs the dual-deploy config and the comparison map. - README: "Dual deployment shrinks the recovery zone" section. - .gitignore: ignore examples/**/output*/ (covers output-dualdeploy/). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LiKBVgTqjZV84zcMVY3w46
1 parent a05ddff commit 9b39783

8 files changed

Lines changed: 169 additions & 8 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,5 @@ ghp-output.js
4545
# regenerable (the plot PNGs are committed on purpose, shown in the READMEs)
4646
examples/**/*.eng
4747
examples/*.eng
48-
examples/**/output/
48+
examples/**/output*/
4949
examples/**/out-*/

examples/psas-launch12/README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ after launch (it stuck near the pad; only altitude, not horizontal position, was
8484
recovered). So there is no measured landing coordinate to compare against; the
8585
ascent altitude above is the part the telemetry can validate.
8686

87+
## Dual deployment shrinks the recovery zone
88+
89+
A single main from ~5 km drifts ~10 km — across US-20. The standard fix is **dual
90+
deployment**: a small drogue at apogee (fast descent) and the main only at low
91+
altitude. `config-dualdeploy.toml` models this (drogue + main at 450 m AGL).
92+
93+
The mechanism is clearest in the descent: under the drogue the dual-deploy flight
94+
comes down ~2.5x faster, only slowing under the main near the ground, so it is
95+
aloft for far less time (292 s vs 736 s):
96+
97+
![descent comparison](recovery-descent.png)
98+
99+
Less time aloft means less wind drift, so the nominal landing zone shrinks from
100+
~10 km to ~3 km and stays local:
101+
102+
![single main vs dual deploy](recovery-map.png)
103+
104+
This is exactly why high-power rockets use dual deployment; `parachute` takes any
105+
number of stages (see the repo `config-example.toml`).
106+
87107
## Caveats (what is estimated)
88108

89109
- **Drag**: trochia uses a *constant* Cd, but this flight is transonic (~Mach
@@ -92,8 +112,8 @@ ascent altitude above is the part the telemetry can validate.
92112
OpenRocket. So unlike the low-speed Estes-Viking case, the apogee here *does*
93113
depend on the drag model.
94114
- **CG / CP / Cna / inertia**: from the OpenRocket/JSBSim model or estimated.
95-
- **Recovery**: a single main deployed at apogee (trochia has no dual-deployment
96-
yet); its diameter is calibrated to the measured descent rate since PSAS's
97-
canopy size is unpublished. A real drogue + low-main deployment would descend
98-
faster up high and so drift less — the ~10 km nominal zone is the
115+
- **Recovery**: the nominal run uses a single main calibrated to the measured
116+
descent (PSAS's canopy size is unpublished). trochia does support multi-stage
117+
recovery (the dual-deployment section above); the nominal stays single-main
118+
because that matches the measured slow descent, so its ~10 km zone is the
99119
single-main worst case.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Same flight as config.toml, but with DUAL DEPLOYMENT (a drogue at apogee, then
2+
# the main low down) instead of a single main. This is how high-power rockets
3+
# limit recovery drift -- compare the landing zone with the single-main run.
4+
# Nominal recovery only (no failure scenarios); wind direction swept for the zone.
5+
6+
[simulation]
7+
dt = 0.01
8+
output.dt = 1.0
9+
output.dir = "output-dualdeploy"
10+
timeout = 900
11+
12+
[launcher]
13+
length = 5
14+
elevation = 89
15+
16+
[wind]
17+
model = "power"
18+
ground.dir = [ 0.0, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 270.0, 300.0, 330.0, 360.0 ]
19+
ground.speed= 5.0
20+
21+
[rocket]
22+
name = "PSAS LV2 (dual deploy)"
23+
type = "single"
24+
25+
[[rocket.stage]]
26+
engine = "N2501.eng"
27+
length = 3.53
28+
diameter= 0.1397
29+
mass = 20.692
30+
I0 = 35.0
31+
If = 25.0
32+
lcg0 = 2.51
33+
lcgf = 2.35
34+
lcgp = 3.20
35+
lcp = 2.72
36+
Cd = 0.60
37+
Cna = 12.0
38+
# drogue at apogee (fast descent) then main at 450 m AGL (slow final descent)
39+
parachute = [
40+
{delay = 0.0, cd = 1.2, diameter = 1.0}, # drogue
41+
{altitude = 450, cd = 1.5, diameter = 3.0}, # main
42+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Why the zones differ: descent altitude-vs-time, single main vs dual deploy.
2+
# pos.dat: time east up north (col 3 = altitude).
3+
set terminal pngcairo size 860,560 enhanced font "sans,11"
4+
set output "recovery-descent.png"
5+
6+
set title "Single main vs dual deployment: descent of the same flight\ndual deploy spends far less time aloft, so it drifts much less"
7+
set xlabel "time [s]"
8+
set ylabel "altitude AGL [m]"
9+
set xrange [0:760]
10+
set yrange [0:5300]
11+
set grid
12+
set key top right
13+
14+
SM = "output/nominal/89/5/0/pos.dat" # single main
15+
DD = "output-dualdeploy/89/5/0/pos.dat" # drogue + main
16+
17+
# mark the main-deployment altitude (450 m AGL)
18+
set arrow from 0,450 to 760,450 nohead dt 3 lc rgb "#888888"
19+
set label "main deploys (450 m)" at 755,620 right tc rgb "#666666"
20+
21+
plot SM using 1:3 with lines lw 2 lc rgb "#cc0000" title "single main (lands 736 s)", \
22+
DD using 1:3 with lines lw 2 lc rgb "#31688e" title "dual deploy (lands 292 s)"
52.1 KB
Loading
80.5 KB
Loading
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python
2+
"""Compare the nominal recovery zone for a single main vs dual deployment.
3+
4+
Reads the nominal landing footprints from the single-main run (output/nominal)
5+
and the dual-deploy run (output-dualdeploy) and draws both on the launch-site map.
6+
7+
Run via uv after both trochia runs: uv run recovery-map.py # -> recovery-map.png
8+
"""
9+
import csv
10+
import os
11+
12+
import pymap3d as pm
13+
from PIL import ImageDraw, ImageFont
14+
from staticmap import StaticMap, Line, CircleMarker
15+
16+
LAUNCH_LAT, LAUNCH_LON, LAUNCH_ALT = 43.7961, -120.6518, 1390.0
17+
18+
ZONES = [
19+
("output/nominal/89/ghp.csv", "single main (~10 km)", "#cc0000"),
20+
("output-dualdeploy/89/ghp.csv", "dual deploy (~3 km)", "#31688e"),
21+
]
22+
23+
24+
def enu_to_lonlat(e, n):
25+
x, y, z = pm.enu2ecef(e, n, 0.0, LAUNCH_LAT, LAUNCH_LON, LAUNCH_ALT)
26+
lat, lon, _ = pm.ecef2geodetic(x, y, z)
27+
return lon, lat
28+
29+
30+
def loop(path):
31+
pts = []
32+
with open(path) as f:
33+
for row in csv.DictReader(f, skipinitialspace=True):
34+
pts.append(enu_to_lonlat(float(row["ghp_e"]), float(row["ghp_n"])))
35+
return pts
36+
37+
38+
def find_font(size):
39+
for p in ("/usr/share/fonts/TTF/Roboto-Regular.ttf",
40+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"):
41+
try:
42+
return ImageFont.truetype(p, size)
43+
except OSError:
44+
continue
45+
return ImageFont.load_default()
46+
47+
48+
def main():
49+
m = StaticMap(900, 900, url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png")
50+
for path, _, color in ZONES:
51+
m.add_line(Line(loop(path), color, 3))
52+
m.add_marker(CircleMarker(enu_to_lonlat(0.0, 0.0), "black", 13))
53+
m.add_marker(CircleMarker(enu_to_lonlat(0.0, 0.0), "white", 7))
54+
55+
img = m.render().convert("RGB")
56+
d = ImageDraw.Draw(img, "RGBA")
57+
font, title = find_font(15), find_font(16)
58+
w, h = 230, 24 * (len(ZONES) + 1) + 34
59+
d.rectangle([10, 10, 10 + w, 10 + h], fill=(255, 255, 255, 225), outline=(0, 0, 0, 255))
60+
d.text((20, 18), "nominal recovery zone", font=title, fill=(0, 0, 0))
61+
for i, (_, label, color) in enumerate(ZONES):
62+
y = 44 + i * 24
63+
d.line([(20, y + 8), (50, y + 8)], fill=color, width=4)
64+
d.text((58, y), label, font=font, fill=(0, 0, 0))
65+
y = 44 + len(ZONES) * 24
66+
d.ellipse([30, y + 2, 40, y + 12], fill="black")
67+
d.text((58, y), "launch (Brothers, OR)", font=font, fill=(0, 0, 0))
68+
69+
img.save("recovery-map.png")
70+
print("saved recovery-map.png")
71+
72+
73+
if __name__ == "__main__":
74+
main()

examples/psas-launch12/run.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
set -euo pipefail
44
cd "$(dirname "$0")"
55
[ -f N2501.eng ] || ./fetch-engine.sh
6-
rm -rf output
7-
../../build/bin/trochia config.toml >/dev/null
6+
rm -rf output output-dualdeploy
7+
../../build/bin/trochia config.toml >/dev/null # single main + scenarios
8+
../../build/bin/trochia config-dualdeploy.toml >/dev/null # dual deployment
89
gnuplot trajectory.gp # validation vs measured telemetry
910
gnuplot footprints.gp # contingency zones (East-North)
1011
uv run footprints-map.py # contingency zones on the OSM map (needs network)
11-
echo "wrote trajectory.png, footprints.png and footprints-map.png"
12+
gnuplot recovery-descent.gp # single-main vs dual-deploy descent (mechanism)
13+
uv run recovery-map.py # single-main vs dual-deploy recovery zone (outcome)
14+
echo "wrote trajectory.png, footprints.png, footprints-map.png, recovery-descent.png and recovery-map.png"

0 commit comments

Comments
 (0)