feat(drl): publish writing-ready documentation and training tables
Add indexed DRL pinball documentation, provenance-aware training CSV export, first-phase flow rendering, and audited reproduction assets so the retained results are ready for manuscript analysis. Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -0,0 +1,14 @@
|
|||||||
|
# DRL pinball asset status
|
||||||
|
|
||||||
|
| Layer | Paths | Authority/use |
|
||||||
|
|---|---|---|
|
||||||
|
| Active V5 code | `case_registry.py`, `train/`, `eval/` | Executable contract; changes require focused tests. |
|
||||||
|
| Active V5 targets/calibration | `train/calibrations/`, `configs/` | Matched target/config provenance; do not substitute files by name alone. |
|
||||||
|
| Retained training bundles | `train/output/*_seed*/` | Compact policy/normalizer and metadata; logs/TensorBoard may be local/ignored. |
|
||||||
|
| Reader training tables | `train/results/figures/`, `train/results/latest/`, `train/results/training_csv/` | Derived views; not new evidence. |
|
||||||
|
| Reproduction roles | `data/reproduction/` | Metadata/manifests and NPZ/CSV role artifacts; may resolve through nonportable symlink. |
|
||||||
|
| Legacy compatibility | `legacy_test/`, `legacy_env/`, `legacy_train/` | Separate Legacy plant and historical contracts. |
|
||||||
|
| Historical analysis | `train/CROSSRE_ANALYSIS.md`, `train/VARDIST_ANALYSIS.md`, `eval/archive/` | Context only; archived claims do not outrank current contracts. |
|
||||||
|
| External storage | resolved symlinks and `/home/frank14f/optane/...` | Machine-local authority; not portable Git input. |
|
||||||
|
|
||||||
|
Never move or overwrite hash-bound evidence to improve readability; add an index or a new derived sibling.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# DRL pinball claim ledger
|
||||||
|
|
||||||
|
This ledger governs writing language. Repository artifacts and manifests remain the evidence; this file records status and limits.
|
||||||
|
|
||||||
|
## Status vocabulary
|
||||||
|
|
||||||
|
- **Supported/bounded:** directly supported at named cases, windows, metrics, and artifact paths.
|
||||||
|
- **Retained historical:** reproducible reader evidence, but not current-contract retraining authority.
|
||||||
|
- **Diagnostic:** useful for understanding behavior, not a causal result.
|
||||||
|
- **Contextual:** calibration or background evidence.
|
||||||
|
- **Negative:** evidence against a proposed interpretation.
|
||||||
|
- **Unsupported/withdrawn:** do not state without new evidence.
|
||||||
|
|
||||||
|
## Current bounded claims
|
||||||
|
|
||||||
|
| ID | Status | Safe statement | Evidence | Limits |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| DRL-C1 | Retained historical | The 15-run V5 scratch matrix contains five Re100 seeds and one seed for each other canonical case. | `train/case_registry.py`, `train/output/*/meta.json` | Only Re100 supports seed spread; others are n=1 demonstrations. |
|
||||||
|
| DRL-C2 | Retained historical | Re100 repeatedly discovered high-reward checkpoints, while late-policy retention varied by seed. | `train/results/figures/scratch_training_summary.csv`, `train/results/latest/figures/fig01*`, `fig02*` | Not robust convergence, asymptotic stability, or uncertainty. |
|
||||||
|
| DRL-C3 | Retained historical | Retained cross-Re and variable-disturbance evaluations are case-specific demonstrations. | `train/results/latest/tables/latest_eval_summary.csv`, `HOW_TO_READ.md` | Historical compatibility path; variable-disturbance learning rates are confounded. |
|
||||||
|
| DRL-C4 | Retained historical | Retained Illusion artifacts show case-dependent reward/force behavior and high action saturation in several cases. | `latest_eval_summary.csv`, `fig09*` | DTW similarity is not field error, delay, causality, or mechanism. |
|
||||||
|
| DRL-C5 | Supported artifact contract | V5 training uses calibrated targets, native calibration-v2 scaling, online observation `VecNormalize`, zero action bias, and explicit action smoothing. | `train/TRAIN_PIPELINE.md`, `train/env_karman.py`, `train/env_illusion.py` | This describes the active code contract, not proof that every retained policy was trained under it. |
|
||||||
|
| DRL-C6 | Reader artifact | `train/export_training_csv.py` exports 7,000 per-iteration rows from 14 conflict-free runs from retained TensorBoard evaluation scalars. | `train/results/training_csv/manifest.json` | Re60 is excluded fail-closed; TensorBoard is auxiliary; `meta.json` remains best-value authority. |
|
||||||
|
|
||||||
|
## Explicit non-claims
|
||||||
|
|
||||||
|
Do not claim universal Reynolds-number generalization, distribution-wide robustness, causal wake mechanism, asymptotic stability, independent uncertainty for n=1 cases, post-fix complete retraining, or physical response delay from DTW. Do not pool V5 with Legacy, SR, CCD, or steady-theory numbers without a declared contract and question.
|
||||||
|
|
||||||
|
## Reproduction-field boundary
|
||||||
|
|
||||||
|
Eight periodic phase fields are retained per role, but current flow plotting selects only phase slot 0. Vortex event fields and steady late fields are separate semantics. A plot is a derived reader view and does not create new CFD evidence.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# DRL pinball data dictionary
|
||||||
|
|
||||||
|
## Units and conventions
|
||||||
|
|
||||||
|
- `L0=20` lattice units; V5 fields are on a `2000×600` lattice, Legacy fields on `1280×512`.
|
||||||
|
- `U0=0.01` is the lattice inlet velocity in the active V5 contract.
|
||||||
|
- Code Reynolds number uses reference length `2L0=40`; therefore code `re100` corresponds to single-cylinder `Re_D=50`.
|
||||||
|
- Action order is `[front, top, bottom]` in V5 retained evaluation. Do not infer Legacy order without its role metadata.
|
||||||
|
- A “training iteration” is one PPO learn chunk plus deterministic evaluation, normally 2048 learn steps followed by up to 360 evaluation steps. It is not a terminating Gym episode.
|
||||||
|
|
||||||
|
## Training artifacts
|
||||||
|
|
||||||
|
`train/output/<case>_seed<seed>/` contains compact retained bundles and, when available locally, `tb/`, `train.log`, and model checkpoints. `meta.json` is the authority for `best_reward`, `best_episode`, `total_episodes`, seed, case, and learning rate. `best_model.zip` must be paired with `best_vecnormalize.pkl`; the normalizer is part of the policy artifact.
|
||||||
|
|
||||||
|
`train/results/figures/scratch_training_summary.csv` is one row per retained run. It is a summary table, not a training trajectory.
|
||||||
|
|
||||||
|
`train/results/training_csv/training_iterations.csv` is the plotting table. Each row is one TensorBoard evaluation step from a conflict-free run and includes `eval_reward`, `eval_r_cd`, `eval_r_cl`, `eval_r_sim`, running-best reward, exploratory drawdown, and a flag for the `meta.json` authoritative best iteration. Its source is explicitly `TensorBoard eval/* scalars`; values at the best point are checked against metadata during generation. `current_contract_retraining_evidence=false` is intentional for retained historical runs. Re60 is excluded because resumed TensorBoard branches conflict and include mixed configuration provenance.
|
||||||
|
|
||||||
|
## Reproduction role artifacts
|
||||||
|
|
||||||
|
The reproduction tree is organized as `v5/<case-or-seeded-run>/<role>/` and `legacy/<scenario>/<role>/`. Common roles are `target`, `controlled`, `zero`; Legacy additionally contains `sr`, `constant`, and special variants.
|
||||||
|
|
||||||
|
- `phase_fields.npz`: eight retained periodic fields, arrays `ux`, `uy` with shape `(8, ny, nx)`, phase selectors, and normally complete-cycle `mean_ux`, `mean_uy`.
|
||||||
|
- `event_fields.npz`: five event-relative fields, selected by `relative_offsets`; never label these as phase slots.
|
||||||
|
- `late_field.npz`: one late snapshot for steady roles and special targets.
|
||||||
|
- `timeseries.csv`: retained sensor/force/action or role time series according to the role contract.
|
||||||
|
- `metadata.json`: schema, role, case, units, clocks, source hashes, and acquisition provenance.
|
||||||
|
|
||||||
|
For phase plots, the current publication instruction is slot `0` only. The eight slots remain in the source artifact for later analysis; plotting all slots is not the default.
|
||||||
|
|
||||||
|
## Metrics and evidence status
|
||||||
|
|
||||||
|
Reward components (`r_cd`, `r_cl`, `r_sim`) are training/evaluation objective components, not field error. DTW is signal similarity, not causal delay or a physical mechanism. `E_phase8` and `E_mean` in evaluation artifacts are separate field comparisons with their declared window and mask. Never compare V5 and Legacy scores without reconciling solver, units, normalization, target, and metric formulas.
|
||||||
|
|
||||||
|
Every numerical claim in writing should identify case, seed/realization count, window, comparator, metric formula, artifact path, and whether it is current, retained historical, diagnostic, or negative evidence.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# DRL pinball history and lessons
|
||||||
|
|
||||||
|
## Why this package has two plants
|
||||||
|
|
||||||
|
The project contains modern V5 CelerisLab PPO workflows and historical LegacyCelerisLab workflows. They share fluidic-pinball concepts but not automatically the same sensor units, normalization, action persistence, solver contract, target construction, or reward formula. Pooling them by filename or plotting style is unsafe.
|
||||||
|
|
||||||
|
## Chronology
|
||||||
|
|
||||||
|
1. **Legacy PPO era:** Kármán cloak, Illusion, vortex, erase, reduced-observation, and steady scripts established the original scene families and compatibility artifacts.
|
||||||
|
2. **Modern V5 migration:** CelerisLab moved to a `2000×600` grid, calibration-v2/native units, explicit case registry, zero-bias actions, `VecNormalize`, and scratch PPO training.
|
||||||
|
3. **Canonical scratch matrix:** 15 retained scratch runs across 11 cases were retained for writing. Re100 has five seeds; other cases have one.
|
||||||
|
4. **Evidence consolidation:** evaluation and plotting were separated from training. Retained results are historical writing inputs, not proof of post-fix retraining.
|
||||||
|
5. **Reproduction acquisition:** V5/Legacy role artifacts, eight periodic phase fields, complete-cycle means, CSV time series, event fields, and late snapshots were standardized with provenance and fail-closed checks.
|
||||||
|
6. **Current writing preparation:** reader documentation, data dictionaries, and per-iteration CSV export make the package indexable without reopening CFD.
|
||||||
|
|
||||||
|
## Lessons that control interpretation
|
||||||
|
|
||||||
|
- Best discovery is not stable retention. Re100 seeds repeatedly reach high best rewards but can fall sharply by the final or late window.
|
||||||
|
- An “episode” in training logs is a PPO chunk plus evaluation, not a Gym episode.
|
||||||
|
- `meta.json` is authoritative for best training values; TensorBoard supplies reconstructable per-iteration curves and must be validated at the metadata best point.
|
||||||
|
- A policy without its same-selection normalizer is incomplete for inference.
|
||||||
|
- Calibration artifacts are part of the target contract; a target array alone is not enough.
|
||||||
|
- `VecNormalize` is online observation whitening, distinct from physical calibration scales.
|
||||||
|
- Symmetry is default-off in active V5. Historical configured probabilities must not be described as effective augmentation without checking wrapper timing.
|
||||||
|
- `kar_d075` and `kar_d2` learning-rate differences confound a pure disturbance-geometry ablation.
|
||||||
|
- DTW similarity is not physical response time, causality, or mechanism evidence.
|
||||||
|
- V5 and Legacy are separate evidence chains. SR and CCD results answer different questions and are not interchangeable DRL scores.
|
||||||
|
- The first stored periodic phase is a plotting selection, not a claim that phase 0 is scientifically privileged.
|
||||||
|
|
||||||
|
## Retired or bounded routes
|
||||||
|
|
||||||
|
Transfer runs, old `_sc` naming, schema-less normalization, and archived evaluation compatibility paths remain useful for historical provenance but are not active V5 retraining authority. Legacy SR Illusion is historical/negative evidence; do not turn its runtime capability into target-tracking claims.
|
||||||
|
|
||||||
|
## Durable engineering rules
|
||||||
|
|
||||||
|
Use one canonical entrypoint per contract, discover identities from metadata, preserve source hashes, stage derived outputs transactionally, never overwrite evidence, and put scope/metric/window/comparator beside every number. Documentation is a reader layer; immutable artifacts remain the evidence layer.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# DRL pinball
|
||||||
|
|
||||||
|
## Five-minute navigation
|
||||||
|
|
||||||
|
This package contains the modern V5 CelerisLab PPO workflow, retained V5 training/evaluation evidence, the historical Legacy compatibility/acquisition path, and reader-facing plots/data. It is a research reproduction and writing package; it is not a claim that every retained policy was retrained after the latest source-contract changes.
|
||||||
|
|
||||||
|
Read in this order:
|
||||||
|
|
||||||
|
1. `README.md` — scope, authority, and navigation.
|
||||||
|
2. `CLAIMS.md` — claim-status ledger and prohibited interpretations.
|
||||||
|
3. `RESULTS.md` — conclusion-to-result-family summary.
|
||||||
|
4. `WRITING_HANDOFF.md` — manuscript-ready wording, evidence boundaries, and known caveats.
|
||||||
|
5. `DATA_DICTIONARY.md` — directory, artifact, CSV, units, and metric definitions.
|
||||||
|
6. `ASSET_STATUS.md` — active, retained, derived, legacy, archive, and external layers.
|
||||||
|
7. `HISTORY_AND_LESSONS.md` — chronology, retired routes, and lessons that prevent misreading.
|
||||||
|
8. `knowledge.md` — legacy historical notes; non-authoritative for active V5 and to be read only after current contracts.
|
||||||
|
6. `train/README.md` → `train/TRAIN_PIPELINE.md` → `train/DESIGN_DECISIONS.md` — active V5 execution contracts.
|
||||||
|
7. `eval/README.md` — retained-policy evaluation and compatibility boundary.
|
||||||
|
8. `plot_reproduction_summary.py` and `plot_flow_fields.py` — CPU/offline plotting entrypoints.
|
||||||
|
9. `train/results/latest/HOW_TO_READ.md` — current retained-results figure and table index.
|
||||||
|
|
||||||
|
## Scientific scope
|
||||||
|
|
||||||
|
The active DRL scope is modern V5 Kármán cloaking and hydrodynamic illusion. Legacy Kármán, Illusion, steady, vortex, erase, and SR roles are preserved for historical comparison, acquisition compatibility, or contextual calibration; they must not be silently pooled with V5.
|
||||||
|
|
||||||
|
The retained V5 scratch matrix contains 15 retained runs across 11 canonical cases; the clean per-iteration CSV contains 14 runs because Re60 is quarantined: five seeds for `kar_re100`, and one seed for each other active case. Only Re100 supports a seed-level spread statement. All other cases are demonstrations (`n=1`).
|
||||||
|
|
||||||
|
The most defensible retained-training wording is: **Re100 repeatedly discovers high-reward policies, but late-policy retention is seed-dependent**. Do not call this universal robustness, post-fix retraining, or stable convergence. Variable-disturbance comparisons are confounded because `kar_d075` and `kar_d2` use learning rate `1e-4`, while most runs use `3e-4`.
|
||||||
|
|
||||||
|
## Evidence authority
|
||||||
|
|
||||||
|
- Training best reward and best iteration: each run's `meta.json`.
|
||||||
|
- Same-iteration reward components: `train.log`, when present.
|
||||||
|
- Per-iteration curves: retained TensorBoard `tb/`, exported to `train/results/training_csv/training_iterations.csv`; this is an auxiliary time series, not a replacement for `meta.json`.
|
||||||
|
- Retained deterministic evaluation: `eval/output/train/*/metrics.json` and `all_seeds.json`, evaluated through the explicit `legacy-policy-v1` compatibility path.
|
||||||
|
- Figures/tables: derived reader views; they do not create new CFD evidence.
|
||||||
|
- Repository data symlinks may resolve to external storage and are intentionally nonportable; manifests and metadata remain the provenance authority.
|
||||||
|
|
||||||
|
## Reproduce reader outputs without CFD
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH="$PWD/src" PYTHONNOUSERSITE=1 conda run -n pycuda_3_10 \
|
||||||
|
python src/drl_pinball/train/export_training_csv.py
|
||||||
|
PYTHONPATH="$PWD/src" PYTHONNOUSERSITE=1 conda run -n pycuda_3_10 \
|
||||||
|
python src/drl_pinball/plot_reproduction_summary.py --campaign ppo
|
||||||
|
```
|
||||||
|
|
||||||
|
The first command regenerates only `training_iterations.csv` and its manifest from retained training artifacts; it does not touch figures or evaluation tables. The second renders compact role summaries. Neither command runs CFD. For retained flow-field plots, `plot_flow_fields.py --phase-index 0` deliberately renders only the first of the eight stored periodic phase slots; event and late-field semantics remain separate.
|
||||||
|
|
||||||
|
## Environment and safety
|
||||||
|
|
||||||
|
Use `pycuda_3_10` with `PYTHONNOUSERSITE=1` on the GPU training/acquisition path. Do not run CFD merely to read or document retained artifacts. Never overwrite evidence directories; use a fresh output path for new derived packages. Keep V5 CelerisLab and LegacyCelerisLab evidence as separate plants and normalization contracts.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# DRL pinball retained results
|
||||||
|
|
||||||
|
## Bottom line
|
||||||
|
|
||||||
|
The retained V5 evidence supports a bounded training observation: Re100 scratch runs repeatedly found high-reward checkpoints, but late-policy retention was seed-dependent. Cross-Re, disturbance-size, and Illusion points are named-condition demonstrations, not a continuous law or robustness study.
|
||||||
|
|
||||||
|
## Result families
|
||||||
|
|
||||||
|
1. **Training discovery/retention:** use `train/results/figures/scratch_training_summary.csv` and `train/results/latest/figures/fig01*`–`fig05*`. Best values resolve to `meta.json`; late retention comes from the retained TensorBoard stream.
|
||||||
|
2. **Retained deterministic evaluation:** use `train/results/latest/tables/latest_eval_summary.csv` and `HOW_TO_READ.md`. This is a separate historical-policy compatibility stream, not the training-best measurement.
|
||||||
|
3. **Per-iteration plotting data:** use `train/results/training_csv/training_iterations.csv`; read its manifest before analysis. Re60 is excluded because its resumed TensorBoard branches conflict and include mixed-config history.
|
||||||
|
4. **Flow-role reproduction:** use `data/reproduction/*/metadata.json`, role manifests, `plot_flow_fields.py`, and the wake-L2 summaries. Phase 0 is the current default plot selection; event/late roles are not phase roles.
|
||||||
|
|
||||||
|
## Safe interpretation
|
||||||
|
|
||||||
|
Reward and DTW are objective/signal metrics. They are not interchangeable with field error, causal effect, mechanism, response time, or stability. Only Re100 has n=5. `kar_d075` and `kar_d2` use `1e-4` learning rate while most runs use `3e-4`, so the disturbance sweep is not a pure one-factor ablation.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# DRL pinball writing handoff
|
||||||
|
|
||||||
|
## Safe manuscript positioning
|
||||||
|
|
||||||
|
The retained modern V5 package documents a calibrated PPO workflow and a bounded scratch-training matrix. It supports descriptive statements about policy discovery, case-dependent reward components, retained evaluation artifacts, action saturation, and field/phase comparisons at named conditions. It does not by itself support universal Reynolds-number generalization, uncertainty bounds outside Re100, post-fix retraining claims, asymptotic stability, causal mechanism, or physical delay from DTW.
|
||||||
|
|
||||||
|
Recommended sentence: “Across five Re100 scratch seeds, high-reward checkpoints were repeatedly discovered, while late-policy retention varied substantially between seeds.”
|
||||||
|
|
||||||
|
Avoid: “The controller converged robustly,” “the method generalizes across Reynolds number,” or “DTW demonstrates a delayed causal response.”
|
||||||
|
|
||||||
|
## Evidence chains
|
||||||
|
|
||||||
|
Training: `train/output/*/meta.json` → same-iteration `train.log` components → TensorBoard curves exported to `train/results/latest/tables/training_iterations.csv`.
|
||||||
|
|
||||||
|
Retained evaluation: `eval/output/train/*/metrics.json` + `all_seeds.json` → explicit legacy-policy-v1 compatibility → figures/tables under `train/results/latest`.
|
||||||
|
|
||||||
|
Flow roles: `data/reproduction` metadata/manifests → phase/event/late NPZ and CSV → `plot_flow_fields.py` or `plot_reproduction_summary.py`. V5 and Legacy remain separate.
|
||||||
|
|
||||||
|
## Writing checklist
|
||||||
|
|
||||||
|
For every result, record: case ID, scene, seed/role, n, target/reference, metric formula, time window, units/normalization, artifact path, and evidence status. Separate training best, late retention, deterministic retained evaluation, and field error. State when a value is a reader-derived product.
|
||||||
|
|
||||||
|
## Agent restart
|
||||||
|
|
||||||
|
Read this file, then `README.md`, `DATA_DICTIONARY.md`, `HISTORY_AND_LESSONS.md`, and the relevant `train/` or `eval/` document. Search `case_registry.py` before adding cases. Recall the durable DRL memory before changing claims or contracts. Do not start CFD for documentation work.
|
||||||
@@ -1,33 +1,30 @@
|
|||||||
# SR reproduction plots
|
# SR reproduction plots
|
||||||
|
|
||||||
This directory is a derived diagnostic plotting package for standardized Legacy acquisition. It is not the acquisition payload and it is not, by itself, scientific evidence that every case or role completed.
|
This directory is a derived diagnostic plotting package for the standardized Legacy acquisition. It is not the acquisition payload and it is not, by itself, evidence that every campaign role completed.
|
||||||
|
|
||||||
## Current status: partial diagnostic
|
## Current status: partial Kármán package
|
||||||
|
|
||||||
`manifest.json` currently declares `allow_partial: true`, `plot_count: 2`, and `expected_plot_count: 23`. The two rendered groups are Legacy `karman_re100` and `illusion_1L`. Therefore this directory is a **2/23 partial diagnostic**, not a complete SR reproduction package and not sufficient evidence for campaign completeness, generalization, deletion necessity, or a current Illusion result. Its absolute source paths and recorded SHA-256 values identify the inputs used for those two plots; they do not imply that the other 21 expected plots exist or passed acquisition validation.
|
`manifest.json` currently records 11 rendered plots out of the 23-role SR matrix (`allow_partial: true`). The package contains the available Kármán target/SR/zero comparisons and registered SR-term deletion diagnostics. The invalid Illusion SR role directories were removed from the reproduction store after the Illusion SR route was closed; Illusion is historical/negative evidence and is not regenerated here.
|
||||||
|
|
||||||
The current standardized reproduction claim is active for Kármán SR. The included Illusion panel is historical/negative diagnostic evidence only: it can expose behavior or incompatibility, but it must not be cited as a current validated Illusion SR reproduction. A visually plausible panel cannot override missing campaign roles, partial-manifest status, or acquisition/provenance gates.
|
The missing entries are expected to remain absent until their acquisition inputs exist. This package must not be relabelled complete by changing the manifest alone. Absolute source paths and SHA-256 values identify the inputs used for each rendered plot.
|
||||||
|
|
||||||
## Acquisition contract represented by a complete package
|
## Plot contract
|
||||||
|
|
||||||
For standard periodic Kármán and Illusion acquisition, `legacy_test/acquire.py` distinguishes:
|
Training cases contain `Target`, `PPO`, `SR`, and physical `Zero`; generalization cases intentionally contain `Target`, `SR`, and physical `Zero` only. Variant plots contain `Target`, the same-case parent SR, the named variant SR, and physical `Zero`.
|
||||||
|
|
||||||
- `Target`: live target-only trajectory, with no policy action, policy normalization, or native reward metric.
|
Each periodic role uses the validated phase-field artifact and plots the first phase-aligned field snapshot together with the six-channel sensor phase portrait. Every vorticity panel uses the fixed symmetric range `[-0.001, +0.001]`.
|
||||||
- `PPO`: `controlled`, the frozen-policy Legacy trajectory.
|
|
||||||
- `Zero (physical, no control)`: normalized counter-bias that yields zero physical cylinder rotation; it is not normalized-action zero when a bias exists.
|
|
||||||
- `SR`: the canonical causal symbolic policy.
|
|
||||||
- named SR deletion diagnostics: one registered term removed and bound to the same-case canonical SR parent.
|
|
||||||
|
|
||||||
Each standard role has 480 warm-up intervals followed by exactly 160 retained post-step boundaries. Role-local phase comes from smoothed rising zero crossings of center `uy` (`sensors[:,3]`) with a minimum-gap filter. The field artifact contains exactly eight nearest boundary snapshots at the eight requested phases; these are unaveraged snapshots selected across complete cycles. Its `mean_ux` and `mean_uy` are arithmetic means over all retained boundary fields in complete half-open cycles, not means of those eight snapshots.
|
The renderer records metadata, telemetry, and field-file hashes in the manifest. It stages the complete derived package and replaces the output atomically; `--allow-partial` is diagnostic mode and must remain explicitly labelled as partial.
|
||||||
|
|
||||||
Telemetry keeps the native Legacy reward DTW where available and adds a distinct target-channel-max-abs-normalized rolling six-channel cycle DTW. These must remain separately labelled; target trajectories do not have native reward DTW.
|
## Regeneration
|
||||||
|
|
||||||
Acquisition publishes only after same-filesystem staging and exact schema validation. Outputs are no-clobber unless overwrite is explicit, prior output is restored if atomic replacement fails, and failed runs remove staging rather than publishing partial evidence.
|
From the repository root:
|
||||||
|
|
||||||
Generalization cases intentionally contain `Target`, `SR`, and physical `Zero` only; absence of PPO is part of the acquisition design, not missing data. Canonical SR and deletion variants share `SR_analysis.stage_3_validate.SymbolicPolicy`, while metadata binds formula-file hashes, artifact IDs, the pair hash, and deployment-expression hashes.
|
```bash
|
||||||
|
PYTHONPATH=src python3 -m drl_pinball.plot_reproduction_summary \
|
||||||
|
--campaign sr --allow-partial \
|
||||||
|
--data-root src/drl_pinball/data/reproduction \
|
||||||
|
--output-root src/drl_pinball/data/reproduction_plots_sr
|
||||||
|
```
|
||||||
|
|
||||||
## Operational warning
|
The Legacy compiler writes shared PTX/configuration files during initialization. Do not initialize CFD cases concurrently; stagger GPU launches until the previous compilation has released those files.
|
||||||
|
|
||||||
Legacy compiler configuration and PTX output are shared files (`LegacyCelerisLab/kernels/macros.h` and `kernel.ptx`). CFD configurations must not compile concurrently. Run them serially and stagger shared-file configuration changes so the previous GPU context and compilation have fully released the files before the next case starts.
|
|
||||||
|
|
||||||
Regenerating this directory should use the plotting command's complete-campaign mode for evidence publication. `--allow-partial` is appropriate only for diagnostics and must continue to be labelled partial.
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 534 KiB |
|
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 726 KiB |
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 822 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 669 KiB |
|
After Width: | Height: | Size: 910 KiB |
|
After Width: | Height: | Size: 579 KiB |
|
After Width: | Height: | Size: 488 KiB |
|
After Width: | Height: | Size: 727 KiB |
|
After Width: | Height: | Size: 715 KiB |
|
After Width: | Height: | Size: 792 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from drl_pinball import plot_reproduction_summary as plots
|
||||||
|
|
||||||
|
|
||||||
|
def test_sr_plot_matrix_roles_and_no_ppo(tmp_path):
|
||||||
|
specs=plots.sr_plot_specs(tmp_path)
|
||||||
|
assert len(specs)==23
|
||||||
|
training=[roles for group,roles,name in specs if group in plots.SR_TRAINING and not name.startswith("variants/")]
|
||||||
|
generalization=[roles for group,roles,name in specs if group in plots.SR_GENERALIZATION]
|
||||||
|
variants=[roles for _,roles,name in specs if name.startswith("variants/")]
|
||||||
|
assert len(training)==7 and all([label for label,_ in roles]==["Target","PPO","SR","Zero (physical, no control)"] for roles in training)
|
||||||
|
assert len(generalization)==9 and all([label for label,_ in roles]==["Target","SR","Zero (physical, no control)"] for roles in generalization)
|
||||||
|
assert len(variants)==7 and all([label for label,_ in roles][:2]==["Target","Parent SR"] for roles in variants)
|
||||||
|
assert all("PPO" not in [label for label,_ in roles] for roles in generalization)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sr_partial_renderer_and_source_hash(tmp_path,monkeypatch):
|
||||||
|
data=tmp_path/"data"; stage=tmp_path/"stage"
|
||||||
|
first=plots.sr_plot_specs(data)[0]
|
||||||
|
for _,path in first[1]: path.mkdir(parents=True)
|
||||||
|
calls=[]
|
||||||
|
monkeypatch.setattr(plots,"plot_role_set",lambda stage,data_root,group,roles,output_name:calls.append((group,roles,output_name)) or {"group":group})
|
||||||
|
entries=plots.render_sr_package(stage,data,allow_partial=True)
|
||||||
|
assert entries==[{"group":first[0]}] and len(calls)==1
|
||||||
|
source=tmp_path/"source.bin"; source.write_bytes(b"canonical")
|
||||||
|
assert plots.sha256_file(source)=="0deeb8fa1dbbee4c0dbe7f5e3c9183940139f26d22797ee8ab07c00557a4c2ff"
|
||||||
|
|
||||||
|
|
||||||
|
def test_npz_renderer_hashes_every_source(tmp_path):
|
||||||
|
data=tmp_path/"data"; stage=tmp_path/"stage"; case="karman_re25"
|
||||||
|
roles=[]
|
||||||
|
for label,role in (("Target","target"),("SR","sr"),("Zero (physical, no control)","zero")):
|
||||||
|
rd=data/"legacy"/case/role; rd.mkdir(parents=True)
|
||||||
|
(rd/"metadata.json").write_text(json.dumps({"units":{"sensors":"lattice"}}))
|
||||||
|
values=np.linspace(-1,1,10)
|
||||||
|
columns=["physical_time"]+[f"sensors_{index}" for index in range(6)]
|
||||||
|
table=np.column_stack((np.arange(10),*[values*(index+1) for index in range(6)]))
|
||||||
|
np.savetxt(rd/"timeseries.csv",table,delimiter=",",header=",".join(columns),comments="")
|
||||||
|
field=np.zeros((8,4,5),np.float32); field[:,1,2]=1e-3
|
||||||
|
np.savez(rd/"phase_fields.npz",ux=field,uy=field,target_phase=np.arange(8)*np.pi/4)
|
||||||
|
roles.append((label,rd))
|
||||||
|
entry=plots.plot_role_set(stage,data,case,tuple(roles),case)
|
||||||
|
assert (stage/entry["plot"]).is_file()
|
||||||
|
assert entry["sensor_limits"]["u"]==entry["sensor_limits"]["u"]
|
||||||
|
assert all(len(source["sha256"])==64 for role in entry["roles"] for source in role["source_files"])
|
||||||
|
assert all(len(role["source_files"])==3 for role in entry["roles"])
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render every retained reproduction flow-field artifact."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, hashlib, json, os, shutil, tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.colors import BoundaryNorm, ListedColormap
|
||||||
|
from matplotlib.patches import Circle, FancyArrowPatch, Rectangle
|
||||||
|
from matplotlib.colorbar import ColorbarBase
|
||||||
|
from matplotlib.ticker import FuncFormatter
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
L0=20.0; U0=.01; PINBALL_RADIUS=10.0
|
||||||
|
VORT_CMAP="RdBu_r"; ERROR_CMAP="Greys"; STREAM_DENSITY=2.4; ROI_STREAM_DENSITY=1.2
|
||||||
|
V5_KARMAN_RADII={"kar_re60":1.0,"kar_re100":1.0,"kar_re200":1.0,"kar_re400":1.0,"kar_d075":.75,"kar_d15":1.5,"kar_d2":2.0}
|
||||||
|
ILLUSION_RADII={"ill_075L":.75,"ill_1L":1.0,"ill_15L":1.5,"ill_2L":2.0,"illusion_05L":.5,"illusion_06L":.6,"illusion_075L":.75,"illusion_08L":.8,"illusion_1L":1.0,"illusion_12L":1.2,"illusion_15L":1.5,"illusion_2L":2.0}
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Role:
|
||||||
|
label:str; path:Path; generation:str; case_id:str; scene:str; scenario:str; role_id:str
|
||||||
|
|
||||||
|
def sha256(path):
|
||||||
|
h=hashlib.sha256()
|
||||||
|
with path.open("rb") as f:
|
||||||
|
for b in iter(lambda:f.read(1<<20),b""): h.update(b)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
def meta(path):
|
||||||
|
q=path/"metadata.json"
|
||||||
|
return json.loads(q.read_text()) if q.is_file() else {}
|
||||||
|
|
||||||
|
def artifact(path):
|
||||||
|
for name in ("phase_fields.npz","event_fields.npz","late_field.npz"):
|
||||||
|
if (path/name).is_file(): return path/name
|
||||||
|
return None
|
||||||
|
|
||||||
|
def discover(root, generation=None, case=None):
|
||||||
|
result=[]
|
||||||
|
for p in sorted(root.glob("**/metadata.json")):
|
||||||
|
d=p.parent; f=artifact(d)
|
||||||
|
if f is None: continue
|
||||||
|
md=meta(d); gen=d.relative_to(root).parts[0]
|
||||||
|
if generation and gen!=generation: continue
|
||||||
|
case_id=str(md.get("case_id") or d.relative_to(root).parts[1])
|
||||||
|
if case and case_id!=case: continue
|
||||||
|
scene=str(md.get("scene") or ("illusion" if case_id.startswith("ill_") else "karman"))
|
||||||
|
scenario=d.relative_to(root).parts[1]
|
||||||
|
role=str(md.get("role") or d.name)
|
||||||
|
result.append(Role(role,d,gen,case_id,scene,scenario,d.name))
|
||||||
|
if not result: raise ValueError(f"no supported field artifacts under {root}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def load_artifact(role, cache=None, phase_index=0):
|
||||||
|
path=artifact(role.path)
|
||||||
|
source_info=cache.get(path) if cache is not None else None
|
||||||
|
if source_info is None:
|
||||||
|
source_info={"source_sha256":sha256(path)}
|
||||||
|
with np.load(path,allow_pickle=False) as z:
|
||||||
|
keys=set(z.files); ux=np.asarray(z["ux"]); uy=np.asarray(z["uy"])
|
||||||
|
if "phase_fields.npz" in path.name:
|
||||||
|
required={"ux","uy","field_indices","cycle_id","target_phase","actual_phase","phase_error"}
|
||||||
|
if not required<=keys or ux.ndim!=3 or uy.shape!=ux.shape or ux.shape[0]!=8: raise ValueError(f"{path}: invalid phase schema")
|
||||||
|
if not np.isfinite(ux).all() or not np.isfinite(uy).all(): raise ValueError(f"{path}: nonfinite field")
|
||||||
|
offsets=np.asarray(z["actual_phase"])-np.asarray(z["target_phase"])
|
||||||
|
if not 0 <= phase_index < 8: raise ValueError(f"phase index must be in [0, 7], got {phase_index}")
|
||||||
|
i = int(phase_index)
|
||||||
|
result=(path,ux[i:i+1],uy[i:i+1],[{"mode":"phase","index":i,"offset":float(offsets[i])}],bool({"mean_ux","mean_uy"}<=keys),source_info,sorted(keys))
|
||||||
|
if cache is not None: cache[path]={"source_sha256":source_info["source_sha256"],"schema":sorted(keys)}
|
||||||
|
return result
|
||||||
|
if "event_fields.npz" in path.name:
|
||||||
|
required={"ux","uy","field_indices","relative_offsets"}
|
||||||
|
if not required<=keys or ux.ndim!=3 or uy.shape!=ux.shape: raise ValueError(f"{path}: invalid event schema")
|
||||||
|
offsets=np.asarray(z["relative_offsets"])
|
||||||
|
result=(path,ux,uy,[{"mode":"event","index":int(i),"offset":float(offsets[i])} for i in range(len(ux))],False,source_info,sorted(keys))
|
||||||
|
if cache is not None: cache[path]={"source_sha256":source_info["source_sha256"],"schema":sorted(keys)}
|
||||||
|
return result
|
||||||
|
if ux.ndim!=3 or uy.shape!=ux.shape or ux.shape[0]!=1: raise ValueError(f"{path}: invalid late schema")
|
||||||
|
result=(path,ux,uy,[{"mode":"late","index":0,"offset":None}],False,source_info,sorted(keys))
|
||||||
|
if cache is not None: cache[path]={"source_sha256":source_info["source_sha256"],"schema":sorted(keys)}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def geometry(role,shape):
|
||||||
|
ny,nx=shape; cy=(ny-1)/2
|
||||||
|
scene=role.scene; target=role.label=="target"
|
||||||
|
sensors_x=600 if scene=="illusion" and role.generation=="v5" else (1200 if scene=="karman" and role.generation=="v5" else 600 if scene=="illusion" else 800)
|
||||||
|
sensors=[(sensors_x,cy+y) for y in (40,0,-40)]
|
||||||
|
if scene=="karman":
|
||||||
|
if role.generation=="v5":
|
||||||
|
if role.case_id not in V5_KARMAN_RADII:
|
||||||
|
raise ValueError(f"no canonical V5 Karman radius for {role.case_id}")
|
||||||
|
radius=V5_KARMAN_RADII[role.case_id]*L0
|
||||||
|
bodies=[(600,cy,radius)] if target else [(600,cy,radius),(1000,cy,10),(1026,cy+15,10),(1026,cy-15,10)]
|
||||||
|
else: bodies=[(200,cy,20)] if target else [(200,cy,20),(600,cy,10),(626,cy+15,10),(626,cy-15,10)]
|
||||||
|
elif scene=="illusion":
|
||||||
|
if role.case_id not in ILLUSION_RADII:
|
||||||
|
raise ValueError(f"no canonical illusion radius for {role.case_id}")
|
||||||
|
r=ILLUSION_RADII[role.case_id]*L0 if target else 10.0
|
||||||
|
bodies=[(400 if role.generation=="v5" else 400,cy,r)] if target else [(380 if role.generation!="v5" else 380,cy,10),(406 if role.generation!="v5" else 406,cy+15,10),(406,cy-15,10)]
|
||||||
|
elif scene=="vortex": bodies=[] if target else [(600,cy,10),(626,cy+15,10),(626,cy-15,10)]
|
||||||
|
elif scene=="erase": bodies=[] if target else [(200,cy,15),(600,cy,10),(626,cy+15,10),(626,cy-15,10)]
|
||||||
|
elif scene=="steady": bodies=[] if target else [(600,cy,10),(626,cy+15,10),(626,cy-15,10)]
|
||||||
|
else: raise ValueError(f"unsupported scene {scene}")
|
||||||
|
return bodies,sensors,"analytic builder geometry"
|
||||||
|
|
||||||
|
def masks(role,labels,shape):
|
||||||
|
yy,xx=np.ogrid[:shape[0],:shape[1]]; solid=np.zeros(shape,bool); sensor=np.zeros(shape,bool)
|
||||||
|
for r in labels:
|
||||||
|
bodies,sensors,_=geometry(r,shape)
|
||||||
|
for x,y,rad in bodies: solid|=(xx-x)**2+(yy-y)**2<=rad**2
|
||||||
|
for x,y in sensors: sensor|=(xx-x)**2+(yy-y)**2<=25
|
||||||
|
return solid,sensor
|
||||||
|
|
||||||
|
def roi(role,shape):
|
||||||
|
ny,nx=shape; _,sensors,_=geometry(role,shape); xs=sensors[1][0]; cy=(ny-1)/2; D=20
|
||||||
|
x0=max(0,int(xs-6*D)); x1=min(nx,int(xs+14*D)+1); y0=max(0,int(cy-5*D)); y1=min(ny,int(cy+5*D)+1)
|
||||||
|
return slice(y0,y1),slice(x0,x1)
|
||||||
|
|
||||||
|
def vorticity(ux,uy):
|
||||||
|
out=np.zeros_like(ux,dtype=np.float64); out[1:-1,1:-1]=(uy[1:-1,2:]-uy[1:-1,:-2])/2-(ux[2:,1:-1]-ux[:-2,1:-1])/2; return out
|
||||||
|
|
||||||
|
def discrete(name,lo,hi,bins):
|
||||||
|
edges=np.linspace(lo,hi,bins+1); colors=plt.get_cmap(name)(np.linspace(0,1,bins))
|
||||||
|
if name==VORT_CMAP and bins%2: colors[bins//2]=[1,1,1,1]
|
||||||
|
return edges,BoundaryNorm(edges,bins),ListedColormap(colors)
|
||||||
|
|
||||||
|
def new_axes(nx,ny):
|
||||||
|
fig=plt.figure(figsize=(nx/100,ny/100),dpi=100,facecolor="white"); ax=fig.add_axes([0,0,1,1]); ax.set_xlim(-.5,nx-.5); ax.set_ylim(-.5,ny-.5); ax.set_aspect("equal"); ax.axis("off"); return fig,ax
|
||||||
|
|
||||||
|
def overlays(ax,role,labels,shape,fill=False):
|
||||||
|
for r in labels:
|
||||||
|
bodies,sensors,_=geometry(r,shape)
|
||||||
|
for x,y,rad in bodies: ax.add_patch(Circle((x,y),rad,facecolor="white" if fill else "none",edgecolor="black",linewidth=.7,zorder=5))
|
||||||
|
if r is labels[0]:
|
||||||
|
for x,y in sensors: ax.add_patch(Circle((x,y),5,facecolor="#00a650" if fill else "none",edgecolor="#00a650",linewidth=.8,zorder=6))
|
||||||
|
ys,xs=roi(role,shape); ax.add_patch(Rectangle((xs.start-.5,ys.start-.5),xs.stop-xs.start,ys.stop-ys.start,fill=False,edgecolor="black",linewidth=.8,zorder=7))
|
||||||
|
|
||||||
|
def save_field(data,out,role,labels,lo,hi,bins,kind="vorticity",zero_contour=False):
|
||||||
|
ny,nx=data.shape; edges,norm,cmap=discrete(VORT_CMAP if kind=="vorticity" else ERROR_CMAP,lo,hi,bins); fig,ax=new_axes(nx,ny); rgba=cmap(norm(data)); solid,sensor=masks(role,labels,(ny,nx)); rgba[solid]=[1,1,1,1]; rgba[sensor]=[0,.65,.31,1]; ax.imshow(rgba,origin="lower",interpolation="nearest",extent=(-.5,nx-.5,-.5,ny-.5))
|
||||||
|
if zero_contour:
|
||||||
|
ax.contour(np.ma.array(data,mask=solid|sensor),levels=[0],colors=["#777"],linewidths=.45,corner_mask=False); cover=np.zeros_like(rgba); cover[solid]=[1,1,1,1]; cover[sensor]=[0,.65,.31,1]; ax.imshow(cover,origin="lower",interpolation="nearest",extent=(-.5,nx-.5,-.5,ny-.5),zorder=4)
|
||||||
|
overlays(ax,role,labels,(ny,nx)); fig.savefig(out,dpi=100,pad_inches=0); plt.close(fig); return edges
|
||||||
|
|
||||||
|
def stream_no_arrows(ax,*args,**kw):
|
||||||
|
s=ax.streamplot(*args,**kw); s.arrows.set_visible(False)
|
||||||
|
for p in tuple(ax.patches):
|
||||||
|
if isinstance(p,FancyArrowPatch): p.remove()
|
||||||
|
return s.lines
|
||||||
|
|
||||||
|
def save_stream(ux,uy,out,role,labels):
|
||||||
|
ny,nx=ux.shape; fig,ax=new_axes(nx,ny); solid,_=masks(role,labels,(ny,nx)); x=np.arange(nx); y=np.arange(ny); mu=np.ma.array(ux,mask=solid); mv=np.ma.array(uy,mask=solid); stream_no_arrows(ax,x,y,mu,mv,density=STREAM_DENSITY,color="black",linewidth=.45); ys,xs=roi(role,(ny,nx)); lines=stream_no_arrows(ax,x[xs],y[ys],mu[ys,xs],mv[ys,xs],density=ROI_STREAM_DENSITY,color="black",linewidth=.45); lines.set_clip_path(Rectangle((xs.start-.5,ys.start-.5),xs.stop-xs.start,ys.stop-ys.start,transform=ax.transData)); overlays(ax,role,labels,(ny,nx),True); fig.savefig(out,dpi=100,pad_inches=0); plt.close(fig)
|
||||||
|
|
||||||
|
def colorbar_ticks(edges,max_ticks,include_zero):
|
||||||
|
if include_zero and len(edges)%2==0:
|
||||||
|
n=len(edges)-1; center=n//2; candidates=np.linspace(0,len(edges)-1,min(max_ticks,len(edges))).round().astype(int); candidates=[i for i in np.unique(candidates) if i not in (center,center+1)]
|
||||||
|
ticks=list(edges[candidates]); ticks.append(0.0); return np.asarray(sorted(set(ticks)))
|
||||||
|
ids=np.rint(np.linspace(0,len(edges)-1,min(max_ticks,len(edges)))).astype(int); ticks=list(edges[np.unique(ids)])
|
||||||
|
if include_zero and edges[0]<0<edges[-1] and not any(np.isclose(t,0) for t in ticks): ticks.append(0.)
|
||||||
|
return np.asarray(sorted(ticks))
|
||||||
|
|
||||||
|
def colorbar(out,kind,lo,hi,bins,orientation):
|
||||||
|
edges,norm,cmap=discrete(VORT_CMAP if kind=="vorticity" else ERROR_CMAP,lo,hi,bins); fig=plt.figure(figsize=((1.6,4) if orientation=="vertical" else (10,1.2)),dpi=100,facecolor="white"); ax=fig.add_axes(([.18,.08,.18,.84] if orientation=="vertical" else [.06,.46,.88,.24])); cb=ColorbarBase(ax,cmap=cmap,norm=norm,boundaries=edges,orientation=orientation); cb.set_ticks(colorbar_ticks(edges,min(11,bins+1) if orientation=="vertical" else min(7,bins+1),kind=="vorticity")); axis=cb.ax.xaxis if orientation=="horizontal" else cb.ax.yaxis; axis.set_major_formatter(FuncFormatter(lambda v,_: f"{v:.1e}" if kind=="vorticity" else f"{v:g}")); cb.set_label(kind); fig.savefig(out,dpi=100,pad_inches=0); plt.close(fig)
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
here=Path(__file__).resolve().parent; p=argparse.ArgumentParser(description=__doc__); p.add_argument("--data-root",type=Path,default=here/"data/reproduction"); p.add_argument("--output-root",type=Path,default=here/"data/reproduction_flow_plots/all"); p.add_argument("--generation",choices=("v5","legacy")); p.add_argument("--case"); p.add_argument("--phase-index",type=int,default=0,help="phase slot to render from each 8-phase cycle (default: 0)"); p.add_argument("--v5-vorticity-limit",type=float,default=.0005); p.add_argument("--legacy-vorticity-limit",type=float,default=.0015); p.add_argument("--vorticity-bins",type=int,default=21); p.add_argument("--error-limit",type=float,default=100); p.add_argument("--error-bins",type=int,default=10); a=p.parse_args(argv); root=a.data_root.resolve(); out=a.output_root.resolve(); roles=discover(root,a.generation,a.case); out.parent.mkdir(parents=True,exist_ok=True); stage=Path(tempfile.mkdtemp(prefix=".all-",dir=out.parent)); entries=[]; groups={}; source_cache={}
|
||||||
|
try:
|
||||||
|
for r in roles:
|
||||||
|
group_key=(r.generation,r.case_id,r.scene,r.scenario) if r.scene=="vortex" else (r.generation,r.case_id,r.scene)
|
||||||
|
groups.setdefault(group_key,[]).append(r)
|
||||||
|
for key,group in sorted(groups.items()):
|
||||||
|
gen,case,scene,*group_scenarios=key; target=next((r for r in group if r.label=="target"),None); target_fields={}; target_has_mean=False
|
||||||
|
if target:
|
||||||
|
_,tu,tv,td,target_has_mean,target_info,target_schema=load_artifact(target, source_cache, a.phase_index); target_fields={x["index"]:(tu[i],tv[i],x) for i,x in enumerate(td)}
|
||||||
|
for r in group:
|
||||||
|
path,uxs,uys,descs,has_mean,source_info,schema=load_artifact(r, source_cache, a.phase_index); base=stage/gen/case/r.scenario/r.role_id
|
||||||
|
for i,d in enumerate(descs):
|
||||||
|
ux,uy=uxs[i],uys[i]; stem=f'{d["mode"]}{d["index"]}'+(f'_offset{d["offset"]:+.3f}' if d["offset"] is not None else ''); vp=base/"vorticity"/(stem+".png"); sp=base/"streamlines"/(stem+".png"); vp.parent.mkdir(parents=True,exist_ok=True); sp.parent.mkdir(parents=True,exist_ok=True); limit=a.v5_vorticity_limit if gen=="v5" else a.legacy_vorticity_limit; ve=save_field(vorticity(ux,uy),vp,r,[r],-limit,limit,a.vorticity_bins,"vorticity",gen=="legacy"); save_stream(ux,uy,sp,r,[r]); e={"generation":gen,"case_id":case,"scenario":r.scenario,"role":r.label,"role_id":r.role_id,"scene":scene,"field_mode":d["mode"],"field_index":d["index"],"relative_offset":d["offset"],"source":str(path),"source_sha256":source_info["source_sha256"],"schema":schema,"shape_xy":[ux.shape[1],ux.shape[0]],"image_dimensions":[ux.shape[1],ux.shape[0]],"vorticity":str(vp.relative_to(stage)),"streamlines":str(sp.relative_to(stage)),"vorticity_edges":ve.tolist(),"geometry_provenance":geometry(r,ux.shape)[2]}
|
||||||
|
ref=target_fields.get(d["index"]) if target and scene not in ("erase",) else target_fields.get(0) if target and scene=="erase" else None
|
||||||
|
if r.label!="target" and ref is not None and ref[0].shape==ux.shape:
|
||||||
|
err=np.minimum(a.error_limit,100*np.hypot(ux-ref[0],uy-ref[1])/U0); ep=base/"error"/(stem+".png"); ep.parent.mkdir(parents=True,exist_ok=True); e["error"]=str(ep.relative_to(stage)); e["error_edges"]=save_field(err,ep,r,[r,target],0,a.error_limit,a.error_bins,"error",False).tolist()
|
||||||
|
elif r.label!="target": e["error_skipped_reason"]="no safely aligned target field"
|
||||||
|
entries.append(e)
|
||||||
|
if has_mean and target_has_mean and r.label!="target" and target and target_fields:
|
||||||
|
with np.load(path,allow_pickle=False) as z: mu,mv=np.asarray(z["mean_ux"]),np.asarray(z["mean_uy"])
|
||||||
|
with np.load(artifact(target.path),allow_pickle=False) as z: tu,tv=np.asarray(z["mean_ux"]),np.asarray(z["mean_uy"])
|
||||||
|
if mu.shape==tu.shape:
|
||||||
|
err=np.minimum(a.error_limit,100*np.hypot(mu-tu,mv-tv)/U0); mp=base/"error"/"mean.png"; mp.parent.mkdir(parents=True,exist_ok=True); entries[-1]["mean_error"]=str(mp.relative_to(stage)); entries[-1]["mean_error_edges"]=save_field(err,mp,r,[r,target],0,a.error_limit,a.error_bins,"error",False).tolist()
|
||||||
|
cbbase=stage/gen/case; cbbase.mkdir(parents=True,exist_ok=True); colorbar(cbbase/"vorticity_colorbar.png","vorticity",-(a.v5_vorticity_limit if gen=="v5" else a.legacy_vorticity_limit),a.v5_vorticity_limit if gen=="v5" else a.legacy_vorticity_limit,a.vorticity_bins,"vertical"); colorbar(cbbase/"vorticity_colorbar_horizontal.png","vorticity",-(a.v5_vorticity_limit if gen=="v5" else a.legacy_vorticity_limit),a.v5_vorticity_limit if gen=="v5" else a.legacy_vorticity_limit,a.vorticity_bins,"horizontal"); colorbar(cbbase/"error_colorbar.png","error",0,a.error_limit,a.error_bins,"vertical"); colorbar(cbbase/"error_colorbar_horizontal.png","error",0,a.error_limit,a.error_bins,"horizontal")
|
||||||
|
manifest={"schema":"drl-pinball-flow-plots-v2","entries":entries,"settings":{"U0":U0,"L0":L0,"vorticity_bins":a.vorticity_bins,"error_bins":a.error_bins,"stream_density":STREAM_DENSITY,"roi_density":ROI_STREAM_DENSITY,"atomic_publish":True,"phase_index":a.phase_index},"inventory":{"field_entries":len(entries),"vorticity":len(entries),"streamlines":len(entries),"errors":sum("error" in e for e in entries),"mean_errors":sum("mean_error" in e for e in entries),"skipped_errors":sum("error_skipped_reason" in e for e in entries),"unique_sources":len(source_cache),"unique_cases":len({(e["generation"],e["case_id"]) for e in entries}),"unique_scenarios":len({(e["generation"],e["case_id"],e["scenario"]) for e in entries})}}; (stage/"manifest.json").write_text(json.dumps(manifest,indent=2)+"\n"); out.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
if out.exists(): shutil.rmtree(out)
|
||||||
|
os.replace(stage,out)
|
||||||
|
except Exception: shutil.rmtree(stage,ignore_errors=True); raise
|
||||||
|
print(f"wrote {len(entries)} field entries to {out}")
|
||||||
|
if __name__=="__main__": main()
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -37,6 +38,15 @@ LEGACY_VORTEX = (
|
|||||||
"vortex_taylor_y000", "vortex_taylor_yp1L", "vortex_taylor_yp2L",
|
"vortex_taylor_y000", "vortex_taylor_yp1L", "vortex_taylor_yp2L",
|
||||||
)
|
)
|
||||||
LEGACY_GROUPS = LEGACY_PERIODIC + ("steady",) + LEGACY_VORTEX + ("erase",)
|
LEGACY_GROUPS = LEGACY_PERIODIC + ("steady",) + LEGACY_VORTEX + ("erase",)
|
||||||
|
SR_TRAINING = LEGACY_PERIODIC
|
||||||
|
SR_GENERALIZATION = (
|
||||||
|
"karman_re25", "karman_re70", "karman_re150", "karman_re300",
|
||||||
|
"illusion_05L", "illusion_06L", "illusion_08L", "illusion_12L", "illusion_2L",
|
||||||
|
)
|
||||||
|
SR_VARIANT_CASE = {
|
||||||
|
"k_front0": "karman_re100", "k_rear0": "karman_re100", "k_rear1": "karman_re100",
|
||||||
|
"i_front0": "illusion_15L", "i_front1": "illusion_15L", "i_rear0": "illusion_15L", "i_rear1": "illusion_15L",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def fail(message: str) -> None:
|
def fail(message: str) -> None:
|
||||||
@@ -65,7 +75,7 @@ def roles_for(root: Path, pipeline: str, group: str):
|
|||||||
|
|
||||||
|
|
||||||
def field_spec(pipeline: str, group: str, role_dir: Path):
|
def field_spec(pipeline: str, group: str, role_dir: Path):
|
||||||
if pipeline == "v5" or group in LEGACY_PERIODIC or (group == "erase" and role_dir.name != "target"):
|
if pipeline == "v5" or group in LEGACY_PERIODIC + SR_GENERALIZATION or (group == "erase" and role_dir.name != "target"):
|
||||||
return "phase_fields.npz", "target_phase", 0.0
|
return "phase_fields.npz", "target_phase", 0.0
|
||||||
if group in LEGACY_VORTEX:
|
if group in LEGACY_VORTEX:
|
||||||
return "event_fields.npz", "relative_offsets", -10
|
return "event_fields.npz", "relative_offsets", -10
|
||||||
@@ -124,6 +134,78 @@ def padded_limits(values):
|
|||||||
return [lo - pad, hi + pad]
|
return [lo - pad, hi + pad]
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path):
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_role_set(stage: Path, data_root: Path, group: str, roles, output_name: str):
|
||||||
|
loaded = [load_role(path, "legacy", group) for _, path in roles]
|
||||||
|
shapes = {item[0].shape for item in loaded}
|
||||||
|
if len(shapes) != 1:
|
||||||
|
fail(f"role field shapes differ for SR/{group}: {shapes}")
|
||||||
|
all_sensors = np.concatenate([item[1] for item in loaded], axis=0)
|
||||||
|
xlim = padded_limits(all_sensors[:, [0, 2, 4]])
|
||||||
|
ylim = padded_limits(all_sensors[:, [1, 3, 5]])
|
||||||
|
ny, nx = next(iter(shapes)); extent = (0, (nx - 1) / L0, 0, (ny - 1) / L0)
|
||||||
|
fig, axes = plt.subplots(2, len(roles), figsize=(6 * len(roles), 8), constrained_layout=True, squeeze=False)
|
||||||
|
images = []
|
||||||
|
for col, ((label, _), (omega, sensors, units, _, _, _)) in enumerate(zip(roles, loaded)):
|
||||||
|
images.append(axes[0, col].imshow(omega, origin="lower", extent=extent, cmap="RdBu_r", vmin=-VORTICITY_LIMIT, vmax=VORTICITY_LIMIT, aspect="equal"))
|
||||||
|
axes[0, col].set_title(f"LEGACY / {group} — {label}")
|
||||||
|
axes[0, col].set_xlabel("x/L0"); axes[0, col].set_ylabel("y/L0")
|
||||||
|
axes[0, col].set_xlim(extent[:2]); axes[0, col].set_ylim(extent[2:])
|
||||||
|
for (u, v, sensor_label), color in zip(SENSOR_PAIRS, SENSOR_COLORS):
|
||||||
|
axes[1, col].plot(sensors[:, u], sensors[:, v], color=color, lw=1.1, alpha=0.85, label=sensor_label)
|
||||||
|
axes[1, col].axhline(0, color="0.35", lw=0.7); axes[1, col].axvline(0, color="0.35", lw=0.7)
|
||||||
|
axes[1, col].grid(True, alpha=0.25); axes[1, col].set_xlim(xlim); axes[1, col].set_ylim(ylim)
|
||||||
|
axes[1, col].set_xlabel(f"sensor u ({units})"); axes[1, col].set_ylabel(f"sensor v ({units})")
|
||||||
|
axes[1, col].set_box_aspect(0.8); axes[1, col].legend(loc="best", frameon=False)
|
||||||
|
cbar = fig.colorbar(images[0], ax=axes[0, :], orientation="vertical", shrink=0.92, pad=0.015)
|
||||||
|
cbar.set_label(r"$\omega_z$ (lattice$^{-1}$)")
|
||||||
|
output = stage / "legacy" / f"{output_name}.png"; output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fig.savefig(output, dpi=160); plt.close(fig)
|
||||||
|
role_entries = []
|
||||||
|
for (label, path), item in zip(roles, loaded):
|
||||||
|
sources = [path / "metadata.json", path / "timeseries.csv", item[3]]
|
||||||
|
role_entries.append({"role": label, "source": str(path), "field_file": str(item[3]), "field_slot": 0,
|
||||||
|
"selector": item[4], "selector_value": float(item[5]),
|
||||||
|
"source_files": [{"path": str(source), "sha256": sha256_file(source), "bytes": source.stat().st_size} for source in sources]})
|
||||||
|
return {"pipeline": "legacy-sr", "group": group, "plot": f"legacy/{output_name}.png", "roles": role_entries,
|
||||||
|
"vorticity_limit": VORTICITY_LIMIT, "sensor_limits": {"u": xlim, "v": ylim}}
|
||||||
|
|
||||||
|
|
||||||
|
def sr_plot_specs(data_root: Path):
|
||||||
|
legacy = data_root / "legacy"; specs = []
|
||||||
|
for case in SR_TRAINING:
|
||||||
|
base = legacy / case
|
||||||
|
specs.append((case, (("Target", base / "target"), ("PPO", base / "controlled"), ("SR", base / "sr"), ("Zero (physical, no control)", base / "zero")), case))
|
||||||
|
for case in SR_GENERALIZATION:
|
||||||
|
base = legacy / case
|
||||||
|
specs.append((case, (("Target", base / "target"), ("SR", base / "sr"), ("Zero (physical, no control)", base / "zero")), case))
|
||||||
|
for variant, case in SR_VARIANT_CASE.items():
|
||||||
|
base = legacy / case
|
||||||
|
specs.append((case, (("Target", base / "target"), ("Parent SR", base / "sr"), (f"Variant SR ({variant})", base / f"sr_{variant}"), ("Zero (physical, no control)", base / "zero")), f"variants/{case}_{variant}"))
|
||||||
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
def render_sr_package(stage: Path, data_root: Path, allow_partial=False):
|
||||||
|
entries = []
|
||||||
|
for group, roles, output_name in sr_plot_specs(data_root):
|
||||||
|
missing = [str(path) for _, path in roles if not path.is_dir()]
|
||||||
|
if missing:
|
||||||
|
if allow_partial:
|
||||||
|
continue
|
||||||
|
fail(f"missing SR plot roles for {group}: {missing}")
|
||||||
|
entries.append(plot_role_set(stage, data_root, group, roles, output_name))
|
||||||
|
if not allow_partial and len(entries) != 23:
|
||||||
|
fail(f"expected 23 SR plots, got {len(entries)}")
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def plot_group(stage: Path, data_root: Path, pipeline: str, group: str):
|
def plot_group(stage: Path, data_root: Path, pipeline: str, group: str):
|
||||||
roles = roles_for(data_root, pipeline, group)
|
roles = roles_for(data_root, pipeline, group)
|
||||||
loaded = [load_role(path, pipeline, group) for _, path in roles]
|
loaded = [load_role(path, pipeline, group) for _, path in roles]
|
||||||
@@ -171,27 +253,38 @@ def main() -> None:
|
|||||||
here = Path(__file__).resolve().parent
|
here = Path(__file__).resolve().parent
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--data-root", type=Path, default=here / "data" / "reproduction")
|
parser.add_argument("--data-root", type=Path, default=here / "data" / "reproduction")
|
||||||
parser.add_argument("--output-root", type=Path, default=here / "data" / "reproduction_plots")
|
parser.add_argument("--output-root", type=Path)
|
||||||
|
parser.add_argument("--campaign", choices=("ppo", "sr"), default="ppo")
|
||||||
|
parser.add_argument("--allow-partial", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
data_root, output_root = args.data_root.resolve(), args.output_root.absolute()
|
data_root = args.data_root.resolve()
|
||||||
require_exact_groups(data_root)
|
output_root = (args.output_root or here / "data" / ("reproduction_plots_sr" if args.campaign == "sr" else "reproduction_plots")).absolute()
|
||||||
|
if args.campaign == "ppo": require_exact_groups(data_root)
|
||||||
if output_root.is_symlink():
|
if output_root.is_symlink():
|
||||||
fail(f"refusing symlink output root: {output_root}")
|
fail(f"refusing symlink output root: {output_root}")
|
||||||
output_root.parent.mkdir(parents=True, exist_ok=True)
|
output_root.parent.mkdir(parents=True, exist_ok=True)
|
||||||
stage = Path(tempfile.mkdtemp(prefix=f".{output_root.name}.staging-", dir=output_root.parent))
|
stage = Path(tempfile.mkdtemp(prefix=f".{output_root.name}.staging-", dir=output_root.parent))
|
||||||
backup = output_root.with_name(f".{output_root.name}.old-{os.getpid()}")
|
backup = output_root.with_name(f".{output_root.name}.old-{os.getpid()}")
|
||||||
try:
|
try:
|
||||||
|
if args.campaign == "sr":
|
||||||
|
entries = render_sr_package(stage, data_root, allow_partial=args.allow_partial)
|
||||||
|
manifest = {"schema": "drl-pinball-sr-reproduction-plots-v1", "plot_count": len(entries),
|
||||||
|
"expected_plot_count": 23, "allow_partial": args.allow_partial,
|
||||||
|
"no_ppo_cases": list(SR_GENERALIZATION),
|
||||||
|
"no_ppo_contract": "Generalization conditions intentionally contain Target/SR/Zero only; PPO is absent by acquisition design.",
|
||||||
|
"variant_contract": "Each diagnostic binds the named variant to its same-case parent SR.",
|
||||||
|
"vorticity_limit": VORTICITY_LIMIT,
|
||||||
|
"vorticity_contract": "fixed symmetric [-0.001, +0.001] for every vorticity panel",
|
||||||
|
"plots": entries}
|
||||||
|
else:
|
||||||
entries = [plot_group(stage, data_root, "v5", group) for group in sorted(V5_GROUPS)]
|
entries = [plot_group(stage, data_root, "v5", group) for group in sorted(V5_GROUPS)]
|
||||||
entries += [plot_group(stage, data_root, "legacy", group) for group in LEGACY_GROUPS]
|
entries += [plot_group(stage, data_root, "legacy", group) for group in LEGACY_GROUPS]
|
||||||
if len(entries) != 30:
|
if len(entries) != 30:
|
||||||
fail(f"expected 30 plots, got {len(entries)}")
|
fail(f"expected 30 plots, got {len(entries)}")
|
||||||
(stage / "manifest.json").write_text(json.dumps({
|
manifest = {"schema": "drl-pinball-reproduction-plots-v1", "plot_count": 30,
|
||||||
"schema": "drl-pinball-reproduction-plots-v1",
|
|
||||||
"plot_count": 30,
|
|
||||||
"vorticity_limit": VORTICITY_LIMIT,
|
"vorticity_limit": VORTICITY_LIMIT,
|
||||||
"vorticity_contract": "fixed symmetric [-0.001, +0.001] for every vorticity panel",
|
"vorticity_contract": "fixed symmetric [-0.001, +0.001] for every vorticity panel", "plots": entries}
|
||||||
"plots": entries,
|
(stage / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||||
}, indent=2) + "\n")
|
|
||||||
if output_root.exists():
|
if output_root.exists():
|
||||||
os.replace(output_root, backup)
|
os.replace(output_root, backup)
|
||||||
os.replace(stage, output_root)
|
os.replace(stage, output_root)
|
||||||
@@ -202,7 +295,7 @@ def main() -> None:
|
|||||||
if backup.exists() and not output_root.exists():
|
if backup.exists() and not output_root.exists():
|
||||||
os.replace(backup, output_root)
|
os.replace(backup, output_root)
|
||||||
raise
|
raise
|
||||||
print(f"wrote 30 plots and manifest to {output_root}")
|
print(f"wrote {len(entries)} plots and manifest to {output_root}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
_TRAIN = Path(__file__).resolve().parents[1] / "train"
|
||||||
|
sys.path.insert(0, str(_TRAIN))
|
||||||
|
|
||||||
|
from env_karman import compute_similarity
|
||||||
|
from normalization import (
|
||||||
|
CALIBRATION_SCHEMA_NATIVE_V2,
|
||||||
|
LEGACY_SENSOR_FACTOR,
|
||||||
|
POLICY_SENSOR_UNITS_LEGACY,
|
||||||
|
SENSOR_UNITS_NATIVE,
|
||||||
|
convert_legacy_calibration,
|
||||||
|
convert_legacy_harmonics,
|
||||||
|
convert_legacy_target,
|
||||||
|
dtw_sensor_factor,
|
||||||
|
policy_sensor_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_calibration():
|
||||||
|
return {"SENSOR_CC": 78.0, "SENS_SCALE": 0.78, "dtw_norm_scale": 0.234}
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibration_conversion_and_policy_schema_behavior():
|
||||||
|
legacy = _legacy_calibration()
|
||||||
|
native = convert_legacy_calibration(legacy)
|
||||||
|
assert native["schema_version"] == CALIBRATION_SCHEMA_NATIVE_V2
|
||||||
|
assert native["sensor_units"] == SENSOR_UNITS_NATIVE
|
||||||
|
assert "SENSOR_CC" not in native
|
||||||
|
assert np.isclose(native["SENS_SCALE"], 0.01)
|
||||||
|
assert np.isclose(native["dtw_norm_scale"], 0.003)
|
||||||
|
assert dtw_sensor_factor(legacy) == LEGACY_SENSOR_FACTOR
|
||||||
|
assert dtw_sensor_factor(native) == 1.0
|
||||||
|
assert np.isclose(policy_sensor_scale(native), 0.01)
|
||||||
|
assert np.isclose(policy_sensor_scale(native, POLICY_SENSOR_UNITS_LEGACY), 0.78)
|
||||||
|
assert np.isclose(policy_sensor_scale(legacy), 0.78)
|
||||||
|
|
||||||
|
|
||||||
|
def test_target_and_harmonics_conversion_only_scale_sensor_channels():
|
||||||
|
target = np.arange(24, dtype=np.float32).reshape(3, 8)
|
||||||
|
converted = convert_legacy_target(target)
|
||||||
|
np.testing.assert_allclose(converted[:, :6], target[:, :6] / 78.0)
|
||||||
|
np.testing.assert_allclose(converted[:, 6:], target[:, 6:])
|
||||||
|
|
||||||
|
harmonics = [
|
||||||
|
{"dc": float(i + 1), "amps": [float(i + 2)], "freqs": [0.1], "phases": [0.2]}
|
||||||
|
for i in range(8)
|
||||||
|
]
|
||||||
|
converted_h = convert_legacy_harmonics(harmonics)
|
||||||
|
for i in range(6):
|
||||||
|
assert np.isclose(converted_h[i]["dc"], harmonics[i]["dc"] / 78.0)
|
||||||
|
assert np.isclose(converted_h[i]["amps"][0], harmonics[i]["amps"][0] / 78.0)
|
||||||
|
assert converted_h[6:] == harmonics[6:]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dtw_similarity_is_invariant_under_unit_conversion():
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
target = rng.normal(size=(150, 6))
|
||||||
|
state = rng.normal(size=(90, 6))
|
||||||
|
legacy_scale = 0.25
|
||||||
|
legacy_sim = compute_similarity(target, state, 30, legacy_scale)
|
||||||
|
native_sim = compute_similarity(
|
||||||
|
target / LEGACY_SENSOR_FACTOR,
|
||||||
|
state / LEGACY_SENSOR_FACTOR,
|
||||||
|
30,
|
||||||
|
legacy_scale / LEGACY_SENSOR_FACTOR,
|
||||||
|
)
|
||||||
|
assert np.isclose(native_sim, legacy_sim, atol=1e-12)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export retained V5 per-iteration TensorBoard metrics to CSV without redrawing figures."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, csv, hashlib, json
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
from tensorboard.backend.event_processing.event_accumulator import EventAccumulator
|
||||||
|
from drl_pinball.train import plot_scratch_results as source
|
||||||
|
|
||||||
|
FIELDS = ("run","case_id","seed","training_iteration","eval_reward","eval_r_cd","eval_r_cl","eval_r_sim","running_best_reward","drawdown_from_running_best","is_authoritative_best_iteration","source_stream","source_role","current_contract_retraining_evidence")
|
||||||
|
|
||||||
|
def sha256(path: Path) -> str:
|
||||||
|
h=hashlib.sha256();
|
||||||
|
with path.open("rb") as f:
|
||||||
|
for chunk in iter(lambda:f.read(1<<20), b""): h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
def tensorboard_conflicts(tb_dir: Path) -> dict[str, int]:
|
||||||
|
accumulator=EventAccumulator(str(tb_dir),size_guidance={"scalars":0}); accumulator.Reload()
|
||||||
|
result={}
|
||||||
|
for short,tag in source.TAGS.items():
|
||||||
|
grouped={}
|
||||||
|
for event in accumulator.Scalars(tag): grouped.setdefault(int(event.step),[]).append(float(event.value))
|
||||||
|
result[short]=sum(len(values)>1 and len(set(values))>1 for values in grouped.values())
|
||||||
|
return result
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
here=Path(__file__).resolve().parent
|
||||||
|
ap=argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--train-root",type=Path,default=here/"output")
|
||||||
|
ap.add_argument("--output-dir",type=Path,default=here/"results"/"training_csv")
|
||||||
|
args=ap.parse_args(); args.output_dir.mkdir(parents=True,exist_ok=True)
|
||||||
|
discovered=source.discover_runs(args.train_root); rows=[]; runs=[]; excluded=[]
|
||||||
|
for run in sorted(discovered,key=lambda x:x.name):
|
||||||
|
conflicts=tensorboard_conflicts(run.path/"tb")
|
||||||
|
if any(conflicts.values()):
|
||||||
|
excluded.append({"run":run.name,"reason":"conflicting duplicate TensorBoard steps from resumed branches; excluded fail-closed","conflicting_steps_by_metric":conflicts,"known_provenance_warning":"kar_re60 includes mixed-config resume history, including a Re100 base-config segment"})
|
||||||
|
continue
|
||||||
|
runs.append(run)
|
||||||
|
meta=json.loads((run.path/"meta.json").read_text()); best=int(meta["best_episode"]); incumbent=np.maximum.accumulate(run.reward)
|
||||||
|
for i,step in enumerate(run.steps):
|
||||||
|
rows.append({"run":run.name,"case_id":run.case,"seed":run.seed,"training_iteration":int(step),"eval_reward":float(run.metrics["reward"][i]),"eval_r_cd":float(run.metrics["r_cd"][i]),"eval_r_cl":float(run.metrics["r_cl"][i]),"eval_r_sim":float(run.metrics["r_sim"][i]),"running_best_reward":float(incumbent[i]),"drawdown_from_running_best":float(incumbent[i]-run.reward[i]),"is_authoritative_best_iteration":int(step)==best,"source_stream":"TensorBoard eval/* scalars","source_role":"auxiliary per-iteration series; best point checked against meta.json","current_contract_retraining_evidence":False})
|
||||||
|
out=args.output_dir/"training_iterations.csv"
|
||||||
|
with out.open("w",newline="") as f:
|
||||||
|
w=csv.DictWriter(f,fieldnames=FIELDS,lineterminator="\n"); w.writeheader(); w.writerows(rows)
|
||||||
|
manifest={"schema":"drl-pinball-training-iterations-csv-v2","row_count":len(rows),"included_run_count":len(runs),"discovered_run_count":len(discovered),"case_count":len({r.case for r in runs}),"excluded_runs":excluded,"duplicate_policy":"fail closed: exclude any run with conflicting duplicate values at a TensorBoard step","source":"retained TensorBoard eval scalars","authority":"meta.json for best reward/iteration; TensorBoard is auxiliary","current_contract_retraining_evidence":False,"files":{"training_iterations.csv":{"sha256":sha256(out),"bytes":out.stat().st_size}}}
|
||||||
|
(args.output_dir/"manifest.json").write_text(json.dumps(manifest,indent=2)+"\n")
|
||||||
|
print(f"wrote {len(rows)} rows for {len(runs)} clean runs; excluded {len(excluded)} ambiguous runs -> {args.output_dir}"); return 0
|
||||||
|
if __name__=="__main__": raise SystemExit(main())
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema": "drl-pinball-training-iterations-csv-v2",
|
||||||
|
"row_count": 7000,
|
||||||
|
"included_run_count": 14,
|
||||||
|
"discovered_run_count": 15,
|
||||||
|
"case_count": 10,
|
||||||
|
"excluded_runs": [
|
||||||
|
{
|
||||||
|
"run": "kar_re60_seed43",
|
||||||
|
"reason": "conflicting duplicate TensorBoard steps from resumed branches; excluded fail-closed",
|
||||||
|
"conflicting_steps_by_metric": {
|
||||||
|
"reward": 213,
|
||||||
|
"r_cd": 213,
|
||||||
|
"r_cl": 213,
|
||||||
|
"r_sim": 213
|
||||||
|
},
|
||||||
|
"known_provenance_warning": "kar_re60 includes mixed-config resume history, including a Re100 base-config segment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"duplicate_policy": "fail closed: exclude any run with conflicting duplicate values at a TensorBoard step",
|
||||||
|
"source": "retained TensorBoard eval scalars",
|
||||||
|
"authority": "meta.json for best reward/iteration; TensorBoard is auxiliary",
|
||||||
|
"current_contract_retraining_evidence": false,
|
||||||
|
"files": {
|
||||||
|
"training_iterations.csv": {
|
||||||
|
"sha256": "8b75fdb6a32f13ef85f7e313f7ae75ce352dd8367c5679f3a3ce8fa262a5a490",
|
||||||
|
"bytes": 1777579
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||