Anatomy of a scientific module

Most Quantas modules use the same conceptual parts even when their exact lifecycle differs. HA is a compact single-shot example; EOS and Thermoelasticity intentionally use more specialized controllers.

Typical layout

A single-shot module commonly contains:

quantas/modules/example/
├── models.py          passive Input, Options, and Result contracts
├── analysis.py        frontend-neutral scientific workflow
├── calculator.py      active orchestration and event translation
├── api.py             internal module facade
├── report.py          ReportTable builders
├── io/
│   ├── reader.py      normalized input and HDF5 readers
│   ├── hdf5_payload.py
│   ├── export.py
│   └── inpgen.py      optional interface-to-Quantas input generation
└── plot/
    ├── spec.py        neutral plot builders
    └── labels.py      module-specific scientific labels

The supported application facade is a separate file:

quantas/api/example.py

The Click adapter is also separate:

quantas/cli/example.py

Passive contracts

Input

Normalized scientific data. Arrays should have documented shapes, units, and copy/ownership semantics. Source provenance belongs here or in metadata.

Options

Scientific and numerical choices. Options should validate themselves or be validated before the calculation starts. Presentation-only choices should not be mixed into scientific options.

Result

Module-specific payload containing full-precision arrays, masks, uncertainties, and diagnostic metadata. It should not contain Matplotlib figures, Rich tables, open files, callbacks, or calculator instances.

Use slots=True where appropriate and copy mutable input arrays when the contract promises ownership.

Analysis functions

The analysis layer performs numerical work without emitting Quantas events or performing frontend I/O. Long loops may accept simple callbacks such as:

from collections.abc import Callable

ProgressCallback = Callable[[int, int], None]

def evaluate_grid(
    values: object,
    *,
    progress_callback: ProgressCallback | None = None,
) -> object:
    for index in range(total):
        # Numerical work only.
        if progress_callback is not None:
            progress_callback(index + 1, total)
    return result

The callback communicates only numerical progress. It does not receive a Rich progress bar or a Click context.

Calculator

A calculator is an active workflow object. It owns:

  • normalized input and options;

  • workflow stages;

  • warning collection;

  • event emission;

  • conversion of low-level callbacks into Quantas events;

  • final assembly of quantas.models.ResultData.

For a straightforward workflow, deriving from quantas.models.BasicCalculator provides prepare, execute, emit, warning handling, error events, and finalization.

The calculator should delegate formulas to core or analysis functions. It should not become the only place where a numerical method can be called.

Result envelope

Single-shot modules return a generic envelope:

ResultData
├── metadata
├── input_data
├── options
├── results[module_key] -> typed Result
├── warnings
└── events

The typed payload contains the science. The envelope contains provenance and workflow context shared by every module.

Module contract

A conventional module can define a quantas.models.ModuleContract that connects the standard operations:

  • read input;

  • run;

  • read/write HDF5;

  • build report tables;

  • build plot specifications.

The contract validates module metadata and the expected result payload key. The public registry exposes a higher-level capability description for applications and frontends.

Report builder

A report builder converts a result into ordered quantas.models.ReportTable objects. The table rows retain raw values. Formatting profiles, alignment, and units are metadata interpreted by a renderer.

Do not pre-format numerical values as strings merely to align a terminal table. That destroys reuse by CSV and GUI renderers.

Plot builder

A module plot builder prepares quantas.models.PlotCollection objects. It selects scientific quantities, masks, labels, uncertainty bands, and geometries. It does not create a Matplotlib figure.

A new scientific visualization normally belongs in the module plot builder. A new concrete drawing primitive belongs in the renderer only when the neutral plot model cannot represent the required data.

Persistence adapter

The module HDF5 adapter writes and reads only the scientific payload inside the shared result envelope. Writers emit the current schema. Readers validate metadata, units, shapes, and module identity before constructing the typed result.

A round trip must preserve values and interpretation:

typed result -> HDF5 -> typed result

Equality tests should cover arrays, masks, metadata required for interpretation, and optional/None fields.

Internal and public facades

quantas.modules.<name>.api may coordinate internal readers, calculators, reports, plots, and persistence. The supported application facade under quantas.api.<name> exposes only reviewed aliases and wrapper functions.

This extra boundary is deliberate. It permits internal reorganization without changing notebook, CLI, or GUI code.

CLI adapter

The Click command should:

  1. parse strings and paths;

  2. construct public Input/Options objects or call public readers;

  3. attach a CLI observer;

  4. call quantas.api;

  5. render reports and plots;

  6. choose output filenames and enforce overwrite policy.

It should not contain formulas, build scientific arrays independently, or repair results returned by the API.

Non-standard module lifecycles

Uniform names are useful, but scientific structure takes priority.

EOS

Keeps multiple immutable fit records in a persistent archive. It therefore exposes fitting, batch, session, calculation, and diagnostic operations rather than pretending to be one single-shot result.

Thermoelasticity

Separates calibration from Point/Grid/Profile analysis. The calibration HDF5 is reused by later operations.

When a new module genuinely needs a different lifecycle, document it and expose capabilities explicitly. Do not force it through an untyped universal run(**kwargs) abstraction.