Skip to content

Feature/plume validation study - #129

Merged
andytorrestb merged 14 commits into
plume-kit:masterfrom
andytorrestb:feature/plume-validation-study
Jul 28, 2026
Merged

Feature/plume validation study#129
andytorrestb merged 14 commits into
plume-kit:masterfrom
andytorrestb:feature/plume-validation-study

Conversation

@andytorrestb

Copy link
Copy Markdown
Member

No description provided.

andytorrestb and others added 14 commits July 27, 2026 22:53
print_jfh_1d_approach_n_fire accepted an n_firings argument and then ignored
it: the emitted Jet Firing History held whatever number of entries the
deceleration simulation happened to produce.

- add validate_n_firings() to approach_maneuvers as the single definition of
  the rule (positive integer; zero, negative, fractional and non-numeric
  values are rejected rather than coerced);
- give compute_1d_approach an optional n_firings that stops the simulation at
  exactly that many entries and raises when the approach completes in fewer
  firings than requested, instead of silently returning a shorter history;
- validate the argument in print_jfh_1d_approach_n_fire, pass it through, and
  assert the written file's entry count.

Omitting n_firings keeps the previous behavior exactly, so no existing caller
changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A study configuration is a declarative layer ON TOP OF an existing PyRPOD
case: the case's config.ini keeps owning the vehicle, thruster, plume-model
and target assets, so every existing case and public API is untouched, while
the YAML adds only what a trade study needs and an INI cannot express --
swept plate angles and source distances, the sweep decomposition mode, the
exact firing count, explicitly prescribed firings, the moment reference
point, coefficient normalization, output/plot settings and an optional
external reference-data location.

Validation is strict and specific: a missing case directory, a case without a
config.ini, an unsupported plume model, an unknown sweep mode, an empty or
non-positive sweep axis, non-orthogonal target axes, duplicate component
names, a non-positive normalization value or a firing list whose length
disagrees with the mode's implied total each raise StudyConfigError naming
the offending key. The plume model is recorded explicitly and must be
SimplifiedGasKinetics; no model registry is introduced.

n_firings has ONE meaning in both sweep modes -- entries contributed by each
pose -- and reuses the rule defined with the JFH-generation code so the
prescribed and dynamics-driven paths cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Validation sweeps do not fly a trajectory: the firing poses are prescribed,
so this module owns their generation and the Jet Firing History they are
written to, leaving the dynamics-based approach-maneuver workflow untouched.

The pose convention -- source on the arc of radius L about the target
reference point, aimed at it, DCM first column carrying the thruster axis and
second column the sweep plane's binormal -- reproduces the committed sweep
JFH generators to file precision, for both the flat and inclined cases.

Two builders: one pose's firings (exactly n_firings entries), and the whole
sweep as one continuous sequence (exactly len(poses) * n_firings entries,
firing times running through). Each firing records the pose it realizes, so a
history spanning many poses stays keyed to the sweep grid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turns the strike pipeline's per-face pressure, shear and heat-flux arrays
into the integrated quantities a validation study reports: pressure, shear
and combined force vectors, the corresponding moments about a user-defined
reference point, center of pressure, peak surface loads, affected area,
component heat load and nondimensional coefficients. Per-face data are not
reduced away here -- they keep flowing to the VTK writer unchanged.

Conventions are stated explicitly and follow the pipeline's own
face-selection rule: pressure acts along -n_hat, shear along the tangential
projection of the radial flow direction.

The center of pressure is defined as the point on the resultant's line of
action closest to the moment reference, since a unique 3D center of pressure
does not exist in general; the irreducible force-parallel couple is reported
separately, and zero-load and near-cancellation cases return no center of
pressure with a status naming the reason rather than a misleadingly large
value. An always-defined pressure-weighted centroid is reported alongside.

Coefficients are computed only for the normalizations whose inputs are
supplied in full; nothing is defaulted or invented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One record per case, component and firing, carrying everything needed to
reproduce and later compare the calculation: identifiers, geometry and mesh
information, coordinate system and units, the prescribed source pose, the
swept parameters, the plume model and its parameters, the integrated loads,
the surface-field peaks, the optional coefficients, the artifact paths and
the code/configuration provenance.

Two machine-readable artifacts, both in formats the repository already uses
and with no new dependency: a flat CSV (vectors expanded to _x/_y/_z columns,
one column per coefficient, so it is directly plottable) and a JSON metadata
document carrying study-level provenance plus the nested per-case records --
the exchange format an externally generated dataset is transformed into for
comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compares study results against independently generated data WITHOUT knowing
its origin: a reference record is named quantities attached to matching keys,
loadable from CSV, JSON or YAML, and whether the numbers came from a DSMC
solver, an analytical solution, an experiment or another code changes
nothing. No producer-specific importer exists here.

Metrics are applied only where they are mathematically meaningful: absolute
error, relative error (None when the reference is zero, never infinity),
normalized RMSE, peak-value error, integrated-load error (compared as
vectors, so a load of the right magnitude pointing the wrong way is an error)
and center-of-pressure displacement. A quantity the reference supplies but
the result does not is reported as missing, and a case with no matching
record is listed as unmatched -- nothing is fabricated or defaulted.

The CSV layout is exactly what StudyResults.write_csv emits, so an external
producer can be transformed into it column-for-column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Force, moment and heat flux versus plate angle and source distance,
center-of-pressure travel, and a study-versus-reference error summary.

Every figure is optional output: the module is imported lazily by the study
engines and pins matplotlib's non-interactive Agg backend, so no run and no
automated test needs graphical output. Series are keyed on the swept pose
rather than on the case, so both study engines plot identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plumbing every study engine needs, in one place: build the existing
PyRPOD case objects (failing early on an unsupported plume model or an
unknown thruster id), precompute the target geometry and component face
selection once, read a generated JFH back through the normal
JetFiringHistory parser, run one history through the plume-strike
calculation, and turn a firing's per-face arrays into per-component result
records.

Strike execution has two paths: with VTK output enabled the full
PlumeStrikeEstimationStudy.jfh_plume_strikes() pipeline runs with the target
vehicle's output root redirected, so studies that execute several histories
cannot overwrite one another's artifacts; with it disabled the pipeline's own
per-firing core runs instead -- identical numbers, no files.

Living here rather than on a base class lets the two study engines be
siblings with no duplicated logic and no inheritance between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PlumeValidationStudy runs a configured sweep with ONE Jet Firing History per
angle-distance combination (sweep.mode: per_case, the default): every pose is
an independent case with its own history, strike run and artifacts, so no
pose's results can be polluted by another's. That is what a validation matrix
compared pose-by-pose against reference data wants.

Per case it builds the prescribed firings, writes and re-reads the case's
JFH, runs the plume-strike calculation, integrates per-component loads,
exports the per-face VTK fields, records structured results and optionally
compares against external reference data.

Known limitation, documented here and in the emitted provenance: face
selection is the pipeline's existing behavior, so plume shadowing, occlusion
and self-shadowing of concave targets are not modeled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ParameterSweepStudy runs the sweep as ONE case driven by ONE Jet Firing
History (sweep.mode: single_jfh): every pose contributes its firings to a
single history, the strike pipeline runs once over all of them, and the
strike files form a single results/strikes/firing-<i>.vtu series -- the
repository's existing sweep convention, scrubbable in ParaView as a time
sequence.

Because the history is shared, the pipeline's cumulative fields finally mean
something: max_pressures / max_shears become the worst load any pose put on
each face, cum_strikes a coverage map and cum_heat_flux_load an accumulated
dose. Their per-component summary is exposed as `study.envelope` and recorded
in the metadata; with the full pipeline disabled those arrays do not exist
and the envelope is reported empty rather than reconstructed.

A separate class rather than a mode flag inside the per-case engine: the two
decompositions answer different questions and their outputs differ in kind,
not just in layout. Everything they share lives in study_runtime, so
per-firing numbers are identical between engines and only the decomposition
differs. n_firings keeps its per-pose meaning, and the single history is
required to hold exactly len(poses) * n_firings entries -- checked after
writing and again after reading the file back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Repairs, in the legacy dynamics-driven sweep path -- each of which raised
before any physics ran:

- init_trade_study constructed PlumeStrikeEstimationStudy.RPOD(case_dir), a
  class that does not exist, and passed a case directory where the study
  takes a MissionEnvironment. It now builds the environment and the real
  PlumeStrikeEstimationStudy;
- the sweeps called jfh_plume_strikes(trade_study=True), which that method's
  (parallel, workers) signature rejects with TypeError. They now use the
  per-firing arrays it returns;
- print_mission_report read impingement maxima off study attributes that are
  never set. It now summarizes them from those returned arrays, keeping the
  old attribute path for existing callers;
- interpret_mission_report read self.rpod.config, which does not exist; the
  configuration lives on the study's MissionEnvironment.

Adds the package-level prescribed-validation API:

    study = TradeStudy.from_config('case.yaml')
    results = study.run()

with compare() and plot() alongside it, and an optional output_dir override
so a committed configuration can be run into a scratch location. The engine
is selected from sweep.mode -- per_case binds PlumeValidationStudy,
single_jfh binds ParameterSweepStudy -- so the API and the configuration file
are the same either way. The class stays a thin facade over the pyrpod.mdao
modules, and its legacy constructor keeps its original behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four examples on existing case assets, all recording the plume model
explicitly and taking moments about the target's own reference point:

- flat_plate_baseline.yaml: one head-on firing at L/D = 4;
- flat_plate_sweep.yaml: 19 angles x 5 distances as 95 independent cases;
- flat_plate_sweep_single_jfh.yaml: the same sweep as one 95-firing history,
  differing from the previous file only in sweep.mode and the output names;
- cylinder_baseline.yaml: the same machinery on a curved, closed target, to
  keep the architecture honest and to be where quantitative cylinder
  reference data plugs in later.

The flat-plate files carry the Cai normalization derived from the case's own
thruster definition file, so coefficients are available. The cylinder file
supplies none, so its coefficients are correctly reported as unavailable
rather than invented -- no cylinder reference data exists yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unit level:
- mdao_unit_test_03, the exact meaning of n_firings: invalid counts are
  rejected; one requested firing produces one JFH entry and N produce exactly
  N, read back through JetFiringHistory; an explicit firing list that
  disagrees is an error; a whole-sweep sequence holds exactly
  poses x n_firings pose-tagged entries with continuous firing times; the
  generated poses reproduce all 95 firings of the committed flat-plate sweep
  JFH to file precision; and the dynamics-driven approach honors an exact
  count, reporting an unreachable one rather than shortening the history.
- mdao_unit_test_04, surface-load integration on meshes whose loading has a
  known closed-form resultant: force, moment about a user-defined point,
  center of pressure (recovered from a linear pressure field, consistent with
  the reported moment, and unavailable with a stated reason for zero-load and
  cancelling loads), coefficients present and absent, component selection.
- mdao_unit_test_05, configuration parsing and validation, including that the
  case's own config.ini keeps parsing unchanged and that n_firings keeps its
  per-pose meaning in both sweep modes.
- mdao_unit_test_06, the comparison metrics and CSV / JSON / YAML datasets
  (compared identically regardless of origin), plus the result schema's
  serialization.

Integration level:
- mdao_integration_test_02, the baseline flat-plate case end to end through
  TradeStudy.from_config: artifacts, provenance, head-on physics, available
  coefficients, and agreement of the integrated normal load with the
  INDEPENDENT Cai 2016 exact reference, not with PyRPOD's own output.
- mdao_integration_test_03, a reduced multi-angle / multi-distance sweep:
  mirror symmetry in +/- angle, load decay with distance, center-of-pressure
  travel, per-case VTK isolation, optional plots, and the same machinery on
  the cylinder target with coefficients correctly unavailable.
- mdao_integration_test_04, the single-history engine: engine dispatch from
  the configuration, one JFH of exactly poses x n_firings entries, one record
  per firing keyed to its pose, a single results/strikes series, and the
  sweep envelope (absent, not fabricated, without the full pipeline). Its
  strongest assertion is equivalence -- per-firing loads match the per-case
  engine pose by pose.

Measured against the exact reference: integrated normal load within 1.5% mean
/ 2.2% max, component heat load ~15% -- the documented accuracy of the
Maxwellian engineering chain, asserted only as a loose envelope so a
convention regression fails loudly while the known gap passes. Studies write
into temporary directories, so a test run leaves no artifacts behind.

The manifest entries and the regenerated tests/README.md land here rather
than in a follow-up commit: the tooling suite checks that the manifest and
pytest's collection agree, so the two must move together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/plume_validation_study.md covers the repaired TradeStudy architecture
and the repairs made to the legacy sweeps, the package-level API, the two
sweep decompositions and when to use each, the YAML configuration schema, the
exact meaning of n_firings and prescribed firing lists, the force / moment /
center-of-pressure definitions (including why a unique 3D center of pressure
needs a stated convention and how degenerate cases are reported), coefficient
normalization requirements, the result schema, VTK outputs, optional plots,
the external reference-data format and how DSMC results plug into it later,
how to add another validation geometry, and the known limitations.

The root README gains a short pointer with the worked flat-plate example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@andytorrestb
andytorrestb force-pushed the feature/plume-validation-study branch from 515085f to 7e4422f Compare July 28, 2026 04:43
@andytorrestb
andytorrestb merged commit f1e824f into plume-kit:master Jul 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant