orp 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orp/__init__.py +18 -0
- orp/cli.py +487 -0
- orp/core/__init__.py +48 -0
- orp/core/aerodynamics/__init__.py +26 -0
- orp/core/aerodynamics/calculator.py +108 -0
- orp/core/aerodynamics/constant.py +102 -0
- orp/core/aerodynamics/flight_conditions.py +76 -0
- orp/core/aerodynamics/newtonian.py +215 -0
- orp/core/atmosphere/__init__.py +26 -0
- orp/core/atmosphere/earth.py +129 -0
- orp/core/atmosphere/exponential.py +111 -0
- orp/core/atmosphere/mars.py +94 -0
- orp/core/atmosphere/model.py +113 -0
- orp/core/atmosphere/us76_highalt.py +128 -0
- orp/core/bank_schedule/__init__.py +13 -0
- orp/core/bank_schedule/schedule.py +393 -0
- orp/core/frames.py +159 -0
- orp/core/gravity/__init__.py +21 -0
- orp/core/gravity/earth.py +62 -0
- orp/core/gravity/mars.py +51 -0
- orp/core/gravity/model.py +43 -0
- orp/core/planet/__init__.py +20 -0
- orp/core/planet/planet.py +112 -0
- orp/core/planet/registry.py +68 -0
- orp/core/provenance/__init__.py +24 -0
- orp/core/provenance/tags.py +225 -0
- orp/core/report.py +100 -0
- orp/core/session.py +580 -0
- orp/core/simulation/__init__.py +37 -0
- orp/core/simulation/conditions.py +104 -0
- orp/core/simulation/engine.py +131 -0
- orp/core/simulation/flight_data.py +291 -0
- orp/core/simulation/status.py +172 -0
- orp/core/simulation/stepper.py +334 -0
- orp/core/vehicles/__init__.py +18 -0
- orp/core/vehicles/base.py +117 -0
- orp/core/vehicles/library.py +177 -0
- orp/data/vehicles/apollo.yaml +62 -0
- orp/data/vehicles/insight.yaml +83 -0
- orp/data/vehicles/msl.yaml +60 -0
- orp/data/vehicles/orion.yaml +78 -0
- orp/data/vehicles/stardust.yaml +86 -0
- orp/experiments/__init__.py +10 -0
- orp/experiments/insight_rotation.py +308 -0
- orp/gates/__init__.py +12 -0
- orp/gates/gate3_artemis.py +202 -0
- orp/gates/gate3_artemis_replay.py +418 -0
- orp/gates/gate_stardust.py +188 -0
- orp/gates/summary.py +139 -0
- orp/gui/__init__.py +10 -0
- orp/gui/app_state.py +389 -0
- orp/gui/conditions_panel.py +418 -0
- orp/gui/feedback.py +88 -0
- orp/gui/glossary.py +172 -0
- orp/gui/icon.py +65 -0
- orp/gui/info_icon.py +50 -0
- orp/gui/main_window.py +348 -0
- orp/gui/plots.py +305 -0
- orp/gui/results_panel.py +280 -0
- orp/gui/vehicle_panel.py +207 -0
- orp/tests/__init__.py +4 -0
- orp/tests/test_atmosphere_us76ext.py +76 -0
- orp/tests/test_bank_schedule_from_csv.py +554 -0
- orp/tests/test_bridge_openreentry.py +258 -0
- orp/tests/test_cli.py +629 -0
- orp/tests/test_eom_invariants.py +525 -0
- orp/tests/test_experiment_insight.py +114 -0
- orp/tests/test_frames.py +100 -0
- orp/tests/test_gate3_artemis.py +79 -0
- orp/tests/test_gate3_replay.py +88 -0
- orp/tests/test_gate_stardust.py +68 -0
- orp/tests/test_gui.py +836 -0
- orp/tests/test_gui_wall.py +155 -0
- orp/tests/test_physics_aero.py +149 -0
- orp/tests/test_physics_atmosphere.py +127 -0
- orp/tests/test_physics_eom.py +153 -0
- orp/tests/test_physics_fixtures.py +110 -0
- orp/tests/test_physics_gravity.py +74 -0
- orp/tests/test_plots.py +160 -0
- orp/tests/test_session.py +415 -0
- orp/tests/test_site.py +78 -0
- orp/tests/test_smoke.py +202 -0
- orp/tests/test_trajectory_channels.py +135 -0
- orp-0.0.1.dist-info/METADATA +210 -0
- orp-0.0.1.dist-info/RECORD +89 -0
- orp-0.0.1.dist-info/WHEEL +5 -0
- orp-0.0.1.dist-info/entry_points.txt +2 -0
- orp-0.0.1.dist-info/licenses/LICENSE +674 -0
- orp-0.0.1.dist-info/top_level.txt +1 -0
orp/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# ORP — Open Reentry Platform
|
|
2
|
+
# Copyright (C) Charles W. Dowd Jr.
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
"""ORP — Open Reentry Platform.
|
|
5
|
+
|
|
6
|
+
A forward-only atmospheric reentry simulator with first-class data provenance and a
|
|
7
|
+
multi-planet (Earth / Mars) vehicle + environment abstraction.
|
|
8
|
+
|
|
9
|
+
The public entry points live under :mod:`orp.core`. See :mod:`orp.core` for the two
|
|
10
|
+
architectural invariants this package enforces everywhere (forward-only simulation and
|
|
11
|
+
provenance-on-everything) and the placeholder-physics convention.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.0.1"
|
|
15
|
+
__author__ = "Charles W. Dowd Jr."
|
|
16
|
+
__license__ = "GPL-3.0-or-later"
|
|
17
|
+
|
|
18
|
+
__all__ = ["__version__", "__author__", "__license__"]
|
orp/cli.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# ORP — Open Reentry Platform
|
|
2
|
+
# Copyright (C) Charles W. Dowd Jr.
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
"""The ORP command line: forward reentry runs, the vehicle library, and the gates.
|
|
5
|
+
|
|
6
|
+
FORWARD-ONLY WALL
|
|
7
|
+
=================
|
|
8
|
+
This interface composes existing pieces and adds no physics. A run takes a vehicle, a
|
|
9
|
+
planet, an explicit entry state (with a mandatory frame tag), and a **pre-recorded bank
|
|
10
|
+
schedule** — and produces a trajectory, figures, a reproducible session file, and a
|
|
11
|
+
provenance report. Bank schedules are inputs; the trajectory and its ground track are
|
|
12
|
+
outputs. No flag, argument, or subcommand accepts an endpoint and produces controls, and
|
|
13
|
+
``orp/tests/test_cli.py`` walks the parser tree to keep it that way.
|
|
14
|
+
|
|
15
|
+
REFUSAL OVER REPAIR
|
|
16
|
+
===================
|
|
17
|
+
Bad input is refused with a one-line plain-language reason and a nonzero exit — an unknown
|
|
18
|
+
vehicle or planet, a missing frame tag, a malformed bank-history CSV, an output path that
|
|
19
|
+
is an existing file. Nothing is substituted or repaired silently.
|
|
20
|
+
|
|
21
|
+
Frame handling: the engine consumes a planet-relative entry state. ``--frame inertial``
|
|
22
|
+
states are converted at this boundary via :mod:`orp.core.frames` (convert first, then
|
|
23
|
+
save — sessions always record the planet-relative state the engine consumed).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import dataclasses
|
|
30
|
+
import math
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
__all__ = ["build_parser", "main"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Refusal(Exception):
|
|
39
|
+
"""A plain-language refusal: printed as one line to stderr, exit nonzero, no traceback."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ArgumentParser(argparse.ArgumentParser):
|
|
43
|
+
"""argparse with one-line parse errors (refusal over usage spam).
|
|
44
|
+
|
|
45
|
+
Parse-stage refusals (missing --frame, both or neither schedule flags, unknown
|
|
46
|
+
subcommand) exit 2 with exactly one plain-language line on stderr, matching the
|
|
47
|
+
behaviour of the post-parse refusals. Subparsers inherit this class.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def error(self, message: str): # noqa: D102 — argparse contract
|
|
51
|
+
self.exit(2, f"{self.prog}: {message}\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Parser
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def _conditions_defaults() -> dict[str, object]:
|
|
59
|
+
"""The engine's current defaults, read from SimulationConditions itself."""
|
|
60
|
+
from orp.core.simulation.conditions import SimulationConditions
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
f.name: f.default
|
|
64
|
+
for f in dataclasses.fields(SimulationConditions)
|
|
65
|
+
if f.default is not dataclasses.MISSING
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
70
|
+
"""Build the ``orp`` argument parser (public so tests can walk the whole tree)."""
|
|
71
|
+
defaults = _conditions_defaults()
|
|
72
|
+
|
|
73
|
+
parser = _ArgumentParser(
|
|
74
|
+
prog="orp",
|
|
75
|
+
description=(
|
|
76
|
+
"ORP - Open Reentry Platform. Forward-only atmospheric reentry runs with "
|
|
77
|
+
"first-class provenance: bank schedules are inputs, the trajectory and its "
|
|
78
|
+
"ground track are outputs."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
subparsers = parser.add_subparsers(
|
|
82
|
+
title="commands", dest="command", required=True, metavar="{run,vehicles,gates}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# ----- orp run ---------------------------------------------------------
|
|
86
|
+
run = subparsers.add_parser(
|
|
87
|
+
"run",
|
|
88
|
+
help="run one forward reentry simulation and write its outputs",
|
|
89
|
+
description=(
|
|
90
|
+
"Run one forward reentry simulation: replay the given bank schedule through "
|
|
91
|
+
"the engine and write trajectory.csv, the five standard figures, "
|
|
92
|
+
"session.yaml, and provenance.txt into --out."
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
run.add_argument("--vehicle", required=True, metavar="NAME",
|
|
96
|
+
help="vehicle library name (see 'orp vehicles')")
|
|
97
|
+
run.add_argument("--planet", required=True, metavar="NAME",
|
|
98
|
+
help="planet name from the registry (earth or mars)")
|
|
99
|
+
run.add_argument("--velocity", type=float, metavar="M_PER_S",
|
|
100
|
+
default=float(defaults["entry_velocity"]),
|
|
101
|
+
help="entry speed in m/s, expressed in --frame (default: %(default)s)")
|
|
102
|
+
run.add_argument("--fpa", type=float, metavar="DEG",
|
|
103
|
+
default=math.degrees(float(defaults["entry_flight_path_angle"])),
|
|
104
|
+
help=("entry flight-path angle in degrees, negative descending, "
|
|
105
|
+
"expressed in --frame (default: %(default).4g)"))
|
|
106
|
+
run.add_argument("--heading", type=float, metavar="DEG",
|
|
107
|
+
default=math.degrees(float(defaults["entry_heading"])),
|
|
108
|
+
help=("entry heading in degrees clockwise from north, expressed in "
|
|
109
|
+
"--frame (default: %(default).4g)"))
|
|
110
|
+
run.add_argument("--altitude", type=float, metavar="M",
|
|
111
|
+
default=float(defaults["entry_altitude"]),
|
|
112
|
+
help="entry altitude in meters above the mean radius (default: %(default)s)")
|
|
113
|
+
run.add_argument("--lat", type=float, metavar="DEG",
|
|
114
|
+
default=math.degrees(float(defaults["entry_latitude"])),
|
|
115
|
+
help="entry latitude in degrees (default: %(default)s)")
|
|
116
|
+
run.add_argument("--lon", type=float, metavar="DEG",
|
|
117
|
+
default=math.degrees(float(defaults["entry_longitude"])),
|
|
118
|
+
help="entry longitude in degrees (default: %(default)s)")
|
|
119
|
+
run.add_argument("--frame", required=True, choices=("inertial", "planet-relative"),
|
|
120
|
+
help=("frame the entry state is expressed in (REQUIRED, no default). "
|
|
121
|
+
"'inertial' is converted to planet-relative at this boundary "
|
|
122
|
+
"via orp.core.frames before the run; sessions record the "
|
|
123
|
+
"converted state."))
|
|
124
|
+
|
|
125
|
+
schedule = run.add_mutually_exclusive_group(required=True)
|
|
126
|
+
schedule.add_argument("--bank-deg", type=float, metavar="CONST",
|
|
127
|
+
help=("constant commanded bank angle in degrees - a "
|
|
128
|
+
"pre-recorded control input, replayed as-is"))
|
|
129
|
+
schedule.add_argument("--bank-csv", metavar="PATH",
|
|
130
|
+
help=("two-column CSV (time_s, bank_angle_deg) commanded-bank "
|
|
131
|
+
"history, loaded via BankSchedule.from_csv and replayed "
|
|
132
|
+
"as-is"))
|
|
133
|
+
|
|
134
|
+
run.add_argument("--dt", type=float, metavar="S",
|
|
135
|
+
default=float(defaults["time_step"]),
|
|
136
|
+
help="integrator time step in seconds (engine default: %(default)s)")
|
|
137
|
+
run.add_argument("--max-time", type=float, metavar="S",
|
|
138
|
+
default=float(defaults["max_simulation_time"]),
|
|
139
|
+
help="maximum simulated time in seconds (engine default: %(default)s)")
|
|
140
|
+
run.add_argument("--out", required=True, metavar="DIR",
|
|
141
|
+
help=("output directory for this run's files (created if needed; "
|
|
142
|
+
"refused if it exists as a file)"))
|
|
143
|
+
run.set_defaults(func=_cmd_run)
|
|
144
|
+
|
|
145
|
+
# ----- orp vehicles ----------------------------------------------------
|
|
146
|
+
vehicles = subparsers.add_parser(
|
|
147
|
+
"vehicles",
|
|
148
|
+
help="list library vehicles with per-property provenance",
|
|
149
|
+
description=("List every vehicle in the library: name, source citation count, "
|
|
150
|
+
"and a per-property provenance summary (worst tag first)."),
|
|
151
|
+
)
|
|
152
|
+
vehicles.set_defaults(func=_cmd_vehicles)
|
|
153
|
+
|
|
154
|
+
# ----- orp gui ---------------------------------------------------------
|
|
155
|
+
gui = subparsers.add_parser(
|
|
156
|
+
"gui",
|
|
157
|
+
help="launch the ORP desktop interface (needs the gui extra)",
|
|
158
|
+
description=(
|
|
159
|
+
"Launch the ORP desktop interface (PyQt6). Requires the optional gui "
|
|
160
|
+
"dependencies: pip install orp[gui]."
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
gui.set_defaults(func=_cmd_gui)
|
|
164
|
+
|
|
165
|
+
# ----- orp gates -------------------------------------------------------
|
|
166
|
+
gates = subparsers.add_parser(
|
|
167
|
+
"gates",
|
|
168
|
+
help="run the validation gates and report their statuses",
|
|
169
|
+
description=("Run the validation gates and print each gate's status exactly as "
|
|
170
|
+
"the gate states it (NOT_VALIDATED and honest FAIL included, never "
|
|
171
|
+
"reworded). Exit 0 when every gate reports its own pinned expected "
|
|
172
|
+
"status; nonzero on unexpected deviation."),
|
|
173
|
+
)
|
|
174
|
+
gates.set_defaults(func=_cmd_gates)
|
|
175
|
+
|
|
176
|
+
return parser
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# orp run
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _require_finite(name: str, value: float, *, positive: bool = False) -> float:
|
|
184
|
+
"""Refuse non-finite (and, where demanded, non-positive) numeric arguments."""
|
|
185
|
+
if not math.isfinite(value):
|
|
186
|
+
raise _Refusal(f"{name} must be a finite number (got {value!r}).")
|
|
187
|
+
if positive and value <= 0.0:
|
|
188
|
+
raise _Refusal(f"{name} must be positive (got {value!r}).")
|
|
189
|
+
return float(value)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
193
|
+
from orp.core.aerodynamics.constant import ConstantCoefficientCalculator
|
|
194
|
+
from orp.core.bank_schedule import BankSchedule
|
|
195
|
+
from orp.core.frames import FrameConversionError, inertial_to_planet_relative
|
|
196
|
+
from orp.core.planet import by_name
|
|
197
|
+
from orp.core.provenance import ProvenanceTag, ValidationLevel, weakest
|
|
198
|
+
from orp.core.session import save_session, source_constant, source_csv
|
|
199
|
+
from orp.core.simulation import SimulationConditions, SimulationEngine
|
|
200
|
+
from orp.core.vehicles import VehicleLibrary
|
|
201
|
+
from orp.gui import plots
|
|
202
|
+
|
|
203
|
+
# Fail fast on numbers the engine cannot meaningfully consume. --dt and
|
|
204
|
+
# --max-time must be positive (a zero time step would never advance).
|
|
205
|
+
velocity = _require_finite("--velocity", args.velocity, positive=True)
|
|
206
|
+
fpa_deg = _require_finite("--fpa", args.fpa)
|
|
207
|
+
heading_deg = _require_finite("--heading", args.heading)
|
|
208
|
+
altitude = _require_finite("--altitude", args.altitude)
|
|
209
|
+
lat_deg = _require_finite("--lat", args.lat)
|
|
210
|
+
lon_deg = _require_finite("--lon", args.lon)
|
|
211
|
+
dt = _require_finite("--dt", args.dt, positive=True)
|
|
212
|
+
max_time = _require_finite("--max-time", args.max_time, positive=True)
|
|
213
|
+
if args.bank_deg is not None:
|
|
214
|
+
_require_finite("--bank-deg", args.bank_deg)
|
|
215
|
+
|
|
216
|
+
out = Path(args.out)
|
|
217
|
+
if out.is_file():
|
|
218
|
+
raise _Refusal(
|
|
219
|
+
f"--out {out} already exists as a file; pass a directory "
|
|
220
|
+
"(it is created if missing)."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
vehicle = VehicleLibrary().load(args.vehicle)
|
|
225
|
+
except (OSError, ValueError) as error:
|
|
226
|
+
# Unknown name (FileNotFoundError) or a malformed/unreadable vehicle YAML.
|
|
227
|
+
raise _Refusal(str(error)) from None
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
planet = by_name(args.planet)
|
|
231
|
+
except KeyError as error:
|
|
232
|
+
raise _Refusal(error.args[0] if error.args else str(error)) from None
|
|
233
|
+
|
|
234
|
+
# --- bank schedule: a pre-recorded control input, replayed as-is -------
|
|
235
|
+
if args.bank_csv is not None:
|
|
236
|
+
schedule_provenance = ProvenanceTag(
|
|
237
|
+
ValidationLevel.ASSERTED,
|
|
238
|
+
source=f"user-supplied CSV: {args.bank_csv}",
|
|
239
|
+
notes="Commanded bank history supplied via CLI --bank-csv; replayed as-is.",
|
|
240
|
+
)
|
|
241
|
+
try:
|
|
242
|
+
bank_schedule = BankSchedule.from_csv(
|
|
243
|
+
args.bank_csv, provenance=schedule_provenance
|
|
244
|
+
)
|
|
245
|
+
except (ValueError, OSError) as error:
|
|
246
|
+
raise _Refusal(str(error)) from None
|
|
247
|
+
# Record the absolute path: session CSV sources resolve against the session
|
|
248
|
+
# file's directory at load time, so a cwd-relative path would not reload.
|
|
249
|
+
schedule_source = source_csv(Path(args.bank_csv).resolve())
|
|
250
|
+
else:
|
|
251
|
+
bank_rad = math.radians(args.bank_deg)
|
|
252
|
+
schedule_provenance = ProvenanceTag(
|
|
253
|
+
ValidationLevel.NOT_VALIDATED,
|
|
254
|
+
source=f"user-supplied constant via CLI --bank-deg {args.bank_deg}",
|
|
255
|
+
notes="Hand-entered constant bank command; unsourced.",
|
|
256
|
+
)
|
|
257
|
+
bank_schedule = BankSchedule.constant(bank_rad, provenance=schedule_provenance)
|
|
258
|
+
schedule_source = source_constant(bank_rad)
|
|
259
|
+
|
|
260
|
+
# --- entry state (SI / radians), frame handled at this boundary --------
|
|
261
|
+
flight_path_angle = math.radians(fpa_deg)
|
|
262
|
+
heading = math.radians(heading_deg)
|
|
263
|
+
latitude = math.radians(lat_deg)
|
|
264
|
+
longitude = math.radians(lon_deg)
|
|
265
|
+
|
|
266
|
+
if args.frame == "inertial":
|
|
267
|
+
try:
|
|
268
|
+
relative = inertial_to_planet_relative(
|
|
269
|
+
planet,
|
|
270
|
+
velocity=velocity,
|
|
271
|
+
flight_path_angle=flight_path_angle,
|
|
272
|
+
heading=heading,
|
|
273
|
+
latitude=latitude,
|
|
274
|
+
altitude=altitude,
|
|
275
|
+
)
|
|
276
|
+
except FrameConversionError as error:
|
|
277
|
+
raise _Refusal(str(error)) from None
|
|
278
|
+
velocity = relative.velocity
|
|
279
|
+
flight_path_angle = relative.flight_path_angle
|
|
280
|
+
heading = relative.heading
|
|
281
|
+
print(
|
|
282
|
+
"Converted entry state inertial -> planet-relative (eastward "
|
|
283
|
+
"planet-rotation velocity subtracted): "
|
|
284
|
+
f"velocity {velocity:.6f} m/s, "
|
|
285
|
+
f"flight-path angle {math.degrees(flight_path_angle):.6f} deg, "
|
|
286
|
+
f"heading {math.degrees(heading):.6f} deg."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Constant-coefficient aero from the vehicle's own cited nominal coefficients
|
|
290
|
+
# (the repo's gate/plot convention); provenance is their weakest link.
|
|
291
|
+
aero = ConstantCoefficientCalculator(
|
|
292
|
+
vehicle.drag_coefficient.get(),
|
|
293
|
+
vehicle.lift_to_drag.get(),
|
|
294
|
+
provenance=weakest([vehicle.drag_coefficient, vehicle.lift_to_drag]),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
conditions = SimulationConditions(
|
|
298
|
+
vehicle=vehicle,
|
|
299
|
+
planet=planet,
|
|
300
|
+
bank_schedule=bank_schedule,
|
|
301
|
+
aerodynamic_calculator=aero,
|
|
302
|
+
entry_velocity=velocity,
|
|
303
|
+
entry_flight_path_angle=flight_path_angle,
|
|
304
|
+
entry_altitude=altitude,
|
|
305
|
+
entry_heading=heading,
|
|
306
|
+
entry_latitude=latitude,
|
|
307
|
+
entry_longitude=longitude,
|
|
308
|
+
time_step=dt,
|
|
309
|
+
max_simulation_time=max_time,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
engine = SimulationEngine()
|
|
313
|
+
result = engine.simulate(conditions)
|
|
314
|
+
|
|
315
|
+
# --- outputs ------------------------------------------------------------
|
|
316
|
+
from orp.core.report import render_provenance_report, write_trajectory_csv
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
except OSError as error:
|
|
321
|
+
raise _Refusal(f"cannot create --out directory {out}: {error}") from None
|
|
322
|
+
write_trajectory_csv(result, out / "trajectory.csv")
|
|
323
|
+
|
|
324
|
+
# plots.py is Figure-based (never pyplot), headless by construction; the env var
|
|
325
|
+
# additionally pins any future backend selection to Agg without importing matplotlib.
|
|
326
|
+
# matplotlib is imported lazily inside the figure-writing call only: without it the
|
|
327
|
+
# simulation still succeeded, so the run degrades to data outputs and exits 0.
|
|
328
|
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
|
329
|
+
try:
|
|
330
|
+
plots.save_standard_plots(result, out)
|
|
331
|
+
except ImportError:
|
|
332
|
+
print(
|
|
333
|
+
'Figures skipped: matplotlib is not installed; install with '
|
|
334
|
+
'pip install "orp[plot]" to enable them.'
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
save_session(
|
|
338
|
+
out / "session.yaml",
|
|
339
|
+
conditions=conditions,
|
|
340
|
+
vehicle_name=args.vehicle,
|
|
341
|
+
schedule_source=schedule_source,
|
|
342
|
+
)
|
|
343
|
+
(out / "provenance.txt").write_text(
|
|
344
|
+
render_provenance_report(
|
|
345
|
+
result=result,
|
|
346
|
+
conditions=conditions,
|
|
347
|
+
engine=engine,
|
|
348
|
+
vehicle_name=args.vehicle,
|
|
349
|
+
),
|
|
350
|
+
encoding="utf-8",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# --- closing summary ----------------------------------------------------
|
|
354
|
+
from orp.core.simulation import flight_data as fd
|
|
355
|
+
|
|
356
|
+
branch = result.get_branch(0)
|
|
357
|
+
end_event = branch.events[-1].name if branch.events else "(no events)"
|
|
358
|
+
print(
|
|
359
|
+
f"Run complete: {branch.length} samples, "
|
|
360
|
+
f"terminated by {end_event} at t={branch.get_last(fd.TYPE_TIME):.3f} s."
|
|
361
|
+
)
|
|
362
|
+
print(f"Peak deceleration: {result.summary.get('peak_deceleration', float('nan')):.4f} g")
|
|
363
|
+
print(f"Peak heat rate: {result.summary.get('peak_heat_rate', float('nan')):.6g} W/m^2")
|
|
364
|
+
print(
|
|
365
|
+
"Final state: "
|
|
366
|
+
f"altitude {branch.get_last(fd.TYPE_ALTITUDE):.1f} m, "
|
|
367
|
+
f"velocity {branch.get_last(fd.TYPE_VELOCITY):.2f} m/s, "
|
|
368
|
+
f"latitude {branch.get_last(fd.TYPE_LATITUDE):.5f} deg, "
|
|
369
|
+
f"longitude {branch.get_last(fd.TYPE_LONGITUDE):.5f} deg, "
|
|
370
|
+
f"flight-path angle {branch.get_last(fd.TYPE_FLIGHT_PATH_ANGLE):.4f} deg, "
|
|
371
|
+
# Display-only wrap to [0, 360); the recorded channel (trajectory.csv) is
|
|
372
|
+
# left exactly as the engine integrated it.
|
|
373
|
+
f"heading {branch.get_last(fd.TYPE_HEADING) % 360.0:.4f} deg"
|
|
374
|
+
)
|
|
375
|
+
print(f"Run provenance (weakest link): {result.provenance.level.name}")
|
|
376
|
+
print(f"Outputs written to {out}")
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# orp vehicles
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def _cmd_vehicles(args: argparse.Namespace) -> int:
|
|
385
|
+
from orp.core.vehicles import VehicleLibrary
|
|
386
|
+
|
|
387
|
+
library = VehicleLibrary()
|
|
388
|
+
names = library.list_available()
|
|
389
|
+
if not names:
|
|
390
|
+
print(f"No vehicles found in {library.data_dir}.")
|
|
391
|
+
return 0
|
|
392
|
+
for name in names:
|
|
393
|
+
vehicle = library.load(name)
|
|
394
|
+
tagged = vehicle.tagged_values()
|
|
395
|
+
citations = {tv.provenance.source for tv in tagged.values() if tv.provenance.source}
|
|
396
|
+
print(
|
|
397
|
+
f"{name} ({vehicle.name}): {len(tagged)} properties, "
|
|
398
|
+
f"{len(citations)} distinct source citation(s); "
|
|
399
|
+
f"weakest link: {vehicle.provenance.level.name}"
|
|
400
|
+
)
|
|
401
|
+
# Per-property provenance, worst tag first (then by property name).
|
|
402
|
+
for prop, tv in sorted(
|
|
403
|
+
tagged.items(), key=lambda kv: (kv[1].provenance.level.rank, kv[0])
|
|
404
|
+
):
|
|
405
|
+
source = f" <{tv.provenance.source}>" if tv.provenance.source else ""
|
|
406
|
+
print(f" {prop}: {tv.provenance.level.name}{source}")
|
|
407
|
+
print()
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# orp gui
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def _cmd_gui(args: argparse.Namespace) -> int:
|
|
416
|
+
"""Launch the desktop interface. PyQt6 is imported lazily HERE so that run,
|
|
417
|
+
vehicles, and gates keep working when the gui extra is not installed."""
|
|
418
|
+
try:
|
|
419
|
+
from PyQt6.QtWidgets import QApplication
|
|
420
|
+
except ImportError:
|
|
421
|
+
raise _Refusal(
|
|
422
|
+
"the desktop interface needs PyQt6, which is not installed; "
|
|
423
|
+
"install it with: pip install orp[gui]"
|
|
424
|
+
) from None
|
|
425
|
+
|
|
426
|
+
from orp.gui.app_state import AppState
|
|
427
|
+
from orp.gui.icon import orp_icon
|
|
428
|
+
from orp.gui.main_window import MainWindow
|
|
429
|
+
|
|
430
|
+
app = QApplication.instance()
|
|
431
|
+
if app is None:
|
|
432
|
+
app = QApplication([])
|
|
433
|
+
app.setWindowIcon(orp_icon())
|
|
434
|
+
window = MainWindow(AppState())
|
|
435
|
+
window.show()
|
|
436
|
+
return app.exec()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
# orp gates
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
def _cmd_gates(args: argparse.Namespace) -> int:
|
|
444
|
+
"""Run the gates; print each status exactly as the gate states it; exit 0 only
|
|
445
|
+
when every gate reports its own pinned expected status (an honest FAIL that the
|
|
446
|
+
gate's tests pin counts as expected). Evaluation lives in orp.gates.summary,
|
|
447
|
+
shared with every other front end so the wording can never drift.
|
|
448
|
+
|
|
449
|
+
Exit codes: 0 all gates as pinned; 1 unexpected deviation; 3 flight data absent
|
|
450
|
+
(installed package without a source checkout) — distinct from gate failure."""
|
|
451
|
+
from orp.gates import gate3_artemis_replay as gr
|
|
452
|
+
from orp.gates.summary import evaluate_gates
|
|
453
|
+
|
|
454
|
+
# The digitized flight datasets live at the repo root (data/flights/), outside
|
|
455
|
+
# the installable package — an installed wheel cannot run the replay gates.
|
|
456
|
+
if not Path(gr.SCHEDULE_CSV).is_file():
|
|
457
|
+
print(
|
|
458
|
+
"orp gates: the flight-replay gates need a source checkout (data/flights/ "
|
|
459
|
+
"is not shipped in the package); clone "
|
|
460
|
+
"https://github.com/OpenSourcePatents/ORP and run from the repo root."
|
|
461
|
+
)
|
|
462
|
+
return 3
|
|
463
|
+
|
|
464
|
+
report = evaluate_gates()
|
|
465
|
+
for row in report.rows:
|
|
466
|
+
print(row.line)
|
|
467
|
+
print(report.summary_line)
|
|
468
|
+
return 0 if report.all_expected else 1
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
# Entry point
|
|
473
|
+
# ---------------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
def main(argv: list[str] | None = None) -> int:
|
|
476
|
+
"""The ``orp`` console entry point. Returns the process exit code."""
|
|
477
|
+
parser = build_parser()
|
|
478
|
+
args = parser.parse_args(argv)
|
|
479
|
+
try:
|
|
480
|
+
return args.func(args)
|
|
481
|
+
except _Refusal as refusal:
|
|
482
|
+
print(f"orp: {refusal}", file=sys.stderr)
|
|
483
|
+
return 2
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
raise SystemExit(main())
|
orp/core/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ORP — Open Reentry Platform
|
|
2
|
+
# Copyright (C) Charles W. Dowd Jr.
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
"""ORP core: simulation engine, vehicle/environment models, and the provenance system.
|
|
5
|
+
|
|
6
|
+
This package mirrors the architecture of OpenRocket's flight-simulation subsystem
|
|
7
|
+
(design patterns only — no source is copied), adapted from ascent to atmospheric
|
|
8
|
+
*reentry*.
|
|
9
|
+
|
|
10
|
+
Two invariants hold across every module here. They are not guidelines; they are the
|
|
11
|
+
identity of the product.
|
|
12
|
+
|
|
13
|
+
FORWARD SIMULATION ONLY
|
|
14
|
+
-----------------------
|
|
15
|
+
ORP integrates the equations of motion forward in time from entry conditions and a
|
|
16
|
+
*replayed* :class:`~orp.core.bank_schedule.schedule.BankSchedule`. **No function anywhere
|
|
17
|
+
in ORP accepts a desired landing point (or any terminal target) and returns a bank
|
|
18
|
+
schedule, control law, or any other guidance solution.** ORP answers "given this control
|
|
19
|
+
history, where does the vehicle go?" — never the inverse "what control history reaches
|
|
20
|
+
this point?". The inverse problem (guidance / trajectory optimization / targeting) is
|
|
21
|
+
deliberately, permanently out of scope. If you are unsure whether a proposed function
|
|
22
|
+
crosses this line, it does: raise, do not compute.
|
|
23
|
+
|
|
24
|
+
PROVENANCE ON EVERYTHING
|
|
25
|
+
------------------------
|
|
26
|
+
Every vehicle property is a :class:`~orp.core.provenance.tags.TaggedValue` carrying a
|
|
27
|
+
:class:`~orp.core.provenance.tags.ValidationLevel` and a source citation. Every
|
|
28
|
+
environment/aerodynamic model exposes a :class:`~orp.core.provenance.tags.ProvenanceTag`.
|
|
29
|
+
Every simulation output (a :class:`~orp.core.simulation.flight_data.FlightData`) carries a
|
|
30
|
+
provenance tag computed as the *weakest* of all contributing inputs — a trajectory is only
|
|
31
|
+
as validated as the least-validated thing that produced it.
|
|
32
|
+
|
|
33
|
+
PLACEHOLDER-PHYSICS CONVENTION
|
|
34
|
+
------------------------------
|
|
35
|
+
This is an architectural skeleton. Methods that compute *flight-dependent* physics
|
|
36
|
+
(aerodynamic force coefficients, equation-of-motion derivatives, altitude-dependent
|
|
37
|
+
atmosphere profiles, latitude/altitude gravity variation) currently return zero — or, where
|
|
38
|
+
a zero would make a derived quantity undefined (e.g. density from a zero-temperature
|
|
39
|
+
atmosphere), a planet *reference constant*. Every such body is marked
|
|
40
|
+
|
|
41
|
+
# --- PHYSICS SEAM ---
|
|
42
|
+
|
|
43
|
+
with the real formula documented in the surrounding docstring, so a real implementation
|
|
44
|
+
drops into a named seam without reshaping the contract. Planet/vehicle *constants*
|
|
45
|
+
(gas constants, surface gravity, mean radius, rotation rate, mass, reference area) are real
|
|
46
|
+
values, because they parameterize the seams rather than being part of the placeholder
|
|
47
|
+
computation.
|
|
48
|
+
"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ORP — Open Reentry Platform
|
|
2
|
+
# Copyright (C) Charles W. Dowd Jr.
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
"""Aerodynamics — momentary flight conditions in, force coefficients out.
|
|
5
|
+
|
|
6
|
+
Mirrors OpenRocket's aerodynamics split: a mutable
|
|
7
|
+
:class:`~orp.core.aerodynamics.flight_conditions.FlightConditions` carries the instantaneous
|
|
8
|
+
state (Mach, angle of attack, dynamic pressure, atmosphere), and an
|
|
9
|
+
:class:`~orp.core.aerodynamics.calculator.AerodynamicCalculator` (Strategy) turns it into
|
|
10
|
+
an :class:`~orp.core.aerodynamics.calculator.AerodynamicForces` coefficient bundle.
|
|
11
|
+
:class:`~orp.core.aerodynamics.newtonian.ModifiedNewtonianCalculator` is the reentry-default
|
|
12
|
+
implementation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from orp.core.aerodynamics.calculator import AerodynamicCalculator, AerodynamicForces
|
|
16
|
+
from orp.core.aerodynamics.constant import ConstantCoefficientCalculator
|
|
17
|
+
from orp.core.aerodynamics.flight_conditions import FlightConditions
|
|
18
|
+
from orp.core.aerodynamics.newtonian import ModifiedNewtonianCalculator
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"FlightConditions",
|
|
22
|
+
"AerodynamicCalculator",
|
|
23
|
+
"AerodynamicForces",
|
|
24
|
+
"ConstantCoefficientCalculator",
|
|
25
|
+
"ModifiedNewtonianCalculator",
|
|
26
|
+
]
|