Skip to content
Merged
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
22 changes: 18 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
name: tests (fast)

# Fast unit suite — runs on every push and PR. No OpenFUSIONToolkit / live
# solver needed (those are the `solver`-marked tests, gated to the merge queue;
# see solver.yml). `pytest` is fast-by-default via `addopts = -m "not solver"`
# in pyproject.toml.
# solver needed (those are the `solver`-marked tests; see solver.yml).
# `pytest` is fast-by-default via `addopts = -m "not solver"` in pyproject.toml.
#
# Two dependency sets so an upstream numpy/scipy release surfaces as its own
# signal instead of failing unrelated feature PRs (e.g. scipy 1.18 changed
# RectBivariateSpline.ev scalar returns; numpy 2.5 made float(shape-(1,))
# a TypeError):
# * pinned — the validated floor (bump deliberately when adopting new deps)
# * latest — whatever pip resolves today (the early-warning canary)
on:
push:
pull_request:
Expand All @@ -15,14 +21,22 @@ concurrency:
jobs:
fast:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
deps: [pinned, latest]
name: fast (${{ matrix.deps }})
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
Comment on lines +24 to 33
- name: Install package + pytest
- name: Install package + pytest (${{ matrix.deps }} deps)
run: |
python -m pip install --upgrade pip
if [ "${{ matrix.deps }}" = "pinned" ]; then
pip install "numpy==2.4.*" "scipy==1.17.*"
fi
pip install -e . pytest
- name: Run fast suite (solver tests deselected by default)
run: pytest -q
28 changes: 28 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
cff-version: 1.2.0
message: "If you use bouquet in your research, please cite it as below."
title: "bouquet: BOotstrap Uncertainty QUantified Equilibrium Toolkit"
type: software
authors:
- family-names: Burgess
given-names: Daniel
- family-names: Hansen
given-names: Chris
version: 0.2.0
doi: 10.5281/zenodo.19398541
date-released: 2026-07-02
license: LGPL-3.0
repository-code: "https://github.com/d-burg/bouquet"
url: "https://d-burg.github.io/bouquet/"
abstract: >-
GP-sampled perturbed tokamak equilibria for uncertainty quantification
with OpenFUSIONToolkit/TokaMaker. bouquet generates families ("bouquets")
of perturbed equilibria from a baseline kinetic equilibrium by drawing
correlated profile perturbations from Gaussian-process regression
posteriors, solving the Grad-Shafranov equation for each sample, and
archiving all results to a single self-describing HDF5 database.
keywords:
- tokamak
- plasma physics
- equilibrium
- uncertainty quantification
- Grad-Shafranov
377 changes: 261 additions & 116 deletions README.md

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions docs/CHANGES_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
# Bouquet — change summaries

## Class API + HDF5 schema v2 round (2026-07, PR #8)

Decisions and outcomes folded from the (deleted) working document
`docs/ux-review-feat-bouquet-class-api.md`:

1. **Config serialization + provenance**: `BouquetConfig.to_dict/from_dict`
(JSON); every archive stores `schema_version` / `bouquet_version` /
`created` (stamped at file creation) + per-scan `config_json`
(`write_provenance` / `load_config`).
2. **`BouquetArchive`** reader class (ScanView/DrawView, lazy, cached attrs,
fixed-name + legacy suffix-scan eqdsk/pfile lookup) — replaces downstream
hand-rolled h5 traversal.
3. **De-threaded readers**: module-level plot/filter/select functions accept a
`Bouquet` / `BouquetArchive` / header / path uniformly; a missing explicit
`scan_key` raises listing available keys (was silent-empty).
4. **Schema v2 — clean break, no legacy readers** (decision: no external users
yet): bare dataset names + `units` attrs, fixed `eqdsk`/`pfile` names,
coil names as a string dataset everywhere, `scan/<key>/` layout only.
Legacy files: `BouquetArchive` warns, `load_equilibrium` raises clearly.
Spec: `docs/archive-schema.md`; source of truth: `bouquet/schema.py`.
5. **Config/API simplifications**: `run.describe()` (non-default knobs),
`workflow` preset enum (`auto` / `geqdsk-standard` / `imas-diff-c` /
`custom`), source-agnostic `run.prepare()`.
6. **Sweeps + plotting**: `run.run_slices()` (one archive, one scan_key per
slice); `plot_bouquet` dispatches on stored `source_kind`
(`plot_imas_bouquet` demoted from `__all__`).
7. **Parallel hardening**: fresh shards, merge-side baseline guard +
expected-shard accounting (`--allow-missing`), nthreads=1 doctrine warning,
`SeedSequence(seed, worker, scan_key)` slice-decorrelated seeding, JSON
SLURM bundles, CWD-independent submit scripts, physical-core default.
8. **Post-review fixes** (8-angle adversarial review of the final diff):
solver-marked tests un-broken (coil_names dataset read), merged archives
get run-level `config_json`, multi-scan `load_config` disambiguation,
`DrawView` O(N) flags + cached attrs, `schema.find_bytes_dataset`
consolidation (8 duplicated lookups), unique eqdsk extraction filenames,
legacy-archive warnings/errors.
9. **Won't-do (user decisions)**: `jphi_scalar_sigma` default stays 0.10;
IDA_run per-shot notebooks stay split (no templating).
10. **Deferred**: example-notebook rewrite to run-object idioms;
reconstruction-style IMAS baseline summary block.

Also in this round: scipy≥1.18/numpy≥2.5 compatibility (`.item()` at the
axis-point `.ev()` call), and the CER/E_r feature (`read_ida_cer`,
`radial_field_from_cer`).

---

# Bouquet + OFT — change summary (golden suite, filtering, Ip-secant removal, systematics)

> **Historical document** (2026-05): a snapshot of the coil-bounds/golden-suite
Expand Down
12 changes: 9 additions & 3 deletions docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ PR-controlled code on lab infrastructure. So until OFT publishes to PyPI we kee
the comprehensive suite off automated CI and run it deliberately.

## Workflows
- `.github/workflows/tests.yml` — **fast** suite on `push`/`pull_request` (automated).
- `.github/workflows/tests.yml` — **fast** suite on `push`/`pull_request`
(automated), run on a 2-way dependency matrix: **pinned** (the validated
numpy/scipy floor — bump deliberately) and **latest** (unpinned canary).
An upstream numpy/scipy release then breaks only the `latest` leg, as its
own signal, instead of failing unrelated feature PRs (this bit us when
scipy 1.18 + numpy 2.5 changed scalar-conversion semantics).
- `.github/workflows/solver.yml` — **solver** suite, `workflow_dispatch` only
(manual "Run workflow" button / `gh workflow run solver.yml`). Not a required
check, so it never blocks a merge. It already contains the `runs-on`, env, and
Expand All @@ -32,8 +37,9 @@ Before merging a branch that touches the perturbation/solve pipeline into `main`
3. The automated `fast` check must also be green.

## Branch protection (`main`)
Require the **`fast`** status check + a review. **Do not** require `solver`
while it's manual — a required check with no runner would block merges forever.
Require the **`fast (pinned)`** status check (+ optionally `fast (latest)` and
a review). **Do not** require `solver` while it's manual — a required check
with no runner would block merges forever.

## Future: automate the solver gate
Once the OFT Ip fixes are merged upstream **and** OpenFUSIONToolkit publishes to
Expand Down
76 changes: 76 additions & 0 deletions docs/archive-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# The bouquet HDF5 archive — schema v2

Authoritative description of the on-disk layout written by bouquet ≥ 0.2.0.
The single source of truth in code is [`bouquet/schema.py`](../bouquet/schema.py)
(`SCHEMA_VERSION`, `PROFILE_UNITS`, fixed dataset names, `write_profile` /
`find_bytes_dataset`); this document mirrors it for human readers. Prefer
reading archives through [`bouquet.BouquetArchive`](../bouquet/archive.py) or
the functional readers (`load_equilibrium`, `load_baseline_profiles`,
`select_indices`, `load_config`) rather than raw `h5py`.

## Layout

```
{header}.h5 file attrs: schema_version (=2),
│ bouquet_version, created, updated
├── config_json JSON dump of the run BouquetConfig
│ (root copy = most recent write; the
│ per-scan copies below are authoritative)
└── scan/<scan_key>/ one group per scan point / time slice
├── config_json this slice's exact config
├── _baseline/ written once per scan point
│ ├── eqdsk, [pfile] raw byte-perfect g-file / p-file
│ ├── psi_N, psi_N_kinetic
│ ├── n_e, T_e, n_i, T_i kinetic profiles
│ ├── pressure[, pressure_thermal]
│ ├── j_phi[, j_BS, j_inductive] separated toroidal currents
│ ├── sigma_ne/te/ni/ti/jphi the uncertainty envelope used
│ ├── [aux_<name>, sigma_aux_<name>] switchboard channels
│ ├── [recon_lcfs_ref] 10k-pt LCFS reference (boundary metric)
│ ├── [x_points], [coil_currents, coil_names]
│ └── attrs: Ip_target, l_i_target, source_kind, [diverted]
└── <count>/ one group per accepted draw
│ (integer; gaps = rejected draws)
├── eqdsk, [pfile] raw bytes, fixed names
├── psi_N[, psi_N_kinetic]
├── j_phi, j_BS, j_inductive[, j_BS,edge]
├── n_e, T_e, n_i, T_i, w_ExB[, Zeff]
├── [pressure, pressure_thermal]
├── [aux_<name>] perturbed switchboard channels
├── [coil_currents, coil_names]
├── [perturbed_lcfs_ref], [x_points]
└── attrs: l_i(1), l_i(3), count, homotopy_*, max_F_drift_pct,
max_VSC_drift_pct, in_spec, inspec_*, l_i_target_used,
[diverted], [passes_coil_filter, passes_boundary_filter,
selected] ← filter flags, written post-hoc
```

## Conventions

- **Bare dataset names, units in attrs.** Profile datasets carry plain names
(`j_phi`, `n_e`, …) with the unit string in `ds.attrs["units"]`
(`PROFILE_UNITS` in `schema.py`). v1 archives embedded units in the name
(`"j_phi [A m^-2]"`).
- **Fixed byte-blob names.** The g-file / p-file bytes are stored as `eqdsk` /
`pfile` inside each group — the group path carries the coordinates. Bytes
are stored opaque (`np.void`) and round-trip bit-perfect.
- **Always `scan/<key>/`.** The scan key is a user-chosen label
(`GenerationConfig.scan_key`, default `0`) — a time in ms, a beta value, …
Several bouquets can share one file under different keys.
- **Gap-tolerant indices.** Rejected draws leave gaps; iterate with
`list_equilibrium_indices` / `BouquetArchive`, never `range(n)`.
- **Filtering is non-destructive.** Filters write boolean attrs
(`passes_*`, `selected` = AND of applied flags); `export_filtered` produces
a pruned copy, the source is never modified.
- **Provenance.** `schema_version` / `bouquet_version` / `created` are stamped
at file creation; `config_json` is added by `write_provenance` (called from
`Bouquet.generate`, `run_shard`, and `merge_archives`). Recover the exact
run configuration with `bq.load_config(path, scan_key=...)`.

## Legacy (pre-v2) archives

Schema v2 was a clean break (2026-07). Files without the `schema_version`
attr are pre-v2: `BouquetArchive` opens them with a warning (byte blobs still
resolve via a suffix scan; profile keys keep their v1 bracketed names), and
`load_equilibrium` raises a clear error. Regenerate old archives with the
current package for full support.
40 changes: 40 additions & 0 deletions docs/flowchart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# bouquet workflow & logic map

Two layers, one page (`index.html`, fully self-contained):

1. **The physics workflow** (`physics_workflow.svg` / `.pdf`) — the
user-facing diagram: only physics quantities and equilibrium/profile
variables, complete across the perturbation, matching, and solve steps.
Embedded in the top-level README; the PDF is paper-figure quality.
2. **The full logic map** (`l1_full.svg` + interactive pan/zoom/search) —
every config knob, decision gate, and stored artifact (550+ nodes), each
with a `file:line` anchor.

- **View**: <https://d-burg.github.io/bouquet/flowchart/> (GitHub Pages), or
open `index.html` locally.
- **Regenerate everything**:

```bash
python docs/flowchart/build.py # needs graphviz `dot` on PATH
```

## Editing the physics workflow (new diagnostics / data sources)

The physics diagram is hand-authored DATA in `build.py`: `PHYS_NODES` (one
dict per box: `id`, `cluster`, `kind`, `lines`) and `PHYS_EDGES` (one tuple
per arrow). To add e.g. MSE q-profile constraints: add one node dict to the
`inputs` cluster, one edge tuple, rerun `build.py` — there is a commented
MSE example in place. Labels are graphviz HTML-like markup; use the
`V("n","e")` helper for italic variables with subscripts.

## Editing the full logic map

Source of truth is `graph.json` (extracted from the code by a fleet of
agents, adversarially spot-verified, then hand-patched). Edit it (keep
`file:line` anchors honest) and rerun `build.py`. It is a descriptive
snapshot of the commit stamped in the page footer — regenerate (or
re-extract) after significant refactors.

The map is a descriptive snapshot of the commit stamped in the page footer —
it can drift from the code across refactors; regenerate (or re-extract) when
it does.
Loading
Loading