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.
Files changed (89) hide show
  1. orp/__init__.py +18 -0
  2. orp/cli.py +487 -0
  3. orp/core/__init__.py +48 -0
  4. orp/core/aerodynamics/__init__.py +26 -0
  5. orp/core/aerodynamics/calculator.py +108 -0
  6. orp/core/aerodynamics/constant.py +102 -0
  7. orp/core/aerodynamics/flight_conditions.py +76 -0
  8. orp/core/aerodynamics/newtonian.py +215 -0
  9. orp/core/atmosphere/__init__.py +26 -0
  10. orp/core/atmosphere/earth.py +129 -0
  11. orp/core/atmosphere/exponential.py +111 -0
  12. orp/core/atmosphere/mars.py +94 -0
  13. orp/core/atmosphere/model.py +113 -0
  14. orp/core/atmosphere/us76_highalt.py +128 -0
  15. orp/core/bank_schedule/__init__.py +13 -0
  16. orp/core/bank_schedule/schedule.py +393 -0
  17. orp/core/frames.py +159 -0
  18. orp/core/gravity/__init__.py +21 -0
  19. orp/core/gravity/earth.py +62 -0
  20. orp/core/gravity/mars.py +51 -0
  21. orp/core/gravity/model.py +43 -0
  22. orp/core/planet/__init__.py +20 -0
  23. orp/core/planet/planet.py +112 -0
  24. orp/core/planet/registry.py +68 -0
  25. orp/core/provenance/__init__.py +24 -0
  26. orp/core/provenance/tags.py +225 -0
  27. orp/core/report.py +100 -0
  28. orp/core/session.py +580 -0
  29. orp/core/simulation/__init__.py +37 -0
  30. orp/core/simulation/conditions.py +104 -0
  31. orp/core/simulation/engine.py +131 -0
  32. orp/core/simulation/flight_data.py +291 -0
  33. orp/core/simulation/status.py +172 -0
  34. orp/core/simulation/stepper.py +334 -0
  35. orp/core/vehicles/__init__.py +18 -0
  36. orp/core/vehicles/base.py +117 -0
  37. orp/core/vehicles/library.py +177 -0
  38. orp/data/vehicles/apollo.yaml +62 -0
  39. orp/data/vehicles/insight.yaml +83 -0
  40. orp/data/vehicles/msl.yaml +60 -0
  41. orp/data/vehicles/orion.yaml +78 -0
  42. orp/data/vehicles/stardust.yaml +86 -0
  43. orp/experiments/__init__.py +10 -0
  44. orp/experiments/insight_rotation.py +308 -0
  45. orp/gates/__init__.py +12 -0
  46. orp/gates/gate3_artemis.py +202 -0
  47. orp/gates/gate3_artemis_replay.py +418 -0
  48. orp/gates/gate_stardust.py +188 -0
  49. orp/gates/summary.py +139 -0
  50. orp/gui/__init__.py +10 -0
  51. orp/gui/app_state.py +389 -0
  52. orp/gui/conditions_panel.py +418 -0
  53. orp/gui/feedback.py +88 -0
  54. orp/gui/glossary.py +172 -0
  55. orp/gui/icon.py +65 -0
  56. orp/gui/info_icon.py +50 -0
  57. orp/gui/main_window.py +348 -0
  58. orp/gui/plots.py +305 -0
  59. orp/gui/results_panel.py +280 -0
  60. orp/gui/vehicle_panel.py +207 -0
  61. orp/tests/__init__.py +4 -0
  62. orp/tests/test_atmosphere_us76ext.py +76 -0
  63. orp/tests/test_bank_schedule_from_csv.py +554 -0
  64. orp/tests/test_bridge_openreentry.py +258 -0
  65. orp/tests/test_cli.py +629 -0
  66. orp/tests/test_eom_invariants.py +525 -0
  67. orp/tests/test_experiment_insight.py +114 -0
  68. orp/tests/test_frames.py +100 -0
  69. orp/tests/test_gate3_artemis.py +79 -0
  70. orp/tests/test_gate3_replay.py +88 -0
  71. orp/tests/test_gate_stardust.py +68 -0
  72. orp/tests/test_gui.py +836 -0
  73. orp/tests/test_gui_wall.py +155 -0
  74. orp/tests/test_physics_aero.py +149 -0
  75. orp/tests/test_physics_atmosphere.py +127 -0
  76. orp/tests/test_physics_eom.py +153 -0
  77. orp/tests/test_physics_fixtures.py +110 -0
  78. orp/tests/test_physics_gravity.py +74 -0
  79. orp/tests/test_plots.py +160 -0
  80. orp/tests/test_session.py +415 -0
  81. orp/tests/test_site.py +78 -0
  82. orp/tests/test_smoke.py +202 -0
  83. orp/tests/test_trajectory_channels.py +135 -0
  84. orp-0.0.1.dist-info/METADATA +210 -0
  85. orp-0.0.1.dist-info/RECORD +89 -0
  86. orp-0.0.1.dist-info/WHEEL +5 -0
  87. orp-0.0.1.dist-info/entry_points.txt +2 -0
  88. orp-0.0.1.dist-info/licenses/LICENSE +674 -0
  89. 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
+ ]