Problem Statement
PLEQUE has no command-line interface. Every interaction with an equilibrium — however trivial —
requires writing Python: importing the right reader, knowing which of several reader entry points
applies to a given source, constructing an Equilibrium, and then knowing which properties answer
the question at hand.
This makes routine work disproportionately expensive:
- Answering "what grid is this g-file on?", "is this equilibrium limited or diverted?", or "where
is the X-point?" takes a scratch script rather than a command.
- Converting an equilibrium between sources and G-EQDSK requires knowing that the writer lives in
a different module from the reader, and that its defaults resample the grid.
- Looking at an equilibrium requires knowing which plotting helper exists and that none of them
save a figure.
- PLEQUE's configuration system exists but is invisible: a user cannot see which configuration file
won, or whether one was found at all, without importing the settings module and inspecting it.
There is also no way to use PLEQUE from a shell pipeline or a job script at all — nothing in the
package emits machine-readable output.
Solution
A pleque command that loads an equilibrium from any of the sources PLEQUE already reads, and then
inspects, plots, or exports it. The grammar names the source first and the action second, so the
command reads the way the task is described: take this equilibrium, and do this to it.
pleque SOURCE IDENTITY [SOURCE_OPTIONS] [ACTION [ACTION_ARGS/OPTIONS]]
pleque geqdsk input.gfile # concise summary
pleque geqdsk input.gfile plot # interactive overview
pleque geqdsk input.gfile plot geometry -o geometry.png # saved, headless
pleque cdb 17854 --time 1.06 info --format json # machine-readable
pleque cdb latest --time 1060 --time-unit ms plot
pleque cudb 6400 --time 2.0 export geqdsk out.gfile --nx 129 --ny 129
pleque config # what configuration is in effect
Four sources (geqdsk, cdb, cudb, jet), three actions (info, plot, export), plus a
top-level config utility. The action may be omitted, in which case the summary is printed.
The CLI is a thin, honest layer over the existing readers, writers and plotting helpers. It adds no
physics. Where the underlying library behaves surprisingly — silently resampling on export,
silently selecting a distant time slice, fabricating a first wall when the source has none — the
CLI's job is to make that visible, not to hide it or to change it.
User Stories
Loading an equilibrium
- As a plasma physicist, I want to load a G-EQDSK file by path, so that I can work with the format
I exchange with collaborators without writing a reader call.
- As a COMPASS user, I want to load an equilibrium from CDB by shot number, so that I can inspect
experimental equilibria directly from the database.
- As a COMPASS-U user, I want to load an equilibrium from CUDB by shot number, so that I can
inspect equilibria from the new device.
- As a JET user, I want to load an equilibrium from JET SAL by pulse number, so that I can inspect
JET equilibria with the same command I use elsewhere.
- As a database user, I want to name a time explicitly, so that I get the slice I asked for rather
than whatever the reader's historical default happened to be.
- As a database user, I want to give that time in seconds by default and in milliseconds when I
say so, so that I can use whichever unit my notes are in without mental arithmetic.
- As a database user, I want to be told when no time was given and none is configured, so that I
never silently receive a reader's hard-coded default time.
- As a database user, I want to be told the time actually selected whenever it differs from the
time I asked for, so that a nearest-slice match can never masquerade as an exact one.
- As a careful user, I want an option to make a distant time match an error rather than a notice,
so that batch jobs fail loudly instead of producing quietly wrong results.
- As a COMPASS user, I want to ask for the most recent shot with an explicit word, so that I get
the newest data without ambiguity about whether omitting the shot meant "latest".
- As a user of a source that has no "latest" concept, I want that request rejected with a clear
message, so that I am not left wondering whether it silently did something else.
- As a user without the COMPASS or JET client libraries installed, I want a clear message naming
what to install, so that a missing optional backend is distinguishable from a broken command.
- As a user without those libraries, I want
--help and the other sources to keep working, so
that one unavailable backend does not disable the whole tool.
Inspecting an equilibrium
- As a plasma physicist, I want a concise summary printed when I name a source and no action, so
that the shortest possible command answers the most common question.
- As a plasma physicist, I want the summary to show the magnetic axis, X-points, strike points and
contact point, so that I can see the equilibrium's topology at a glance.
- As a plasma physicist, I want to see whether the plasma is limited or diverted stated in words,
so that I do not have to infer it from the presence of an X-point.
- As a plasma physicist, I want to see the computational grid's size and extent, so that I know
the resolution I am working with before I compute anything.
- As a plasma physicist, I want to see the COCOS convention in effect, so that I can interpret
signs correctly.
- As a plasma physicist, I want to be told when the first wall was fabricated by PLEQUE rather
than read from the source, so that I never mistake a synthetic rectangle for the real machine.
- As a user, I want the summary to be fast and to never fail on a valid equilibrium, so that I can
use it freely on anything that loads.
- As a user who wants derived quantities, I want an explicit flag for them, so that I opt into the
computational cost knowingly rather than paying it on every invocation.
- As a script author, I want machine-readable output on request, so that I can drive downstream
tooling without parsing human prose.
- As a script author, I want the machine-readable shape to be stable and versioned, so that I can
depend on it without re-checking after every release.
- As a script author, I want unavailable values present and explicitly null rather than omitted,
so that my consumer sees one shape regardless of which source produced it.
- As a script author, I want machine-readable output to be exactly what it claims to be, never
reflowed or decorated for a terminal, so that piping it never corrupts it.
- As a shell user, I want data on standard output and diagnostics on standard error, so that
redirecting output captures only the data.
Plotting
- As a plasma physicist, I want to open an interactive plot of an equilibrium, so that I can look
at it without writing plotting code.
- As a plasma physicist, I want to choose between the overview plot and the geometry plot, so that
I can see either the flux surfaces or the COCOS-convention directions.
- As a user on a headless machine, I want a plot saved to a file without any window being opened,
so that plotting works over SSH and in batch jobs.
- As a user saving a plot, I want the image format taken from the filename I chose, so that I do
not have to state it twice.
- As a user preparing a figure, I want to control the output resolution, so that the image is
usable in a document.
Exporting
- As a plasma physicist, I want to write an equilibrium out as G-EQDSK, so that I can hand it to
codes that only read that format.
- As a user exporting, I want to name the output format explicitly, so that the file I get never
depends on how I happened to name the file.
- As a user exporting, I want to be told the grid the file was actually written on, so that I
notice when the output is coarser than the input.
- As a user exporting, I want control over the output grid and boundary resolution, so that I can
produce a faithful round trip when I need one.
- As a user exporting, I want to set the label written into the file header, so that the
provenance of a generated file is recorded in the file itself.
- As a user exporting, I want to be stopped before overwriting an existing file, so that a
mistyped filename cannot destroy data.
- As a user who does mean to overwrite, I want a flag that permits it, so that regenerating an
output is a single command.
Configuration
- As a repeat user of one machine's database, I want to configure that source's default time and
connection parameters, so that I do not retype them on every invocation.
- As a user, I want command-line options to beat environment variables, which beat configuration
files, which beat built-in defaults, so that precedence is predictable.
- As a user, I want to see which configuration file is in effect and which locations were checked,
so that I can diagnose a setting that did not apply.
- As a user, I want to see the resolved source settings, so that I can confirm what the tool will
actually do before running a long job.
- As a library user who never touches the CLI, I want the new configuration to be documented
alongside the rest, so that the settings reference stays complete.
Errors and diagnostics
- As a script author, I want distinct exit codes for usage errors, data errors and missing
optional dependencies, so that automation can branch on the failure kind.
- As a user, I want ordinary failures reported as a clear message without a stack trace, so that
the output is readable.
- As a contributor debugging a problem, I want a flag that restores the full stack trace, so that
I can diagnose an unexpected failure.
- As a user, I want the tool quiet by default, so that its output is not mixed with the library's
informational logging.
- As a user diagnosing something, I want progressively more verbose logging on request, so that I
can see what the library is doing.
- As a user running quietly, I want correctness-critical notices — such as a time slice differing
from the one I requested — to survive, so that silencing chatter cannot silence a warning that
changes the meaning of the result.
Library users
- As a library user, I want the equilibrium's grid dimensions, flux values at the axis and
boundary, and plasma-type available as public API, so that I do not have to reach into private
attributes as the CLI would otherwise have to.
- As an existing library user, I want the currently misspelled plasma-type property to keep
working, so that my code does not break when a correctly spelled one is added.
- As a packager, I want the version reported by the command and the version in the packaging
metadata to be the same number, so that a released artifact is unambiguous.
Implementation Decisions
Command grammar
- Source first, then exactly one terminal action per invocation. Omitting the action prints the
same summary as info.
plot takes an optional positional kind (overview | geometry), defaulting to overview,
modelled as a typed enumeration so that validation and completion are generated rather than
hand-written. A COCOS-geometry kind is excluded — the corresponding library helper is a
non-functional stub.
export is a subcommand group naming the format: export geqdsk OUT. This supersedes the
earlier design decision to infer the format from the output file suffix. There is no --format
option on export and the suffix is never examined. Inference was dropped because there is no
defensible suffix list for G-EQDSK — the format ships as .gfile, .eqdsk, gNNNNN.NNNN, or
with no suffix at all, and most of the repository's own bundled equilibria have no recognizable
suffix.
- Each export format declares its own option set. An option that does not apply to the chosen
format is a usage error, not a silently ignored no-op. This is why export is a subcommand group
rather than a flat command: the G-EQDSK grid and label options are that format's vocabulary, not
export's.
- The "most recent shot" selector is a positional literal (
latest) occupying the identity slot,
accepted only by the source whose backend supports it. Omitting the identity never means latest.
The other database sources reject it as a usage error.
Source options and time handling
- Uniform rule: every non-identity reader parameter is exposed both as a command-line option and as
a configuration key. The identity (path, shot, pulse) stays positional and is never configurable.
- Time follows its own precedence: command line, then configured per-source default, then error.
It never falls back to a reader's historical default value. This matters concretely — one reader
defaults to a specific millisecond time and another to a specific second time, and both of those
defaults must be unreachable through the CLI.
- Time is normalised to seconds internally. One reader accepts only milliseconds and has no unit
parameter, so the CLI converts for that reader specifically. Another reader's "no time given"
value means "return a time-slice collection" rather than a single equilibrium, so it must never
be passed through — a source resolves to exactly one equilibrium snapshot.
- The G-EQDSK reader hard-codes a COCOS value rather than consulting PLEQUE's configured default.
The CLI passes the configured value instead. This is a deliberate divergence: the alternative
leaves the configured default dead for the most common source.
- No time tolerance is applied by default, matching the Python API. Instead the actually-selected
time is always surfaced. An opt-in tolerance turns a distant match into an error.
- All optional backend imports are deferred to call time. One reader module imports its backend at
module scope and therefore cannot be imported at all without it, so the CLI must not import it
during startup or help rendering.
Summary output
An explicit, versioned schema shared by the human and machine-readable renderings. Unavailable
values are present and null, never omitted, so consumers see one shape across sources. Every
default field is already computed during equilibrium construction — the summary triggers no
contouring, no surface averaging and no flux-surface tracing, and cannot fail on an equilibrium
that loaded successfully.
schema_version int
source geqdsk | cdb | cudb | jet
source_identifier path or shot, as given
shot int | null
time_requested_s float | null
time_s float | null (actual)
cocos int
plasma_type limiter | diverted
grid.nr / grid.nz int
grid.r_min / r_max / z_min / z_max float
psi_axis / psi_lcfs float
magnetic_axis {r, z}
primary_x_point {r, z} | null
secondary_x_point {r, z} | null
contact_point {r, z} | null
strike_points [{r, z}, ...]
first_wall.n_points int
first_wall.synthetic bool
Provenance (source, source_identifier, shot, time_requested_s) comes from a record the CLI's
loader carries alongside the equilibrium, not from the equilibrium object — which records no
source, no path and no reader name, and reports sentinel values for shot and time when loaded from
a g-file. The actual time is read back from the equilibrium and normalised by its own unit,
rendering as null when it is the sentinel.
An extended mode adds LCFS geometry (area, volume, minor radius, elongation, triangularity) and
q95. Three quantities are excluded from every mode, each for a specific reason:
- Plasma current — the library computes it from a squared flux gradient, so its sign is fixed
by the COCOS convention rather than by the plasma. Reporting it in an inspection tool would
present a convention artifact as a measurement.
- q on axis — the q spline is fitted from a small non-zero normalised flux upward, so the
on-axis value is an extrapolation past the fitted domain.
- Separatrix — its search loop raises on failure after a bounded number of attempts, which
would let the summary die on an otherwise valid equilibrium.
Documented stability contract: fields may be added within a schema version, never removed or
retyped.
Configuration
A typed per-source configuration section is added to PLEQUE's existing settings model, with one
sub-model per source carrying its default time, time unit, and non-identity reader parameters. A
typed field is mandatory rather than stylistic — the settings model forbids extra keys, so a loose
table would raise a validation error on load. Environment-variable access follows from the existing
nested-delimiter convention automatically.
A top-level config command reports the winning configuration file, all locations consulted, and
the resolved per-source values, built on accessors the settings model already exposes. It sits
outside the three-action vocabulary deliberately: configuration files are not merged — the first
existing file wins and the rest are ignored — and without a way to see which one won, that rule is
invisible.
Output, errors and logging
- Data to standard output, diagnostics to standard error.
- Rich rendering applies to human output only. Machine-readable output is serialised directly to
standard output, bypassing the renderer entirely, because terminal-width reflowing would corrupt
long lines into invalid output. Colour needs no dedicated flag — the renderer already disables it
for non-terminal output and honours the conventional environment variable.
- Exit codes: success; unexpected error (stack trace suppressed unless requested); usage error
(the CLI framework's own default, left unchanged); source or data error; missing optional
dependency, with a message naming the install command.
- Exception mapping happens at the CLI boundary only. No exception hierarchy is introduced into
the library — it currently defines none, raising built-ins and third-party types throughout.
Each source wrapper catches the specific failures it knows about; an unrecognised error is
treated as a bug and gets the unexpected-error code and a stack trace, rather than being
misreported as a data error.
- Verbosity resets after importing the library, which configures logging at import time. The
actual-selected-time notice goes through the CLI's own diagnostic writer rather than the library
logger, so that quiet mode cannot suppress a notice that changes the meaning of the result.
Plotting
Interactive when no output file is given; when one is, a non-interactive backend is selected and
the figure is saved without a window appearing. No library plotting helper saves a figure or forces
a backend today, and one of them imports the plotting stack at module scope — so the CLI must not
import that module during startup, and backend selection must precede the first figure. Image
format comes from the chosen filename.
Export
Calls the module-level G-EQDSK writer rather than the equilibrium's convenience method, because the
latter does not expose the boundary-point count or the file label. Library resampling defaults are
retained rather than overridden — output should not depend on which source it came from — but the
resolved output grid is reported as a diagnostic, so downsampling is never silent. An existing
output file is not overwritten without an explicit flag; the library writer truncates silently, and
this is the one place the tool can destroy user data.
Library additions
Small and additive, required because the CLI must not read private state:
- Grid dimensions, flux at the magnetic axis and at the boundary, exposed as public read-only
properties. The boundary flux value is inconsistently typed internally — a float on one path and
a zero-dimensional array on another — and is normalised on the way out.
- A correctly spelled plasma-type property. The existing misspelled one keeps working.
- A record of whether the first wall was read from the source or synthesised, since the library
fabricates a rectangular wall when the source provides none and currently leaves no trace.
Packaging: the CLI framework becomes a first-class dependency and the console entry point is always
installed — the package already depends on considerably heavier libraries, and an optional extra
would leave a command that exists but fails. The packaging version and the package's own version
constant, which currently disagree, are unified onto a single source of truth.
Testing Decisions
A good test here drives the tool the way a user does and asserts only on what a user can observe:
the exit code, what appears on standard output, what appears on standard error, and what files
exist afterwards. Nothing should assert on internal call sequencing, module structure, or the
presence of particular helper functions — all of which are free to change.
Seams. Two, both at the highest available point:
- The assembled command-line application, driven through the framework's test runner. This is
the primary seam and covers argument parsing, configuration resolution, loading, action
dispatch, rendering and exit codes in one pass. Nothing sits above it.
- The existing parametrised equilibrium fixture already used across the test suite, for the
new public accessors. This is an existing seam and is preferred to inventing a new one; it also
exercises the accessors across all bundled equilibria rather than one.
Test doubles, not additional seams. Only one of the four sources can be loaded in continuous
integration: one backend's tests are entirely skipped when its client library is absent, and
another source's module cannot even be imported without its backend. The three backend-dependent
sources are therefore covered by substituting the reader function at the external boundary while
still driving everything through the primary seam. This is a stub at the edge of the system, not a
second place to test from — and it is the only coverage those three sources will ever get in CI.
Coverage at the primary seam (using the bundled equilibria the test suite already ships):
- Naming a source with no action produces the same output as the summary action.
- The machine-readable summary parses, carries its schema version, and presents the full field set
with nulls rather than omissions.
- The extended summary adds the derived fields and excludes the three deliberately-omitted ones.
- Plotting to a file produces a file and opens no window.
- Export followed by a re-read reproduces the equilibrium — mirroring the round-trip assertion the
existing G-EQDSK tests already make.
- Export refuses an existing output file, and proceeds when explicitly permitted.
- Each exit code is produced by a condition that should produce it.
- The configuration command reports the file actually in effect.
Coverage via substituted readers:
- Each source passes the identity and options through as given.
- The millisecond conversion happens for the one reader that needs it and for no other.
- The time precedence chain resolves in the documented order, and produces an error when nothing
supplies a time.
- The "latest" selector reaches the one backend that supports it and is rejected for the others.
- A time-slice-collection value is never passed to a reader.
Coverage at the existing fixture seam:
- Each new accessor returns a sensible value for every bundled equilibrium.
- The boundary flux value is a plain float regardless of construction path.
- The misspelled plasma-type property still works and agrees with the new spelling.
- The synthetic-wall indicator is true for an equilibrium with no wall in its source and false
otherwise.
Prior art. The round-trip export test follows the existing G-EQDSK write-then-read test. The
substituted-reader approach mirrors how the existing database tests are structured, except that
skipping is replaced by substitution so the tests actually run. The parametrised fixture is the one
the existing suite already uses throughout.
Out of Scope
- Transformations of an equilibrium before the terminal action. Conceptually part of the design
but deferred; when added, they should be typed options rather than chained subcommands.
- Any source beyond the four named. In particular the OMAS reader takes an in-memory Python
object rather than a locatable resource, and another reader augments an existing equilibrium
rather than producing one — neither fits the grammar.
- Any export format beyond G-EQDSK. The OMAS writer returns an in-memory structure rather than
writing a file, its dependency is not declared by the project, and no NetCDF or HDF5 writer
exists despite both libraries being dependencies. Adding formats means writing library writers
first; it is not CLI work.
- Export to standard output. The only stream-capable writer is private; supporting this means
either depending on a private module or designing a public streaming API.
- A plugin system, callable configuration, dynamic source registry, or third-party loader
architecture. Sources are hard-coded thin wrappers. A plugin system remains a possible later
and much larger architectural change.
- Any additional serialisation format for the summary beyond the one machine-readable format.
- Mirroring the equilibrium class's Python interface. Array- and function-valued calculations
remain better suited to Python and are not CLI material.
- Fixing the non-functional COCOS-geometry plotting helper, the incomplete COCOS handling in the
equilibrium class, or the documentation's incorrect claim about unknown configuration keys. All
are real defects encountered while designing this, and all deserve their own issues.
Further Notes
Post-v1 order. Transformations first, since their shape is already settled. Then profile and
field sampling — the highest value per line of code, because the coordinate container already
normalises every input form such a command would want to accept. Then validation, which is largely
assembling checks the library already performs during construction. Additional output formats rank
last, for the reason given above.
Defects found while designing this, each worth a separate issue and none of them in scope here:
the COCOS-geometry plotting helper is not merely a stub but contains calls that cannot execute; the
configuration documentation states that unknown keys are silently ignored when in fact the settings
model rejects them; and the debug-plot helper contains a call that appears to be a typo for a
different function, swallowed by a surrounding exception handler.
Deliberate divergence to record. With this change, loading a g-file through the CLI applies the
configured COCOS default while a bare library call does not. This is intentional and documented,
not an oversight — but it is the one place where the CLI is not a transparent wrapper.
Design provenance. This spec is the outcome of a design session recorded in the repository's
CLI design handoff document, which carries the numbered decisions and the reasoning behind each.
That document should be updated alongside implementation to record the decisions taken after it was
written, and to mark the format-inference decision as superseded.
Problem Statement
PLEQUE has no command-line interface. Every interaction with an equilibrium — however trivial —
requires writing Python: importing the right reader, knowing which of several reader entry points
applies to a given source, constructing an
Equilibrium, and then knowing which properties answerthe question at hand.
This makes routine work disproportionately expensive:
is the X-point?" takes a scratch script rather than a command.
a different module from the reader, and that its defaults resample the grid.
save a figure.
won, or whether one was found at all, without importing the settings module and inspecting it.
There is also no way to use PLEQUE from a shell pipeline or a job script at all — nothing in the
package emits machine-readable output.
Solution
A
plequecommand that loads an equilibrium from any of the sources PLEQUE already reads, and theninspects, plots, or exports it. The grammar names the source first and the action second, so the
command reads the way the task is described: take this equilibrium, and do this to it.
Four sources (
geqdsk,cdb,cudb,jet), three actions (info,plot,export), plus atop-level
configutility. The action may be omitted, in which case the summary is printed.The CLI is a thin, honest layer over the existing readers, writers and plotting helpers. It adds no
physics. Where the underlying library behaves surprisingly — silently resampling on export,
silently selecting a distant time slice, fabricating a first wall when the source has none — the
CLI's job is to make that visible, not to hide it or to change it.
User Stories
Loading an equilibrium
I exchange with collaborators without writing a reader call.
experimental equilibria directly from the database.
inspect equilibria from the new device.
JET equilibria with the same command I use elsewhere.
than whatever the reader's historical default happened to be.
say so, so that I can use whichever unit my notes are in without mental arithmetic.
never silently receive a reader's hard-coded default time.
time I asked for, so that a nearest-slice match can never masquerade as an exact one.
so that batch jobs fail loudly instead of producing quietly wrong results.
the newest data without ambiguity about whether omitting the shot meant "latest".
message, so that I am not left wondering whether it silently did something else.
what to install, so that a missing optional backend is distinguishable from a broken command.
--helpand the other sources to keep working, sothat one unavailable backend does not disable the whole tool.
Inspecting an equilibrium
that the shortest possible command answers the most common question.
contact point, so that I can see the equilibrium's topology at a glance.
so that I do not have to infer it from the presence of an X-point.
the resolution I am working with before I compute anything.
signs correctly.
than read from the source, so that I never mistake a synthetic rectangle for the real machine.
use it freely on anything that loads.
computational cost knowingly rather than paying it on every invocation.
tooling without parsing human prose.
depend on it without re-checking after every release.
so that my consumer sees one shape regardless of which source produced it.
reflowed or decorated for a terminal, so that piping it never corrupts it.
redirecting output captures only the data.
Plotting
at it without writing plotting code.
I can see either the flux surfaces or the COCOS-convention directions.
so that plotting works over SSH and in batch jobs.
not have to state it twice.
usable in a document.
Exporting
codes that only read that format.
depends on how I happened to name the file.
notice when the output is coarser than the input.
produce a faithful round trip when I need one.
provenance of a generated file is recorded in the file itself.
mistyped filename cannot destroy data.
output is a single command.
Configuration
connection parameters, so that I do not retype them on every invocation.
files, which beat built-in defaults, so that precedence is predictable.
so that I can diagnose a setting that did not apply.
actually do before running a long job.
alongside the rest, so that the settings reference stays complete.
Errors and diagnostics
optional dependencies, so that automation can branch on the failure kind.
the output is readable.
I can diagnose an unexpected failure.
informational logging.
can see what the library is doing.
from the one I requested — to survive, so that silencing chatter cannot silence a warning that
changes the meaning of the result.
Library users
boundary, and plasma-type available as public API, so that I do not have to reach into private
attributes as the CLI would otherwise have to.
working, so that my code does not break when a correctly spelled one is added.
metadata to be the same number, so that a released artifact is unambiguous.
Implementation Decisions
Command grammar
same summary as
info.plottakes an optional positional kind (overview|geometry), defaulting tooverview,modelled as a typed enumeration so that validation and completion are generated rather than
hand-written. A COCOS-geometry kind is excluded — the corresponding library helper is a
non-functional stub.
exportis a subcommand group naming the format:export geqdsk OUT. This supersedes theearlier design decision to infer the format from the output file suffix. There is no
--formatoption on export and the suffix is never examined. Inference was dropped because there is no
defensible suffix list for G-EQDSK — the format ships as
.gfile,.eqdsk,gNNNNN.NNNN, orwith no suffix at all, and most of the repository's own bundled equilibria have no recognizable
suffix.
format is a usage error, not a silently ignored no-op. This is why export is a subcommand group
rather than a flat command: the G-EQDSK grid and label options are that format's vocabulary, not
export's.
latest) occupying the identity slot,accepted only by the source whose backend supports it. Omitting the identity never means latest.
The other database sources reject it as a usage error.
Source options and time handling
a configuration key. The identity (path, shot, pulse) stays positional and is never configurable.
It never falls back to a reader's historical default value. This matters concretely — one reader
defaults to a specific millisecond time and another to a specific second time, and both of those
defaults must be unreachable through the CLI.
parameter, so the CLI converts for that reader specifically. Another reader's "no time given"
value means "return a time-slice collection" rather than a single equilibrium, so it must never
be passed through — a source resolves to exactly one equilibrium snapshot.
The CLI passes the configured value instead. This is a deliberate divergence: the alternative
leaves the configured default dead for the most common source.
time is always surfaced. An opt-in tolerance turns a distant match into an error.
module scope and therefore cannot be imported at all without it, so the CLI must not import it
during startup or help rendering.
Summary output
An explicit, versioned schema shared by the human and machine-readable renderings. Unavailable
values are present and null, never omitted, so consumers see one shape across sources. Every
default field is already computed during equilibrium construction — the summary triggers no
contouring, no surface averaging and no flux-surface tracing, and cannot fail on an equilibrium
that loaded successfully.
Provenance (
source,source_identifier,shot,time_requested_s) comes from a record the CLI'sloader carries alongside the equilibrium, not from the equilibrium object — which records no
source, no path and no reader name, and reports sentinel values for shot and time when loaded from
a g-file. The actual time is read back from the equilibrium and normalised by its own unit,
rendering as null when it is the sentinel.
An extended mode adds LCFS geometry (area, volume, minor radius, elongation, triangularity) and
q95. Three quantities are excluded from every mode, each for a specific reason:
by the COCOS convention rather than by the plasma. Reporting it in an inspection tool would
present a convention artifact as a measurement.
on-axis value is an extrapolation past the fitted domain.
would let the summary die on an otherwise valid equilibrium.
Documented stability contract: fields may be added within a schema version, never removed or
retyped.
Configuration
A typed per-source configuration section is added to PLEQUE's existing settings model, with one
sub-model per source carrying its default time, time unit, and non-identity reader parameters. A
typed field is mandatory rather than stylistic — the settings model forbids extra keys, so a loose
table would raise a validation error on load. Environment-variable access follows from the existing
nested-delimiter convention automatically.
A top-level
configcommand reports the winning configuration file, all locations consulted, andthe resolved per-source values, built on accessors the settings model already exposes. It sits
outside the three-action vocabulary deliberately: configuration files are not merged — the first
existing file wins and the rest are ignored — and without a way to see which one won, that rule is
invisible.
Output, errors and logging
standard output, bypassing the renderer entirely, because terminal-width reflowing would corrupt
long lines into invalid output. Colour needs no dedicated flag — the renderer already disables it
for non-terminal output and honours the conventional environment variable.
(the CLI framework's own default, left unchanged); source or data error; missing optional
dependency, with a message naming the install command.
the library — it currently defines none, raising built-ins and third-party types throughout.
Each source wrapper catches the specific failures it knows about; an unrecognised error is
treated as a bug and gets the unexpected-error code and a stack trace, rather than being
misreported as a data error.
actual-selected-time notice goes through the CLI's own diagnostic writer rather than the library
logger, so that quiet mode cannot suppress a notice that changes the meaning of the result.
Plotting
Interactive when no output file is given; when one is, a non-interactive backend is selected and
the figure is saved without a window appearing. No library plotting helper saves a figure or forces
a backend today, and one of them imports the plotting stack at module scope — so the CLI must not
import that module during startup, and backend selection must precede the first figure. Image
format comes from the chosen filename.
Export
Calls the module-level G-EQDSK writer rather than the equilibrium's convenience method, because the
latter does not expose the boundary-point count or the file label. Library resampling defaults are
retained rather than overridden — output should not depend on which source it came from — but the
resolved output grid is reported as a diagnostic, so downsampling is never silent. An existing
output file is not overwritten without an explicit flag; the library writer truncates silently, and
this is the one place the tool can destroy user data.
Library additions
Small and additive, required because the CLI must not read private state:
properties. The boundary flux value is inconsistently typed internally — a float on one path and
a zero-dimensional array on another — and is normalised on the way out.
fabricates a rectangular wall when the source provides none and currently leaves no trace.
Packaging: the CLI framework becomes a first-class dependency and the console entry point is always
installed — the package already depends on considerably heavier libraries, and an optional extra
would leave a command that exists but fails. The packaging version and the package's own version
constant, which currently disagree, are unified onto a single source of truth.
Testing Decisions
A good test here drives the tool the way a user does and asserts only on what a user can observe:
the exit code, what appears on standard output, what appears on standard error, and what files
exist afterwards. Nothing should assert on internal call sequencing, module structure, or the
presence of particular helper functions — all of which are free to change.
Seams. Two, both at the highest available point:
the primary seam and covers argument parsing, configuration resolution, loading, action
dispatch, rendering and exit codes in one pass. Nothing sits above it.
new public accessors. This is an existing seam and is preferred to inventing a new one; it also
exercises the accessors across all bundled equilibria rather than one.
Test doubles, not additional seams. Only one of the four sources can be loaded in continuous
integration: one backend's tests are entirely skipped when its client library is absent, and
another source's module cannot even be imported without its backend. The three backend-dependent
sources are therefore covered by substituting the reader function at the external boundary while
still driving everything through the primary seam. This is a stub at the edge of the system, not a
second place to test from — and it is the only coverage those three sources will ever get in CI.
Coverage at the primary seam (using the bundled equilibria the test suite already ships):
with nulls rather than omissions.
existing G-EQDSK tests already make.
Coverage via substituted readers:
supplies a time.
Coverage at the existing fixture seam:
otherwise.
Prior art. The round-trip export test follows the existing G-EQDSK write-then-read test. The
substituted-reader approach mirrors how the existing database tests are structured, except that
skipping is replaced by substitution so the tests actually run. The parametrised fixture is the one
the existing suite already uses throughout.
Out of Scope
but deferred; when added, they should be typed options rather than chained subcommands.
object rather than a locatable resource, and another reader augments an existing equilibrium
rather than producing one — neither fits the grammar.
writing a file, its dependency is not declared by the project, and no NetCDF or HDF5 writer
exists despite both libraries being dependencies. Adding formats means writing library writers
first; it is not CLI work.
either depending on a private module or designing a public streaming API.
architecture. Sources are hard-coded thin wrappers. A plugin system remains a possible later
and much larger architectural change.
remain better suited to Python and are not CLI material.
equilibrium class, or the documentation's incorrect claim about unknown configuration keys. All
are real defects encountered while designing this, and all deserve their own issues.
Further Notes
Post-v1 order. Transformations first, since their shape is already settled. Then profile and
field sampling — the highest value per line of code, because the coordinate container already
normalises every input form such a command would want to accept. Then validation, which is largely
assembling checks the library already performs during construction. Additional output formats rank
last, for the reason given above.
Defects found while designing this, each worth a separate issue and none of them in scope here:
the COCOS-geometry plotting helper is not merely a stub but contains calls that cannot execute; the
configuration documentation states that unknown keys are silently ignored when in fact the settings
model rejects them; and the debug-plot helper contains a call that appears to be a typo for a
different function, swallowed by a surrounding exception handler.
Deliberate divergence to record. With this change, loading a g-file through the CLI applies the
configured COCOS default while a bare library call does not. This is intentional and documented,
not an oversight — but it is the one place where the CLI is not a transparent wrapper.
Design provenance. This spec is the outcome of a design session recorded in the repository's
CLI design handoff document, which carries the numbered decisions and the reasoning behind each.
That document should be updated alongside implementation to record the decisions taken after it was
written, and to mark the format-inference decision as superseded.