Extending the framework¶
The smallest useful abstraction¶
There are two classes, one per settings file, plus three small hook types. The runner knows about dependencies and files, not Gaussian means, a specific network, or metric names.
What is trained: subclass nnpd.Experiment and implement:
Method |
Responsibility |
|---|---|
|
Construct the simulator/problem object. |
|
Construct the prior and its sampler. |
|
Write the model’s required training data; return JSON metadata. |
|
Return a new untrained |
|
Train that model in place; return JSON-compatible history. |
Optional methods supply validation, a training workload estimate, cohort members,
and cache signatures/source dependencies. A fixed comparison cohort defaults to one
empty member. The settings arguments are the resolved training settings.
What is computed from the trained models: subclass nnpd.Analysis and override
products(), metrics() and plots(), which return named Product, Metric and
Plot registrations. Optional validate(settings) checks the analysis settings,
and estimate(settings, problem, prior) returns the analysis workload of one
model, with limit violations in "errors". An analysis never trains.
The core framework does not require using the bundled NRE trainer or even using a
likelihood simulator class. tests/test_pipeline.py::IndependentExample and
IndependentAnalysis demonstrate a non-Gaussian implementation with its own data
schema, training function and metric.
For a new application, create an importable Python package and name its classes
in the settings files, for example "experiment": "my_application:MyExperiment"
in settings_training.py and "analysis": "my_application:MyAnalysis" in
settings_analysis.py. The classes must have a no-argument constructor;
scientific choices belong in the settings. The runner itself reads experiment,
output and runtime from the training settings and analysis, metrics,
plots and plotting.dpi from the analysis settings; all other settings are owned
by the application. The generic seed helper uses the training seed when present,
otherwise zero.
A new simulator or prior¶
For an NRE application, Simulator.sample(theta, rng) returns one observation per
parameter row, preserving leading dimensions. parameters describes the ordered
inferred coordinates; observation_dim is the number of observation features.
The optional log_likelihood(x, theta) is only needed for analytic diagnostic
products, not for classification training.
Prior.sample(size, rng) and Prior.log_prob(theta) are separate replaceable
methods. The bundled IndependentPrior defines both exactly for its four bounded
families. A correlated/non-factorized prior should subclass Prior and implement
its own density and sampler instead of pretending to be independent. Its
parameters must match the simulator’s ordered inferred coordinates.
support_axes is a tuple: None for a continuous coordinate, an array of allowed
values for a discrete coordinate. This determines the reference measure for the
bundled candidate integration. Mixed continuous/discrete products are supported.
More general constrained supports or importance proposals need a custom candidate
product with correct quadrature/proposal weights; the rectangular design is not
a universal integrator.
The Gaussian example separates likelihood parameters from inferred parameters.
mean:*, std:*, mean:j, and std:j are conveniences in that application,
not syntax hard-coded in the framework. Arbitrary selected subsets have automatic
network input sizes. Unselected coordinates remain fixed; integrating nuisance
parameters would be a different sampling/inference implementation.
Cache correctness is part of the hook contract¶
Product.settings(context) must include every setting read by its builder
that is not already captured in a dependency artifact. Builders read
context.training_settings and context.analysis_settings. The default is both
complete settings dictionaries: conservative and safe, but less reusable. Reduce it only when the
builder truly depends on fewer fields. Include explicit dataset-release IDs or
file checksums for external inputs.
The complete source modules containing builders and declared sources are
hashed, together with entry-point identity, an explicit version, and Python,
NumPy, SciPy and PyTorch versions. Declare helpers from other modules:
Product(builder, needs=("model",), sources=(external_helper, CustomSimulator))
This is intentionally not a magical recursive import tracker. Dynamically loaded
code, external files, service responses and simulator releases are not inferred.
List their source dependencies or change the explicit version/settings identifier.
The experiment’s sources("data"), sources("model") and
signature(stage, context) follow the same rule for root datasets and checkpoints.
Changing a metric or a plotting module does not belong in a model fingerprint.
Metrics that read the model directly¶
A metric may declare needs=("model",) and use context.model; it does not need
an intermediate product. It may also combine model access with any number of
saved datasets. See parameter_count in gaussian/extensions.py.
Metrics return finite JSON-compatible values: numbers, booleans, strings, lists
and dictionaries, or NumPy values that can be converted to those. Use None for
an undefined quantity, not NaN. Large arrays belong in products, not in metric
JSON. The runner writes both one JSON per metric and a combined metrics.json.
Plot anything¶
A plot hook receives the same full context and dependency mapping as a metric.
Return a dictionary of descriptive filename stems to Matplotlib figures. One hook
can return any number of figures. The runner saves PNGs and closes the figures.
You can call the hook directly from Python/notebooks and use figure.savefig(...)
for another format or display the figure interactively. Source data, raw scores,
simulator sampling, prior evaluation and model calls are all available.
gaussian/extensions.py::observation_histogram is a complete working example
accessing shared observations, the model class and the prior measure. The built-in
plots are examples, not a required visual style or a restrictive plotting API.
Adding a model or replacing training¶
The bundled classifier contract is a tensor (batch, observation_dim + parameter_dim)
with observations first, parameters second, returning one logit per row. Do not
add a sigmoid when using fit_binary; sigmoid probabilities can be computed for
presentation separately. For another architecture, add a factory to NETWORKS
in nnpd/nre/models.py, or override build_model and the application’s validation.
Factories receive (input_size, model_settings).
To change optimization, return a different trainer from the experiment’s train
method; the orchestration remains unchanged. The existing trainer supports Adam,
AdamW, best/last checkpoints, gradient clipping and optional AMP. A custom trainer
must honor the runtime’s distributed contract itself. Merely subclassing
Experiment does not automatically make an arbitrary training algorithm DDP-safe.
The built-in trainer assumes the model has no train-time cross-example behavior
such as unsynchronized batch normalization.
Only complete trained state dictionaries are persisted. Mid-epoch optimizer, scheduler and gradient-scaler continuation is not implemented. This keeps the training interface small; add an explicit checkpoint product/trainer if that form of fault tolerance is needed.
Reload and compare results¶
from nnpd import restore
from nnpd.results import runs, export_csv, comparison_plot
records = runs("outputs/smoke")
record = records[0]
context = restore(record["path"], device="cpu")
model = context.model
observations = context.require("observations").array("observations")
summary = context.require("inference")
export_csv("outputs/smoke", "outputs/summary.csv", {
"prior": "member.prior",
"dimension": "settings.problem.dimension",
"absolute_bias": "metrics.bias.mean_absolute",
"coverage_68": "metrics.coverage.projected_mean.0",
})
restore reloads data/weights without training, with the experiment and analysis
classes and settings the run was trained and analyzed with. Requiring a derived
product checks its current recipe; changed analysis code may rebuild that product. To reproduce
an earlier analysis, use the saved source snapshot and compatible package versions.
Or inspect its artifact arrays directly using the paths in artifacts.json.
Filter records before plotting to keep different OFAT axes, replicas or backends
from being accidentally pooled as though they were the same experiment.