Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
245 changes: 245 additions & 0 deletions case/plume/iss_panel_thesis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# ISS-representative solar-panel plume impingement

An idealized **22 m x 12 m flat panel**, standing in for one ISS solar-array
wing, struck by a single argon round jet whose source is **translated
parallel to the panel**. The case generates reproducible analytical PyRPOD
datasets for the collisionless Cai plume models.

> **No DSMC.** These studies produce analytical PyRPOD data only. Nothing in
> this case launches OpenFOAM, reads or writes DSMC fields, or applies a
> collisional correction. Comparing these datasets with externally generated
> DSMC results is a **separate workflow**.

## Geometry and conventions

| | |
|---|---|
| Panel | 22 m (u) x 12 m (v), geometric center at the origin |
| Panel normal `n` | global **+Z**, pointing toward the plume source |
| Longitudinal `u` | global **+X**, the 22 m dimension (`target.tangent`) |
| Transverse `v` | global **+Y**, the 12 m dimension (`n x u`, derived) |
| Moment reference | the panel center, `[0, 0, 0]` |
| Thruster | argon round jet, D = 1 m, S0 = 2.0, T0 = 200 K, Tw = 300 K, fully diffuse |
| Committed mesh | 44 x 24 quads -> **2112 triangles**, 0.5 m elements |

The plume source is placed by

```
source_position = panel_center + L*n + u_offset*u + v_offset*v
plume axis = -n (fixed, never re-aimed)
```

so the plume centerline meets the panel exactly at `(u_offset, v_offset)`.
This is `sweep.source_axis_mode: parallel_to_normal`. It is **not** the same
experiment as moving the source while continuously aiming it at the panel
center — that is the default `aim_at_reference` mode, which the existing
flat-plate studies use.

**Sign conventions.** `normal_force = -F.n` is **positive when the load
presses into the panel** (away from the source). `local_moment_u/v/n` are the
global moment vector projected on `(u, v, n)` about the panel center; a
source displaced toward `+u` gives a **positive** `local_moment_v`.
`center_of_pressure_u/v` are measured **from the panel center**.

## Studies

| Command | Config | What it is |
|---|---|---|
| `baseline-simplified` | `study/iss_panel_baseline_simplified.yaml` | Centered source, L = 4 m, `SimplifiedGasKinetics` |
| `baseline-full-cai` | `study/iss_panel_baseline_full_cai.yaml` | The same case with `CollisionlessGasKinetics` |
| `sweep` | `study/iss_panel_offset_distance_sweep.yaml` | 3 distances x 5 u offsets = 15 cases |

The two baselines differ in **exactly one line** — `plume_model.name` — so
the pair isolates the far-field simplification. On the committed mesh they
give a centered normal force of 2.824 N (Simplified) against 2.802 N (full
Cai), with peak pressures of 0.3317 Pa and 0.3136 Pa.

## Exact commands

Run from the repository root. Each command writes into the study's own
configured output directory under `results/` (gitignored).

```bash
# List the available studies
python case/plume/iss_panel_thesis/run.py

# 1. Simplified baseline (seconds)
python case/plume/iss_panel_thesis/run.py baseline-simplified

# 2. Full collisionless Cai baseline (~20 s on the committed mesh)
python case/plume/iss_panel_thesis/run.py baseline-full-cai

# 3. Distance/offset sweep, 15 cases (~20 s)
python case/plume/iss_panel_thesis/run.py sweep

# Everything, in that order
python case/plume/iss_panel_thesis/run.py all

# Run into a scratch directory instead, and watch progress
python case/plume/iss_panel_thesis/run.py sweep \
--output-dir /tmp/iss_panel --verbose
```

Plots are generated by default (both YAML files set `output.plots.enabled`
and `per_case_distribution`). To run without them:

```bash
python case/plume/iss_panel_thesis/run.py sweep --no-plots
```

To generate plots **separately** from an already-run study — or from Python
generally:

```python
from pyrpod.mdao.TradeStudy import TradeStudy

study = TradeStudy.from_config(
'case/plume/iss_panel_thesis/study/iss_panel_offset_distance_sweep.yaml')
results = study.run()
paths = study.plot() # returns every figure path written
```

## Where the outputs land

For the sweep (`results/studies/offset_distance_sweep/`):

```
jfh/case000_modelSimplified_L2_um10_v0.A prescribed JFH, one per case
cases/case000_modelSimplified_L2_um10_v0/
results/strikes/firing-0.vtu per-face VTK (PRIMARY output)
distributions/case000_..._panel_firing001.csv panel-local distribution CSV
distributions/case000_..._panel_firing001.meta.json units + case metadata
sweep_results.csv one flat row per case x component x firing
sweep_metadata.json provenance + nested case records
plots/normal_force_vs_offset_u.png sweep trends
plots/moment_v_vs_offset_u.png
plots/peak_pressure_vs_offset_u.png
plots/cop_u_vs_offset_u.png
plots/normal_force_vs_distance.png
plots/panel_pressure_case000_....png per-case pressure map
```

The baselines use the same layout with `case_results.csv` /
`study_metadata.json`. `CaseResult.vtk_path` and
`CaseResult.surface_distribution_path` record the per-case artifact paths, so
the summary CSV points at everything else.

**The VTK files remain the primary full-resolution output.** The
distribution CSVs carry the *same* native per-face values in panel-local
`(u, v)` coordinates — no interpolation, no resampling, no structured grid —
for plotting and for a later comparison workflow that should not need a VTK
reader. Columns: `face_index, centroid_x/y/z, local_u, local_v, area,
pressure, shear_stress, heat_flux, strike_count`; units and case metadata are
in the sidecar `.meta.json`.

## Regenerating the mesh

The STL is committed, so nothing at run time needs a mesher. To change the
resolution:

```bash
cd case/plume/iss_panel_thesis/stl
python generate_panel.py # committed default, 2112 faces
python generate_panel.py --n-u 88 --n-v 48 # 0.25 m elements, 8448 faces
```

The generator uses [`surfmesh`](https://github.com/plume-kit/surfmesh) for the
quad grid (`pip install surfmesh`). It is a **build-time dependency of that
script only** — PyRPOD and the automated tests never import it.

Raising the resolution costs linearly in struck faces. The full Cai model is
roughly 600x more expensive **per struck face** than the simplified one
(quadrature over the nozzle exit disk versus a closed form), so a
research-grade full-Cai sweep is the combination to size carefully.

## Expanding the sweep

The committed sweep is deliberately small enough to run locally. To grow it,
edit `study/iss_panel_offset_distance_sweep.yaml`:

```yaml
sweep:
source_distances: [1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0]
source_offsets_u: [-10.0, -8.0, -6.0, -4.0, -2.0, 0.0,
2.0, 4.0, 6.0, 8.0, 10.0]
source_offsets_v: [0.0, 2.0, 4.0] # transverse offsets too
```

The case count is exactly `n_distances x n_u_offsets x n_v_offsets`. Swept
`v` offsets automatically add the transverse trend figures.

To compare both models over the same grid, copy the file, change
`plume_model.name` and `study.name`, run both, and concatenate the two
summary CSVs — every trend figure groups its series by distance **and**
model.

### Knudsen labels

**Kn in PyRPOD is derived metadata, not a collisional model input.** The
plume models are collisionless; nothing in the solution reads Kn back. The
block exists so an analytical case can be *labelled* with the rarefaction
regime it is meant to represent, for a later, separate comparison workflow.

The committed sweep uses the swept stand-off as the reference length:

```yaml
knudsen:
mean_free_path_m: 1.0
reference_length: source_distance
definition: lambda_over_source_distance
```

so `Kn = 1/L` varies across the sweep (0.5, 0.25, 0.1667 at L = 2, 4, 6 m).

For a fixed reference length — one study **per Kn label** — use the explicit
mode instead. With the nozzle diameter D = 1 m as the reference, copy the
sweep YAML five times and set `mean_free_path_m` to `100`, `10`, `1`, `0.1`
and `0.01` to obtain **Kn = 100, 10, 1, 0.1, 0.01**:

```yaml
knudsen:
mean_free_path_m: 0.1 # -> Kn = 0.1
reference_length_m: 1.0 # nozzle diameter D
definition: lambda_over_nozzle_diameter
```

Give each copy its own `study.name` and `output_dir`. Exactly one
reference-length mode may be given; supplying both, or neither, is a
configuration error.

## Independent Cai 2016 reference (optional)

`cai2016_reference.py` evaluates the **exact** Cai 2016 surface solution
(`pyrpod/plume/CaiImpingement2016.py`, Eqs. 9-14) at the *same* face
centroids and exports the *same* distribution schema, so the two can be
diffed column for column.

It is an **independent analytical reference generator, not a plume-model
backend**: PyRPOD never imports it, `PlumeStrikeCalculator` cannot reach it,
and it takes no part in any study run. It implements normal incidence only
(Cai's `alpha_0 = 90 deg`), which is exactly the `parallel_to_normal` pose.

```bash
cd case/plume/iss_panel_thesis

# Evaluate at the committed mesh for the baseline pose
python cai2016_reference.py --distance 4.0

# Diff against a distribution a study already exported
python cai2016_reference.py --distance 4.0 --compare \
results/studies/baseline_simplified/cases/case000_modelSimplified_L4_u0_v0/distributions/case000_modelSimplified_L4_u0_v0_panel_firing001.csv
```

On the committed mesh at L = 4 m the peak values agree to **2.8 % in
pressure, 3.4 % in shear and 7.8 % in heat flux**. That residual is the
Maxwellian wall chain (plume field -> `LocalFieldState` -> Shen formulas)
against a direct integration of the wall fluxes — the same documented gap
`tests/mdao/mdao_integration_test_02.py` already checks for integrated
loads. It is not a defect in either path.

## See also

- `docs/plume_validation_study.md` — the study framework, the YAML schema,
the axis modes, the Knudsen block and the result schema.
- `case/plume/plume_flat_plate_sweep/` — the Cai 2016 verification case whose
thruster conditions this case reuses, and the `aim_at_reference` studies.
Loading
Loading