Skip to content
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
!docs/resources/*.pdf
*.png
!docs/src/assets/**/*.png
# committed documentation figures (generated by docs/src/figures/**/make_*.jl)
!docs/src/figures/**/*.png
*.jld2
.gitattributes
Manifest.toml
# the docs figure-provenance manifest is not a Julia Manifest.toml (case-insensitive FS collides)
!docs/src/figures/manifest.toml
*.log
docs/build/
anaconda_projects/
Expand Down
6 changes: 2 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
Expand Down Expand Up @@ -57,14 +58,11 @@ QuadGK = "2.11.3"
Roots = "2.2.13"
SparseArrays = "1"
SpecialFunctions = "2.5.1"
Random = "1"
StaticArrays = "1.9.15"
Statistics = "1"
TOML = "1"
Test = "1"
julia = "1.11"

[extras]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[targets]
test = ["Random"]
126 changes: 126 additions & 0 deletions docs/DOC_STANDARD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# GPEC documentation standard

This is the authoring standard for the GPEC user documentation under
`docs/src/`. The goal is that each physics module page reads like a compact
*Journal of Computational Physics* methods paper: the governing equations, the
numerical method, and the validation that the method works — with figures whose
provenance is explicit. Those module pages **are** built by Documenter and
published on the public documentation site.

This standard file (`docs/DOC_STANDARD.md`) is itself the one exception: it lives
outside `docs/src/` and is not listed in `make.jl`, so Documenter does not
render it to the website. It is a contributor-facing reference, read here in the
repository — which is why `developer_notes.md` links to it by GitHub URL rather
than as a site-internal page.

The `Ballooning and Mercier Local Stability` and `Inner Layer Module` pages are
the reference exemplars; new or reworked module pages should follow their shape.

## Module-page template

A module reference page (`docs/src/<module>.md`) has these sections, in order.
Not every section is large for every module, but the skeleton is the same.

1. **Overview** — what the module computes and where it sits in the
equilibrium → stability → perturbed-equilibrium pipeline.
2. **Governing equations** — the physics the module discretizes, written as
numbered display equations. Each equation or block cites the paper it comes
from (the PDFs in `docs/resources/`), e.g. *(GWP2016 Eq. 11)*. Citing
equation numbers is required, not optional — it is what makes the
implementation auditable against theory.
3. **Numerical method** — the discretization and algorithm: what is expanded in
what basis, how the linear/eigenvalue/BVP problem is assembled and solved,
and the *design choices* that matter (why this contour, this ordering, this
precision). This is the part that distinguishes a methods page from an API
dump.
4. **Validation & benchmarks** — evidence the method is correct: a table of
pinned values cross-checked against an independent code or analytic result,
and figures (see below) showing convergence, invariance, or agreement.
5. **Practical usage** — the configuration keys and the public entry points a
user calls, with a short worked example.
6. **API Reference** — an `@autodocs` block for the module so every exported
docstring is rendered and `checkdocs=:exports` stays satisfied.

## Figures

### Where figures live

- **Content figures** (anything shown on a page) live in
`docs/src/figures/<module>/`. Each committed `<name>.png` sits next to the
script `make_<name>.jl` that produced it — script and figure are always
committed together, so it is always clear how a figure was made.
- `docs/src/assets/` is reserved for **Documenter chrome only** (the site logo,
themes, custom CSS). Content figures never go there.
- The shared helper `docs/figure_tools.jl` (outside `src/`, so it is not
published) provides `save_doc_figure`, provenance stamping, the manifest, and
the `step_series` spectrum helper. Every `make_<name>.jl` `include`s it.

### Where a figure's data comes from

Documentation figures get their **own** generator scripts under
`docs/src/figures/`. They are not forced to route through `benchmarks/`. When a
figure genuinely *is* a benchmark or Fortran cross-check result, its
`make_<name>.jl` may call the relevant `benchmarks/` script, and its `depends`
list records that coupling — but the committed PNG and a runnable script still
live together under `docs/src/figures/<module>/`.

`benchmarks/` remains the home of performance comparisons and Fortran
cross-checks (its outputs are git-ignored and may be heavy). `docs/src/figures/`
is the home of the small, purpose-built, committed figures that illustrate a
page.

### Figures do not build with the docs

Figure scripts run **manually**, in the root project:

```bash
julia --project=. docs/src/figures/<module>/make_<name>.jl
```

They are **never** run at Documenter build time. The build only embeds the
committed PNGs, so `docs/Project.toml` stays minimal (no `Plots`) and the build
stays fast. This is also why we do not regenerate every figure on every docs
change — see the regeneration policy below.

### Provenance

`save_doc_figure` stamps a small `GPEC <shorthash> · <date>` mark in the corner
of every figure (so a figure's age is visible when browsing the rendered site)
and writes a machine-readable entry to `docs/src/figures/manifest.toml`:

```toml
[inner_layer.rotated_ray_contour]
script = "make_rotated_ray_contour.jl"
commit = "3a0837e3"
date = "2026-07-08"
depends = ["src/InnerLayer/GGJ/Ray.jl"]
```

`depends` lists the repo-relative source files whose *numbers* the figure
visualizes. Generate figures as the last step before committing a change; the
stamp then reflects the state they were made against (a `-dirty` suffix marks a
figure generated with an uncommitted working tree, which is honest, not an
error).

### When to regenerate a figure

Regenerate a figure (and its manifest entry) only when:

1. a file in its `depends` list changed the numbers it shows — the regression
harness is the trigger: if a tracked quantity for that module moved, its
figures are suspect;
2. the physics or method the figure illustrates changed; or
3. the figure script itself changed.

Do **not** regenerate for prose edits, formatting, or changes to unrelated
modules. Because each figure carries its generating commit and a `depends`
list, a reviewer (or a future `docs/check_figures.jl`) can tell when a figure
predates the last change to a file it depends on and is therefore stale.

### Legacy figures

A figure migrated from before this system, whose original generator was not
preserved and which cannot be faithfully reproduced from current code, is
registered with `register_legacy_figure` (`commit = "legacy"`, a `note`
explaining why). This is honest bookkeeping for genuinely irreproducible
figures — it is never a substitute for writing a generator for a new figure.
166 changes: 166 additions & 0 deletions docs/figure_tools.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# figure_tools.jl
#
# Shared helpers for GPEC documentation figures. Every figure generator
# (`docs/src/figures/<module>/make_<name>.jl`) `include`s this file and calls
# `save_doc_figure` to stamp, save, and register its output. See
# `docs/DOC_STANDARD.md` for the figure-organization and provenance policy.
#
# This file lives OUTSIDE `docs/src/` so Documenter does not publish it. The
# `make_<name>.jl` scripts do live under `docs/src/figures/` (co-located with
# their PNGs) and are published alongside the figures for reproducibility.
#
# Figure scripts run MANUALLY in the root project, never at docs-build time:
# julia --project=. docs/src/figures/<module>/make_<name>.jl
# so the Documenter build stays fast and Plots-free.

using Plots
using Dates
using TOML

const _DOCS_DIR = @__DIR__ # .../docs
const _REPO_ROOT = normpath(joinpath(_DOCS_DIR, "..")) # repo root
const FIGURES_ROOT = joinpath(_DOCS_DIR, "src", "figures") # docs/src/figures
const MANIFEST_PATH = joinpath(FIGURES_ROOT, "manifest.toml")

"""
figure_provenance() -> (; commit, date, dirty)

Short git commit and ISO date stamped onto every figure. `commit` is the
`git rev-parse --short HEAD` hash (reusing the idiom from
`benchmarks/benchmark_git_branches.jl`), with a `-dirty` suffix when the
working tree has uncommitted changes so a figure never claims a cleaner
provenance than it has. Falls back to `"unknown"` outside a git checkout.
"""
function figure_provenance()
date = string(Dates.today())
try
h = strip(read(`git -C $(_REPO_ROOT) rev-parse --short HEAD`, String))
dirty = !isempty(strip(read(`git -C $(_REPO_ROOT) status --porcelain`, String)))
return (; commit=(dirty ? "$h-dirty" : h), date=date, dirty=dirty)
catch
return (; commit="unknown", date=date, dirty=false)
end
end

"""
stamp!(p; commit, date, subplot=length(p.subplots))

Annotate a small `GPEC <commit> · <date>` provenance mark in the bottom-right
corner of subplot `subplot` (default: the last panel), so the figure's age is
visible when browsing the rendered docs. Works on linear or log axes
(positions in data coordinates). Never throws: a plotting hiccup warns and
leaves the figure unstamped rather than losing the plot.
"""
function stamp!(p::Plots.Plot; commit::AbstractString, date::AbstractString,
subplot::Int=length(p.subplots))
try
sp = p[subplot]
xl = Plots.xlims(sp)
yl = Plots.ylims(sp)
xpos = xl[2] # right edge (data coords)
ypos = yl[1] # bottom edge
txt = Plots.text("GPEC $(commit) · $(date)", 6, RGBA(0.5, 0.5, 0.5, 0.9),
:right, :bottom)
annotate!(sp, xpos, ypos, txt)
catch err
@warn "stamp! failed; saving figure without provenance mark" exception = err
end
return p
end

"""
save_doc_figure(p, mod, name; script, depends, npx...) -> String

Stamp `p` with the current git provenance, save it to
`docs/src/figures/<mod>/<name>.png`, and upsert its `manifest.toml` entry
(`script`, `commit`, `date`, `depends`). `depends` lists the repo-relative
source files whose numbers the figure visualizes — the regeneration policy in
`docs/DOC_STANDARD.md` keys off them. Returns the absolute PNG path (printed
so it can be opened directly). Extra keyword args are forwarded to `savefig`.
"""
function save_doc_figure(p::Plots.Plot, mod::AbstractString, name::AbstractString;
script::AbstractString, depends::AbstractVector{<:AbstractString}=String[])
prov = figure_provenance()
stamp!(p; commit=prov.commit, date=prov.date)

outdir = joinpath(FIGURES_ROOT, mod)
mkpath(outdir)
outpath = joinpath(outdir, name * ".png")
savefig(p, outpath)

_update_manifest!(mod, name; script=String(script), commit=prov.commit,
date=prov.date, depends=String.(depends))

println("Saved doc figure: $(abspath(outpath))")
return abspath(outpath)
end

# Load / merge-write the figures manifest, keyed [<mod>.<name>]. Kept sorted and
# human-diffable; one entry per committed figure.
function _load_manifest()
return isfile(MANIFEST_PATH) ? TOML.parsefile(MANIFEST_PATH) : Dict{String,Any}()
end

function _write_manifest(manifest)
open(MANIFEST_PATH, "w") do io
println(io, "# GPEC documentation-figure provenance manifest.")
println(io, "# One [<module>.<figure>] entry per committed PNG under docs/src/figures/.")
println(io, "# Regenerate a figure (and this entry) only when a file in its `depends`")
println(io, "# list changes the numbers it shows — see docs/DOC_STANDARD.md.")
println(io)
TOML.print(io, manifest; sorted=true)
end
return manifest
end

function _update_manifest!(mod::AbstractString, name::AbstractString;
script::String, commit::String, date::String, depends::Vector{String})
manifest = _load_manifest()
modtbl = get!(manifest, mod, Dict{String,Any}())
modtbl[name] = Dict{String,Any}(
"script" => script,
"commit" => commit,
"date" => date,
"depends" => depends
)
return _write_manifest(manifest)
end

"""
register_legacy_figure(mod, name; note, depends=String[])

Record a manifest entry for a figure that predates this provenance system and
whose original generator script was not preserved (`commit = "legacy"`). Use
this only for migrated figures that cannot be faithfully reproduced from the
current code — never to skip writing a generator for a new figure. `note`
states why the figure is legacy (e.g. "compared the since-removed Mercier.jl").
"""
function register_legacy_figure(mod::AbstractString, name::AbstractString;
note::AbstractString, depends::AbstractVector{<:AbstractString}=String[])
manifest = _load_manifest()
modtbl = get!(manifest, mod, Dict{String,Any}())
modtbl[name] = Dict{String,Any}(
"script" => "(not preserved)",
"commit" => "legacy",
"date" => "unknown",
"depends" => String.(depends),
"note" => String(note)
)
_write_manifest(manifest)
println("Registered legacy figure: $(mod).$(name)")
return manifest
end

"""
step_series(m_vals, amps) -> (m_ext, amp_ext)

Canonical spectrum-plot helper (CLAUDE.md plotting convention): pad a zero on
each end of a discrete-mode series so a `seriestype=:steppre` plot draws clean
boxes that fall to zero at the edges. Centralized here so doc figures and
benchmarks share one definition.
"""
function step_series(m_vals, amps)
m_ext = [m_vals[1] - 1; m_vals; m_vals[end] + 1]
amp_ext = [0.0; amps; 0.0]
return m_ext, amp_ext
end
4 changes: 2 additions & 2 deletions docs/src/ballooning.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ numerically fragile. The comparison below shows that the `Baloo.f` style, which
sets the displacement to vanish at ``\pm\theta_{\max}`` and computes ``\Delta'``
from the resulting solution, gives the most stable continuous profile.

![Comparison of numerical stability among methods for extracting ``\Delta'`` and ``c_{a1}``.](assets/ballooning/delta_prime_matching.png)
![Comparison of numerical stability among methods for extracting ``\Delta'`` and ``c_{a1}``.](figures/ballooning/delta_prime_matching.png)

`Bal.jl` therefore adopts the `Baloo.f`-style boundary replacement for the
ballooning ``\Delta'`` calculation.
Expand Down Expand Up @@ -905,7 +905,7 @@ The previous standalone `Mercier.jl` path can therefore be removed from the loca
stability calculation. The comparison below shows agreement between the previous
Mercier calculation and the new `Bal.jl` calculation.

![Comparison of Mercier criterion calculations from `Mercier.jl` and `Bal.jl`.](assets/ballooning/mercier_comparison.png)
![Comparison of Mercier criterion calculations from `Mercier.jl` and `Bal.jl`.](figures/ballooning/mercier_comparison.png)

The local-stability output now stores ballooning ``\Delta'`` in the fourth
`locstab_fs` entry. In the HDF5 output this is written as
Expand Down
2 changes: 1 addition & 1 deletion docs/src/citations.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Provides an efficient algorithm for computing the full Δ' matrix (coupling betw
> *Physics of Plasmas* **27**, 012506 (2020).
> DOI: [10.1063/1.5134999](https://doi.org/10.1063/1.5134999)

Showcases a different basis that significantly aids convergence of the Galerkin solver used in the GGJ InnerLayer module.
Constructs the Wasow large-``x`` asymptotic basis (the `inps` kernel) that supplies the far-field boundary condition for every GGJ InnerLayer backend — the Galerkin solver and the rotated-contour collocation (`:ray`) solver alike.

---

Expand Down
12 changes: 12 additions & 0 deletions docs/src/developer_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ CODE - TAG - Detailed message

Where CODE is the module name (EQUIL, ForceFreeStates, VAC, PERTURBED EQUILIBRIUM, etc.) and TAG describes the type of change (WIP, MINOR, IMPROVEMENT, BUG FIX, NEW FEATURE, REFACTOR, CLEANUP, etc.). This format is used for compiling release notes — tags should be human-readable but are not enforced to a fixed set.

## Documentation standard

Module reference pages follow a shared *Journal of Computational Physics*-style
structure (governing equations → numerical method → validation figures → API),
and every documentation figure is committed together with the script that made
it and a provenance stamp. The full policy — page template, figure
organization under `docs/src/figures/<module>/`, provenance, and the
regenerate-only-when-`depends`-change rule — is in
[`docs/DOC_STANDARD.md`](https://github.com/OpenFUSIONToolkit/GPEC/blob/develop/docs/DOC_STANDARD.md).
The `Ballooning and Mercier Local Stability` and `Inner Layer Module` pages are
the reference exemplars.

## Regression Testing

The regression harness **must be run on every pull request before merging into `develop`**. It is the project's primary safeguard for tracking how numerical results evolve across changes, so it is only useful if every PR exercises it. When you open a PR, paste the regression report into the PR thread so reviewers can see what moved (and what did not). If your change touches a quantity that is not yet tracked, add a new regression case — or extend an existing one — in the same PR.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading