4 · A new instance¶
An instance packages a complete, reproducible inverse problem — config, domain, ground-truth
scenes, an independent data generator, the problem builder, a default curriculum, metrics and
baselines — behind the nefi.instances.Instance protocol. Everything else in nefi talks to that
protocol: the CLI (nefi run/bench/diagnose/ablate/sweep), the benchmark harness, the
diagnostics, the smoke tests. This tutorial turns the operator of
tutorial 3 into the instance sensor_blur.
The protocol¶
| hook | returns | notes |
|---|---|---|
Config (class attribute) |
a dataclass | every number (paper settings!) is a field; Instance(cfg_dict_or_dataclass, **overrides) validates keys |
domain() |
Domain |
|
scene_generator() |
bench.SceneGenerator |
classes = (...) difficulty classes; sample(rng, cls, shape) must honour shape (supersampled data) |
data_generator() |
bench.DataGenerator |
independent model, distinct fidelity_tag |
build_problem(measurement) |
InverseProblem |
set curriculum=self.default_curriculum() |
default_curriculum() |
Curriculum |
the paper schedule |
build_problem_measurement_shape(field_shape) |
shape | measurement shape at native resolution (used to resample supersampled data) |
metrics() |
{name: fn(pred, gt)} |
or override evaluate(result, gt) for multi-field metrics |
baselines() |
{name: measurement -> (problem, curriculum)} |
optional |
default_scene_class() |
str | defaults to cfg.scene |
Instance.make_measurement(seed, scene_class) and Instance.run(seed=..., device=...) are
provided by the base class.
The code¶
"""sensor_blur — recover an image from a blurred, 2x-downsampled sensor image (tutorial 4)."""
from __future__ import annotations
from dataclasses import dataclass
import torch
import nefi
from nefi.baselines import baseline_problem
from nefi.bench import DataGenerator, SceneGenerator
from nefi.instances import Instance
from nefi.metrics import psnr, ssim
from nefi.registry import register
from sensor_blur_operator import SensorBlur # the operator of tutorial 3
@dataclass
class SensorBlurConfig:
n: int = 64 # native field grid (n × n) on [0, 1]²
sigma: float = 0.03 # optics blur (physical units)
factor: int = 2 # sensor pixels are factor× coarser
noise_std: float = 0.01 # relative to the clean maximum
supersample: int = 4 # data simulated on a 4× finer float64 grid
scene: str = "few" # scene class: few | many
hidden: int = 64
depth: int = 3
n_octaves: int = 6
steps: tuple[int, int] = (150, 300)
lr: float = 3e-3
tv: float = 1e-5
class DiskScenes(SceneGenerator):
"""Random disks with random amplitudes; analytic, so any resolution can be rendered."""
classes = ("few", "many")
def sample(self, rng, cls=None, shape=None):
cls = self.check_class(cls)
r = self.domain.physical_coords(shape, dtype=torch.float64)
x = torch.zeros(r.shape[:-1], dtype=torch.float64)
for _ in range(int(rng.integers(2, 4) if cls == "few" else rng.integers(6, 10))):
c = rng.uniform(0.2, 0.8, size=2)
rad, amp = rng.uniform(0.04, 0.12), rng.uniform(0.4, 1.0)
x = x + amp * (((r[..., 0] - c[0]) ** 2 + (r[..., 1] - c[1]) ** 2) < rad**2)
return {"x": x.float()}
@register("instance", "sensor_blur")
class SensorBlurInstance(Instance):
name = "sensor_blur"
Config = SensorBlurConfig
description = "image from a blurred, 2x-downsampled sensor (tutorial 4)"
smoke_overrides = {"n": 32, "hidden": 32, "depth": 2, "steps": (20, 30)} # `--smoke`
def domain(self):
return nefi.Domain.unit((self.cfg.n, self.cfg.n))
def operator(self, domain=None):
c = self.cfg
return SensorBlur(domain or self.domain(), c.sigma, c.factor)
def scene_generator(self):
return DiskScenes(self.domain())
def data_generator(self):
c = self.cfg
fine = self.domain().refine(c.supersample)
tag = f"sensorblur-{c.supersample}x-float64"
return DataGenerator(
SensorBlur(fine, c.sigma, c.factor, fidelity_tag=tag),
noise_std=c.noise_std, supersample=c.supersample, fidelity_tag=tag,
)
def build_problem_measurement_shape(self, field_shape):
return self.operator().output_shape(field_shape)
def build_problem(self, measurement):
c = self.cfg
field = nefi.NeuralField(
2, nefi.Heads({"x": nefi.Softplus(init_value=0.1)}),
hidden=c.hidden, depth=c.depth, n_octaves=c.n_octaves,
)
losses = nefi.LossSet({"data": nefi.MSE(), "tv": nefi.TV("x")}, weights={"tv": c.tv})
return nefi.InverseProblem(
self.domain(), field, self.operator(), losses, measurement,
curriculum=self.default_curriculum(), name=self.name,
)
def default_curriculum(self):
c = self.cfg
return nefi.Curriculum.multiscale((c.n, c.n), n_stages=2, steps=c.steps, lr=c.lr)
def metrics(self):
return {"psnr": psnr, "ssim": ssim}
def baselines(self):
# free-pixel grid with the same losses (Tikhonov / Grid Opt.), via the baselines package
return {"grid": lambda m: baseline_problem(self.build_problem(m), "grid")}
Notes:
- Data are generated once per sample by the instance and shared by all methods. Scene classes are difficulty strata (NeTMY: few/medium/many × close/medium/far; NeFTY: homogeneous/layered).
nefi.baselines.baseline_problem(problem, kind)gives any instance the classical family —"grid","lbfgs","admm","gaussian_splat","deep_decoder"— by swapping only the parameterization / solver while keeping the operator, losses and measurement, so comparisons isolate the prior. Non-gradient baselines request their solver throughproblem.meta["solver"]; the CLI and benchmark dispatch automatically.smoke_overrides(a class attribute or method returning config overrides) makesnefi run sensor_blur --smokea seconds-long check; alternatively shipconfigs/sensor_blur_smoke.yamlor aPRESETS["smoke"]dict in the module.
Registering it¶
@register("instance", "sensor_blur") adds the class to the registry when the module is imported.
Built-in instances live in nefi/instances/<name>/ and are listed in _INSTANCE_MODULES in
nefi/instances/__init__.py. Your own instances can stay in your own code; tell the CLI to import
the module in any of three ways:
NEFI_PLUGINS=sensor_blur_instance nefi list instances # module name (on PYTHONPATH) or a .py path
nefi run my_config.yaml # with `imports: [sensor_blur_instance.py]`
or declare an entry point in your package's pyproject.toml:
A config file¶
# configs/sensor_blur.yaml
imports: [sensor_blur_instance.py] # relative to this file; registers the instance
instance: sensor_blur
config: # fields of SensorBlurConfig (unknown keys are an error)
n: 64
scene: many
steps: [150, 300]
run: # defaults for CLI flags
seed: 0
methods: [neural, grid] # used by `nefi bench`
# optional:
# curriculum: {stages: [{shape: [32, 32], steps: 150, lr: 3.0e-3}, ...], restarts: 1}
# solver: {compile: false, dtype: float32}
instance: {type: sensor_blur, n: 64, ...} (the registry form) is accepted too.
Using it¶
nefi run sensor_blur · neural · seed 0 · scene many · device cpu
stage1 (32, 32): 150 steps, completed, 2.25s
stage2 (64, 64): 300 steps, completed, 10.37s
total 14.13s · 10113 parameters
metrics psnr 20.47 · ssim 0.3679
data fit PSNR 27.58 dB · RMSE/σ 4.321
saved out_run (result.pt config.yaml metrics.json history.csv)
nefi bench sensor_blur.yaml --n 8 --seeds 0,1,2 # neural vs grid (run.methods)
nefi diagnose sensor_blur.yaml # ill-posedness report
nefi ablate sensor_blur.yaml --variants no_annealing,no_pe,single_stage --n 4
nefi sweep sensor_blur.yaml --axis n_octaves --values 3,6,9 --n 4
nefi run sensor_blur.yaml --set n=128 --set stage.lr=1e-3 --device cuda
and from Python:
inst = SensorBlurInstance(n=64, scene="many")
out = inst.run(seed=0, device="cpu") # RunOutput(result, metrics, gt, measurement)
print(out.metrics)
Checklist¶
- Config dataclass with every number (cite the paper table in comments).
- Scenes honour
shape; data generator independent (differentfidelity_tag). -
build_problemsets the default curriculum; metrics appropriate to the science. - Baselines through
nefi.baselines.baseline_problem(or explicit builders). -
configs/<name>.yaml(+_paper.yaml) and a smoke path (smoke_overrides,configs/<name>_smoke.yamlorPRESETS["smoke"]). - A test running the smoke configuration end to end;
docs/instances/<name>.md.