Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5f7f728
nozzle: drop unused PhysicsValidator import (combustion-efficiency fi…
swissskimmilk Jul 21, 2026
3c45435
Added Python linting for errors to CI/CD and fixes failures
swissskimmilk Jul 15, 2026
00fa37b
Dead code cleanup, improved CI catching of bad syntax and practices
swissskimmilk Jul 15, 2026
71c7e10
thermal (layer 3): fix 386x gas viscosity unit error, add sanity and …
swissskimmilk Jul 21, 2026
f4d1f58
nozzle golden tests: drop retired momentum-method thrust from exit-st…
swissskimmilk Jul 21, 2026
607f882
stability (layer1/2): fail-safe chug fallback, kill dead evil scoring…
swissskimmilk Jul 21, 2026
8361291
thermal (layer 3): take cp/k/Pr from CEA
swissskimmilk Jul 21, 2026
eaa0cf7
thermal (layer 3): solve for the wall temp properly, fix old version …
swissskimmilk Jul 21, 2026
21e86ee
thermal (layer 3): fix silent dict key typos
swissskimmilk Jul 21, 2026
d306feb
Seemingly working blowdowm, will continue to work on it
swissskimmilk Jul 21, 2026
2ee6b55
blowdown: share solver helpers across consumers, stop timeseries kill…
swissskimmilk Jul 21, 2026
6998525
thermal (layer 3): use gas emissivity approx for gas radiation, not t…
swissskimmilk Jul 21, 2026
61c28bf
thermal (layer 3): warn on kero mixes that temperature modeling is sh…
swissskimmilk Jul 21, 2026
c31dbf2
chug: forward real injector pressure drop to the stability model, sto…
swissskimmilk Jul 21, 2026
2dd8cfb
combustion: apply Huzel Tc x eta^2, so combustion efficiency is actua…
swissskimmilk Jul 21, 2026
95ec99d
config: guard that every config meets its own gates, plus per-commit …
swissskimmilk Jul 21, 2026
a165666
parity: run A/B locally when the kernel is built, and compare against…
swissskimmilk Jul 21, 2026
f65a43e
backend: boot on canonical/impinging, not the presetless default.yaml…
swissskimmilk Jul 21, 2026
1832596
thermal: evaluate heat transfer at the combustion-corrected chamber t…
swissskimmilk Jul 21, 2026
4c98024
config guard: fail only on broken configs, report off-spec ones inste…
swissskimmilk Jul 21, 2026
05546ba
thermal: add Bartz gas-side film coefficient, validated term-by-term …
swissskimmilk Jul 21, 2026
bc9b719
config: share liner/insert material constants via material_preset ins…
swissskimmilk Jul 21, 2026
ccd84fa
thermal: add Bartz sigma property-variation correction, checked again…
swissskimmilk Jul 21, 2026
78cd55d
thermal: cite Huzel eq 4-14 for the Bartz sigma correction, pin the e…
swissskimmilk Jul 21, 2026
84b0b22
config: split canonical configs into hand-authored intent and optimiz…
swissskimmilk Jul 21, 2026
bfad3f2
thermal: document Bartz provenance with Huzel page refs, distinguish …
swissskimmilk Jul 21, 2026
c2c735e
config: rename design sidecar to .outputs.yaml and move synced burn_t…
swissskimmilk Jul 21, 2026
35b1108
nozzle: derive throat curvature radius from the rao arc coefficients,…
swissskimmilk Jul 21, 2026
12d975b
config: move the design-point stamp to the outputs sidecar, read requ…
swissskimmilk Jul 21, 2026
2f3f234
thermal: add bartz_chamber_h_g, one gas-side provider for all cooling…
swissskimmilk Jul 21, 2026
832bc9c
thermal: use Bartz for the gas-side film coefficient instead of Dittu…
swissskimmilk Jul 21, 2026
09ac92d
native tests: rebuild all targets before ctest via a setup fixture, c…
swissskimmilk Jul 21, 2026
2a053f9
config: stamp the achieved design point (F/MR/Pc) on regeneration, ad…
swissskimmilk Jul 21, 2026
71b4896
config: un-merge presets in save_config so a regenerated config keeps…
swissskimmilk Jul 22, 2026
7b18542
config: regenerate canonical/impinging via Layer 1 for methalox 8000N…
swissskimmilk Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Lint (ruff bug-gate)

# Repo-wide static bug-gate. Runs the bug-detecting ruff ruleset (see /ruff.toml)
# over every subproject — EngineDesign, daq-server, firmware, pid-designer — to catch
# undefined-name / use-before-assignment / redefinition defects that unit tests miss
# when the crashing path isn't exercised. Fast (~seconds): no project deps installed,
# just ruff itself.

# Quoted `on` — YAML 1.1 treats bare `on` as boolean true (matches the other workflows).
'on':
push:
pull_request:
workflow_dispatch:

# Cancel superseded runs on the same ref to save CI minutes.
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true

jobs:
ruff:
name: Ruff bug-gate (whole repo)
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

# Pinned so a future ruff release adding new rules to the selected families
# can't turn this gate red without an intentional bump.
- name: Install ruff
run: pip install ruff==0.15.21

# Config (rule selection) lives in /ruff.toml so local `ruff check` matches CI.
- name: Run ruff bug-gate
run: ruff check .
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ docs/doxygen/

# DAQ output data
scripts/postprocessing/output/

# Engine design review working doc (local only)
ENGINE_DESIGN_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import streamlit as st
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import time
import uuid

from engine.pipeline.config_schemas import PintleEngineConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def run_full_engine_optimization_with_flight_sim(
"""
from engine.pipeline.system_diagnostics import SystemDiagnostics
from scipy.optimize import minimize, differential_evolution
from pathlib import Path
from datetime import datetime

# Get objective tolerance for early stopping
Expand Down Expand Up @@ -171,6 +170,11 @@ def update_progress(stage: str, progress: float, message: str):
lox_P_end_ratio = pressure_config.get("lox_end_pct", 0.70)
fuel_P_start = pressure_config.get("fuel_start_psi", 500) * psi_to_Pa
fuel_P_end_ratio = pressure_config.get("fuel_end_pct", 0.70)
# Hard pressure ceilings, also used as the fallback tank-start pressures (0.8x)
# when Layer 1 doesn't return a pressure curve. Read here rather than in the
# Layer 2 block below because those fallbacks are consumed far earlier.
max_lox_P_psi = pressure_config.get("max_lox_pressure_psi", 500)
max_fuel_P_psi = pressure_config.get("max_fuel_pressure_psi", 500)

# ------------------------------------------------------------------
# Estimate ambient pressure at launch site for exit-pressure targeting
Expand Down Expand Up @@ -360,11 +364,10 @@ def update_progress(stage: str, progress: float, message: str):
if use_time_varying and layer1_acceptable:
try:
from engine.optimizer.layers.layer2_pressure import run_layer2_pressure

# Get max pressures from config (these are HARD LIMITS - never exceeded)
max_lox_P_psi = pressure_config.get("max_lox_pressure_psi", 500)
max_fuel_P_psi = pressure_config.get("max_fuel_pressure_psi", 500)


# max_lox_P_psi / max_fuel_P_psi (hard pressure ceilings) are read once
# near the top of the function since earlier fallbacks depend on them.

# Get rocket mass and tank capacity from config (if available)
# These are needed for impulse and capacity calculations in layer2_pressure
rocket_dry_mass_kg = getattr(config_obj.rocket, 'dry_mass_kg', None) if hasattr(config_obj, 'rocket') else None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

from __future__ import annotations

from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, Tuple
import numpy as np
import pandas as pd
import streamlit as st
import copy
from plotly.subplots import make_subplots
from engine.core.chamber_geometry import chamber_geometry_calc

from engine.pipeline.config_schemas import PintleEngineConfig
from engine.core.runner import PintleEngineRunner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3591,8 +3591,7 @@ def _sanitize_for_yaml(obj):
graphite_recession_rate_thermal = np.asarray(graphite_recession_rate_thermal[:n_points])
graphite_recession_rate_oxidation = np.asarray(graphite_recession_rate_oxidation[:n_points])

# Create plots
import plotly.graph_objects as go
# Create plots (go / make_subplots are imported at module scope)
from plotly.subplots import make_subplots

# Convert Pa to psi (1 psi = 6894.76 Pa)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ def size_complete_geometry(
L_chamber = cg.length if cg.length else (cg.volume / cg.A_throat if cg.A_throat and cg.A_throat > 0 else 0.18)

# Calculate diameters
if L_chamber > 0:
if V_chamber > 0 and A_throat > 0:
if L_chamber > 0:
D_chamber_initial = np.sqrt(4.0 * V_chamber / (np.pi * L_chamber))
else:
D_chamber_initial = np.sqrt(4.0 * V_chamber / np.pi) # Assume cylindrical
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Smoke test for the full multi-layer optimization orchestrator.

``run_full_engine_optimization_with_flight_sim`` (``engine/optimizer/main_optimizer.py``)
is the Streamlit/CLI "full optimize" entrypoint that chains Layers 1-4. It has no other
test coverage, and a use-before-assignment ``NameError`` (``max_lox_P_psi``) once made it
crash on *every* run right after Layer 1 — invisible to CI because nothing exercised it
(the backend/React path calls the layers directly).

This test stubs the heavy layer entrypoints and drives the orchestrator end-to-end with
``use_time_varying=False`` so the *glue* between layers — variable plumbing, the sample-based
interpolation fallback, and the final result assembly — is exercised in well under a second
without running a real optimization or touching CEA. It guards against that whole class of
orchestration-wiring regression (NameError / TypeError / bad result shape), which static
linting alone cannot fully cover (e.g. wrong call signatures).
"""

import pytest
from unittest.mock import patch, MagicMock

# The orchestrator imports the Streamlit/RocketPy/plotly UI chain at import time. These are
# installed in CI (requirements-ci.txt); skip cleanly if a minimal local env lacks them.
mo = pytest.importorskip("engine.optimizer.main_optimizer")
from engine.pipeline.io import load_config

CANONICAL_CONFIG = "configs/canonical/impinging.yaml"


def _fake_layer1(**kwargs):
"""Stand in for run_layer1_optimization: return a real config + minimal results dict."""
cfg = load_config(CANONICAL_CONFIG)
layer1_results = {
"performance": {
"F": 7000.0,
"initial_thrust_error": 0.05,
"initial_MR_error": 0.05,
"pressure_candidate_valid": True,
},
"optimized_pressure_curves": {"lox_start_psi": 500.0, "fuel_start_psi": 500.0},
"iteration_history": [],
"convergence_info": {},
}
return cfg, layer1_results


def test_full_orchestrator_wiring_smoke():
"""The orchestrator runs entry -> Layer 1 -> result assembly without raising."""
cfg = load_config(CANONICAL_CONFIG)

# Stubbed runner: evaluate() returns a real numeric dict so the sample-based
# interpolation fallback (interp1d over sampled thrust/Isp/Pc) gets real arrays.
runner_stub = MagicMock()
runner_stub.evaluate.return_value = {
"F": 7000.0, "Isp": 250.0, "Pc": 2.0e6,
"mdot_O": 1.0, "mdot_F": 0.4, "mdot_total": 1.4, "MR": 2.5,
}

requirements = {"target_thrust": 7000.0, "target_apogee": 3048.0, "optimal_of_ratio": 2.55}
tolerances = {"thrust": 0.10, "apogee": 0.15}
pressure_config = {
"lox_start_psi": 500.0, "fuel_start_psi": 500.0,
"max_lox_pressure_psi": 600.0, "max_fuel_pressure_psi": 600.0,
}

with patch.object(mo, "run_layer1_optimization", side_effect=_fake_layer1), \
patch.object(mo, "PintleEngineRunner", return_value=runner_stub):
out_cfg, results = mo.run_full_engine_optimization_with_flight_sim(
config_obj=cfg,
runner=runner_stub,
requirements=requirements,
target_burn_time=10.0,
max_iterations=1,
tolerances=tolerances,
pressure_config=pressure_config,
use_time_varying=False,
)

# Return contract: (config, results dict). The tail assembly — which lives past the
# line that used to NameError — must run and stamp the pressure-curve config.
assert out_cfg is not None
assert isinstance(results, dict)
assert "pressure_curve_config" in results
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,13 @@ def design_optimization_view(config_obj: PintleEngineConfig, runner: Optional[Pi
""")

# Create tabs for different optimization stages
tab_design, tab_full_engine, tab_layer1, tab_layer2, tab_layer3, tab_layer4, tab_layer5 = st.tabs([
tab_design, tab_full_engine, tab_layer1, tab_layer2, tab_layer3, tab_layer4 = st.tabs([
"📋 Design Requirements",
"🚀 Full Engine Optimizer",
"🔧 Layer 1: Static Optimization",
"⏱️ Layer 2: Pressure Candidate",
"🔥 Layer 3: Thermal Protection",
"✈️ Layer 4: Flight Simulation",
"📊 Layer 5: Flight Analysis"
"✈️ Layer 4: Flight Simulation"
])

with tab_design:
Expand All @@ -313,9 +312,6 @@ def design_optimization_view(config_obj: PintleEngineConfig, runner: Optional[Pi

with tab_layer4:
config_obj = _layer4_tab(config_obj, runner)

with tab_layer5:
config_obj = _layer5_tab(config_obj, runner)


return config_obj

56 changes: 56 additions & 0 deletions EngineDesign/archive/dead_code_20260721/heuristic_chug_scoring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""ARCHIVED 2026-07-21 — heuristic chug "stability index/margin" removed from
``engine/pipeline/stability/analysis.py::calculate_chugging_frequency``.

Why it was removed
------------------
1. It measured the WRONG variables. The index was a product of a chamber-pressure factor, an
L* factor and a frequency-band factor. It never looked at **injector stiffness
(dP_inj / Pc)**, feed inertance/resistance, or the combustion time lag — i.e. none of the
physics that actually drives chug. It therefore could not distinguish a stable design from
an unstable one, and was structurally blind to the blowdown failure mode (stiffness
degrading as tank pressure decays).

2. It was NON-BINDING. With ``stability_index`` floored at 0.4 and the mapping
``margin = index * 1.5 + 0.4``, the minimum achievable margin was exactly 1.0, and a typical
design landed ~1.42 — comfortably above every configured threshold. It could not fail a real
design. The floors and the "more generous mapping" were added incrementally to stop it
rejecting good designs (see the commit comments: "More lenient factors...", "Minimum 0.4 for
any reasonable design", "More generous mapping...") rather than fixing the model.

3. It was already DEAD. ``comprehensive_stability_analysis`` overwrote
``chugging["stability_margin"]`` with the physical ``chug_gate_margin`` from
``compute_physical_stability``, so this value was computed and then discarded on every
evaluation. It survived only as the silent neutral-pass fallback, which is now fail-safe.

Replacement: the rigorous lumped model in ``engine/pipeline/stability/chug.py`` (impedance
characteristic equation -> Nyquist gain margin / growth rate), reached via
``compute_physical_stability``.

Kept for reference only. Not imported anywhere.
"""


def _archived_chug_stability_index(Pc: float, Lstar: float, freq: float) -> "tuple[float, float]":
"""The removed heuristic. Returns (stability_index, stability_margin)."""
Pc_ref = 1.0e6
Lstar_ref = 1.0
Pc_factor = min(1.0, (Pc / Pc_ref) ** 0.3) if Pc > 0 else 0.0
Lstar_factor = min(1.0, (Lstar / Lstar_ref) ** 0.2) if Lstar > 0 else 0.0
Pc_factor = max(Pc_factor, 0.6) if Pc > 0.3e6 else Pc_factor
Lstar_factor = max(Lstar_factor, 0.7) if Lstar > 0.6 else Lstar_factor

if freq < 5.0:
f_factor = 0.6
elif freq < 10.0:
f_factor = 0.75
elif freq > 600.0:
f_factor = 0.9
elif freq > 400.0:
f_factor = 0.95
else:
f_factor = 1.0

stability_index = Pc_factor * Lstar_factor * f_factor
stability_index = max(stability_index, 0.4)
stability_margin = stability_index * 1.5 + 0.4
return float(stability_index), float(stability_margin)
21 changes: 16 additions & 5 deletions EngineDesign/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,27 @@

@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan - load default config on startup."""
# Try to load default config on startup
default_config_path = project_root / "configs" / "default.yaml"
"""Application lifespan - load the canonical impinging config on startup.

Was ``configs/default.yaml``, which sets no ``propellant_preset`` -- so the loaded config had
NO fluid identity (``fluids.oxidizer.name`` and ``fluids.fuel.name`` both None). Evaluating it
yields non-finite solver residuals, and via the runner it produced NEGATIVE thrust
(F = -334 N, Isp = -7.27 s). The app booted on that config, so a fresh session evaluated a
physically impossible engine until the user loaded something else.

The canonical configs are already the documented source of truth -- ``POST /api/config/switch``
loads ``configs/canonical/<injector_type>.yaml`` wholesale on an injector change (see
CONFIG_SYSTEM.md). Booting from one removes a third, unmaintained config from the startup path
instead of keeping it in sync.
"""
default_config_path = project_root / "configs" / "canonical" / "impinging.yaml"
if default_config_path.exists():
try:
config_obj = load_config(str(default_config_path))
app_state.set_config(config_obj, str(default_config_path))
print(f"Loaded default config from {default_config_path}")
print(f"Loaded startup config from {default_config_path}")
except Exception as e:
print(f"Warning: Could not load default config: {e}")
print(f"Warning: Could not load startup config: {e}")

yield # App runs here

Expand Down
10 changes: 6 additions & 4 deletions EngineDesign/backend/routers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,13 @@ def deep_merge(base: dict, updates: dict) -> dict:
# Validate and create new config
new_config = PintleEngineConfig(**merged)

# Save to disk if we have a path
# Save to disk if we have a path. MUST go through io.save_config: a split config keeps its
# optimizer-generated half in <stem>.outputs.yaml, and dumping the merged model into the
# intent file would put generated fields where the loader forbids them -- every subsequent
# load would then raise on the collision.
if app_state.config_path:
with open(app_state.config_path, "w", encoding="utf-8") as f:
# Use clean dict (exclude unset/none if desired, but here we just dump the model)
yaml.dump(config_to_dict(new_config), f, default_flow_style=False, sort_keys=False)
from engine.pipeline.io import save_config as _save_config
_save_config(config_to_dict(new_config), app_state.config_path)

app_state.set_config(new_config, app_state.config_path)

Expand Down
21 changes: 13 additions & 8 deletions EngineDesign/backend/routers/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,19 @@ async def get_chamber_geometry():
# These are the inputs to solve_chamber_geometry_with_cea
# =====================================================================
if chamber_geom is not None:
# New unified chamber_geometry section
Pc_design = getattr(chamber_geom, 'design_pressure', 2.0e6)
F_design = getattr(chamber_geom, 'design_thrust', 5000.0)
MR = getattr(chamber_geom, 'design_MR', 2.55)
Lstar = getattr(chamber_geom, 'Lstar', 1.0)
D_chamber_design = getattr(chamber_geom, 'chamber_diameter', D_chamber)
D_exit_design = getattr(chamber_geom, 'exit_diameter', D_exit)
nozzle_eff = getattr(chamber_geom, 'nozzle_efficiency', 0.95)
# New unified chamber_geometry section. These are REQUIRED schema fields
# (ChamberGeometryConfig: design_pressure/design_thrust/design_MR/Lstar/
# chamber_diameter/nozzle_efficiency all Field(gt=0), no default), so read them
# directly. getattr-with-default here would be fail-open: a missing/renamed field
# would silently substitute a magic number and yield a physically different
# engine instead of raising -- the same masking pattern removed elsewhere.
Pc_design = chamber_geom.design_pressure
F_design = chamber_geom.design_thrust
MR = chamber_geom.design_MR
Lstar = chamber_geom.Lstar
D_chamber_design = chamber_geom.chamber_diameter
D_exit_design = chamber_geom.exit_diameter
nozzle_eff = chamber_geom.nozzle_efficiency
else:
# Legacy sections (chamber and nozzle should exist if we get here due to check above)
Pc_design = getattr(chamber, 'design_pressure', 2.0e6) if chamber else 2.0e6
Expand Down
Loading
Loading