Skip to content

Feature/iss panel plume study - #130

Open
andytorrestb wants to merge 10 commits into
plume-kit:masterfrom
andytorrestb:feature/iss-panel-plume-study
Open

Feature/iss panel plume study#130
andytorrestb wants to merge 10 commits into
plume-kit:masterfrom
andytorrestb:feature/iss-panel-plume-study

Conversation

@andytorrestb

Copy link
Copy Markdown
Member

No description provided.

andytorrestb and others added 10 commits August 4, 2026 21:15
The strike pipeline hard-coded SimplifiedGasKinetics, so a study could
name a model but never select one. CollisionlessGasKinetics already
subclasses it with an identical constructor and overrides only the field
getters, so dispatch is a class lookup rather than a plugin framework.

pyrpod/plume/gas_kinetics_models.py adds that lookup plus the two pieces
of model-independent physics the pipeline needs:

  * local_field_state() reduces any model to one common LocalFieldState
    (number density, mass density, axial/radial velocity, velocity
    magnitude, temperature, local speed ratio), calling every field getter
    through the instance so an overriding model is honoured;
  * maxwellian_surface_loads() applies the Shen gas-surface interaction to
    that state, so pressure/shear/heat-transfer logic is written once.

The model is selected by the case's existing [pm] kinetics key
(Simplified, Collisionless, or None to disable surface loads), so both the
vectorized core and the scalar reference pick it up without any change to
PlumeStrikeEstimationStudy. An unknown key is an error, never a silent
fall back. Omitting it keeps the historical behavior, and the Simplified
path is preserved bit-for-bit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
study_config no longer hard-rejects every model but SimplifiedGasKinetics;
plume_model.name is validated against the registry and now SELECTS the
model that computes the plume field. An omitted block keeps the historical
default, and an unknown name is still a clear StudyConfigError.

Three schema additions, all optional and all defaulted so existing YAML
parses unchanged:

  * sweep.source_offsets_u / source_offsets_v translate the plume source
    parallel to the target surface, on the surface-local basis
    TargetSpec.local_basis() derives from the existing normal/tangent keys
    (u = tangent, v = n x u, u x v = n). Both default to [0.0].
  * sweep.source_axis_mode picks the pose convention: aim_at_reference
    (existing arc, aimed at the reference point) or parallel_to_normal
    (translated source, axis fixed anti-parallel to the normal). The two
    agree exactly at zero offset, so the new mode extends rather than
    redefines the old one. Incompatible combinations -- offsets while
    aiming, an approach angle with a fixed axis -- are rejected outright
    rather than silently combined.
  * a knudsen block records DERIVED metadata only: the mean free path must
    be given (never inferred from gas properties), exactly one of the two
    reference-length modes must be chosen, and nothing in the pipeline
    reads Kn back. The models stay collisionless.

SweepSpec.sweep_poses enumerates the full parameterization distance-major,
then u, then v, then angle, which collapses to the historical order at the
default offsets; SweepSpec.poses keeps its (angle, distance) projection for
existing callers. firing_plan gains translated_pose_for() and dispatches on
the axis mode in one place, so the per-case and single-history engines
cannot drift apart.

The obsolete assertion that CollisionlessGasKinetics must be REJECTED is
replaced by tests that both supported models are accepted, that an unknown
name is refused, and that omitting the block keeps the default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CaseResult gained the fields a panel study reports, all optional and
defaulted so older result files and the reference-comparison API keep
working unchanged:

  * source_offset_u / source_offset_v / source_axis_mode -- the pose that
    produced the record;
  * normal_force, local_force_u/v, local_moment_u/v/n and
    center_of_pressure_u/v -- the SAME integrated force and moment vectors
    re-expressed on the target's surface-local basis, adding no physics;
  * knudsen_number, mean_free_path, knudsen_reference_length and
    knudsen_definition -- empty columns, not fabricated values, when the
    study configures no knudsen block;
  * model_variant (a derived short label) and surface_distribution_path.

surface_loads gained project_to_panel_frame() and panel_local_coordinates()
with the sign conventions written down and worked through in the module
docstring: normal_force = -F.n is POSITIVE for a load pressing into the
panel, and a source displaced toward +u gives a positive local_moment_v.
CSV columns stay flat and directly plottable; quantity() exposes the new
comparable scalars; no per-face array is embedded in a CSV row.

load_case_assets no longer refuses every model but Simplified. It applies
the study's plume_model by setting the environment's in-memory [pm]
kinetics key -- the one input both strike paths read -- so the selection
drives the calculation rather than only the metadata. The case's
config.ini on disk is untouched, and a study naming the model its case
already configures changes nothing. Kinetics disabled is still an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds an opt-in second view of the per-face fields the VTK files already
carry: one CSV per case and component with face_index, global centroid,
panel-local (u, v), area, pressure, shear stress, heat flux and strike
count. The VTK export stays the primary full-resolution output and is
untouched; this is for plotting scripts, spreadsheets and a later
comparison workflow that should not need a VTK reader.

Values are copied through unchanged -- no interpolation, no resampling, no
structured-grid projection, and no common-grid projection onto any external
mesh. Every row is one native mesh face, traceable back to the mesh and the
VTK file by its preserved face_index. A sidecar JSON records the column
units, the panel basis, the pose, the plume model and any derived Knudsen
metadata, so a distribution file is self-describing.

Enabled with output.surface_distribution.enabled (default false); the path
lands in CaseResult.surface_distribution_path. Both engines export it, into
the per-case directory for per_case mode and the study root for single_jfh.

case_id_for() now names what actually varies: aim_at_reference keeps the
historical case000_alpha0p0_d4 form byte-for-byte, while parallel_to_normal
uses case000_modelCollisionless_L4_u0_v0, since the approach angle is fixed
in that mode. The per-case engine iterates the full SweepPose grid.

A firing with no swept distance (an explicitly prescribed pose in
single_jfh mode) records the mean free path and definition but leaves the
distance-referenced Kn unset rather than inventing one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
study_plots covers the angle sweep; panel_plots covers the offset sweep,
where the independent variable is the panel-local source offset:

  * plot_panel_pressure() draws one case's panel-local pressure field from
    its exported distribution CSV, with the panel edges and the plume
    centerline marked. A flat plate is an unstructured triangle mesh, so no
    structured grid is fabricated -- it is drawn as a Delaunay
    triangulation of the face centroids, falling back to a face-coloured
    scatter when the face count is too small for contours to be honest.
  * plot_offset_sweep_trends() writes normal force, moment about v, peak
    pressure and center-of-pressure u against the u offset, plus normal
    force against stand-off distance. Every series is grouped by distance
    AND plume model, so merged Simplified and Collisionless results stay
    separate rather than being averaged. Transverse figures appear only
    when v is actually swept.

study_runtime.study_plots_for() decides which families a given study wants
and is called by both engines, so the choice is not duplicated and does not
leak into the TradeStudy facade. Both plotting modules are imported lazily,
so a study that asks for no plots never imports matplotlib; the Agg backend
and the existing palette are reused, and no new dependency is added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An idealized 22 m x 12 m flat panel standing in for one ISS solar-array
wing, centered at the origin with its normal along +Z and struck by the
verified Cai 2016 argon round jet (D = 1 m, S0 = 2.0, T0 = 200 K,
Tw = 300 K, fully diffuse), with the source TRANSLATED parallel to the
panel rather than re-aimed at its center.

  config.ini                              case assets and gating geometry
  stl/generate_panel.py                   parameterized plate mesher
  stl/iss_panel.stl                       committed 44 x 24 quads, 2112 faces
  tcd/tcf_1_argon.txt, tcd/tdf.csv        single head-on argon thruster
  study/iss_panel_baseline_simplified.yaml
  study/iss_panel_baseline_full_cai.yaml
  study/iss_panel_offset_distance_sweep.yaml
  run.py                                  runner for one study or all
  README.md                               conventions, commands, artifacts

The two baselines differ in exactly one line -- plume_model.name -- so the
pair isolates the far-field simplification: 2.824 N vs 2.802 N centered
normal force, 0.3317 Pa vs 0.3136 Pa peak pressure. That the numbers differ
at all is the evidence that model selection reaches the calculation.

The sweep is 3 distances x 5 u offsets = 15 per_case cases, with
distribution export, plots and the Kn metadata block enabled; it runs in
about 20 s. Verified symmetric: zero moment and zero CoP offset at u = 0,
equal-and-opposite moments at +/-u, and normal force falling at the edges
as the footprint spills off the panel. README.md documents expanding the
arrays and running one study per Kn label of 100, 10, 1, 0.1, 0.01.

The mesher uses surfmesh (github.com/plume-kit/surfmesh) for the quad grid
and splits each quad into two triangles. Since the STL is committed, that
is a build-time dependency of the script alone -- neither PyRPOD nor the
tests import it.

Also moves the panel-pressure figure title to the figure so a
drawn-to-scale wide panel does not run its title under the colorbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cai2016_reference.py evaluates the exact Cai 2016 surface solution
(CaiImpingement2016.py, Eqs. 9-14) at the same panel face centroids a study
used and exports the same panel-local distribution schema, so the two can
be diffed column for column.

It is a REFERENCE GENERATOR, not a plume-model backend: it lives in the
case directory, PyRPOD never imports it, PlumeStrikeCalculator cannot reach
it, and it takes no part in any study run. CaiImpingement2016 stays
independent of the production strike pipeline exactly as before.

It implements normal incidence only (Cai alpha_0 = 90 deg), which is
precisely the parallel_to_normal pose, and refuses any other geometry
rather than silently mapping it. The frame mapping between the panel basis
and Cai's nozzle frame is written out in the docstring.

Value of the check: PyRPOD reaches the wall through a chain (plume field ->
LocalFieldState -> Shen formulas) while this reference integrates the
incident and re-emitted wall fluxes directly. At L = 4 m on the committed
mesh the peaks agree to 2.8% in pressure, 3.4% in shear and 7.8% in heat
flux -- the same documented Maxwellian-chain gap mdao_integration_test_02
already checks for integrated loads, now available per face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two new files, both registered in tests/test_manifest.yaml (README
regenerated with scripts/generate_test_dashboard.py). No existing assertion
was weakened or removed.

mdao_unit_test_07.py (63 tests) covers the units:
  * both Cai variants selectable by name, unknown names rejected rather
    than defaulted, kinetics keys round-tripping;
  * both models reduced to the same populated LocalFieldState, agreeing
    bit-for-bit on the centerline (shared closed forms) and differing off
    it (which is what makes the selection meaningful), with one shared
    Maxwellian implementation consuming either;
  * parallel_to_normal poses: centered position and DCM, a proper
    orthonormal rotation at every offset, translation without tilting the
    axis, and exact agreement with aim_at_reference at zero offset;
  * offsets defaulting to zero, case count = distances x u x v, the pose
    order, and refusal of incompatible pose definitions;
  * Kn for both reference-length modes, the five documented Kn labels,
    absent when unconfigured, and every invalid form rejected;
  * panel-local transforms and hand-computed normal-force, moment and
    center-of-pressure projections including the sign conventions;
  * the distribution schema and the new result columns.

mdao_integration_test_05.py (41 tests) runs the committed case end to end.
The key one: running the two baselines, which differ only in
plume_model.name, and asserting the LOADS differ -- if selection were
metadata only, they would be identical. Also re-integrates the exported
per-face pressures and checks the result against CaseResult.pressure_force,
checks centered-source symmetry and offset moment signs, the sweep's count,
order and identifiers, headless plotting, and that the existing flat-plate
configurations, case identifiers and baseline numbers are unchanged.

Sweep tests build their own 64-face panel rather than running the
committed 15-case sweep on 2112 faces; the baseline runs on the committed
mesh unchanged.

Full suite: 427 passed, 12 skipped, 1 failed --
rpod_unit_test_01::test_stl_to_vtk, which fails identically on master and
is unrelated (convert_stl_to_vtk does not write its output file).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ions

docs/plume_validation_study.md gains four sections and updates several:

  * Plume models -- the two selectable collisionless Cai variants, the
    mapping to [pm] kinetics, and the fact that the name SELECTS the model
    that computes the field rather than recording metadata. Notes that
    CaiImpingement2016 stays independent of the strike pipeline.
  * Source axis modes -- aim_at_reference vs parallel_to_normal written out
    as formulas, why they are different experiments, and that they coincide
    exactly at zero offset.
  * Panel-local u/v offsets -- the basis derived from the existing
    normal/tangent keys, the enumeration order, the case count and the case
    identifier forms.
  * Derived Knudsen metadata -- both reference-length modes, every
    validation rule, and repeated statements that Kn never enters the
    solution.
  * Panel-local surface distributions -- the column schema, why it exists
    alongside (never instead of) the VTK files, and the explicit absence of
    interpolation and common-grid projection.
  * ISS-panel example -- exact commands for all three studies and the
    independent Cai 2016 cross-check.

Sign conventions now have their own subsection with a table and a worked
moment derivation: normal_force = -F.n is positive INTO the panel, and a
source displaced toward +u gives a positive local_moment_v.

The result-schema and YAML sections list every addition with its default and
state that all of them are optional or safely defaulted. The DSMC subsection
and Known limitations now say plainly that no DSMC handling exists -- no
OpenFOAM execution, dictionaries, field import, mesh interpolation, job
management or comparison report -- and that both models are collisionless
with no collisional, wake or shadowing correction.

Also drops the stale "the only plume model this workflow supports" comment
from the committed flat-plate baseline YAML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two narrow typing slips in the new code, both annotation-only:

  * KnudsenSpec.from_mapping's XOR check guarantees the explicit reference
    length is not None on that branch, but mypy cannot see it -- narrowed
    with a targeted ignore and a comment naming the invariant;
  * plot_panel_pressure's mappable is a TriContourSet on one branch and a
    PathCollection on the other, so it is annotated as their common
    ScalarMappable base.

`python -m mypy pyrpod/mdao pyrpod/plume` is clean again (18 files), as it
was on master (15 files).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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