Visualization — nefi.viz¶
nefi.viz turns inversions into figures, animations, dashboards and shareable reports. It is the
presentation layer of the library: every figure in the demo gallery, the per-run figures on the
GPU servers and the paper-style panels come from the functions documented here.
- Optional dependency. matplotlib is imported lazily inside the plotting functions:
import nefiandimport nefi.viznever need it (pip install "nefi[viz]"to plot). - Headless-safe. Every function builds a
matplotlib.figure.Figurewithoutpyplot, so nothing ever opens a window; scripts and tests callmatplotlib.use("Agg")first. - Inputs are whatever the library produces: torch tensors, numpy arrays,
Resultobjects,{name: tensor}field dicts,Measurements, benchmark rows. - Outputs go to
runs/(git-ignored).viz.savefigkeeps PNGs at ≤ 150 dpi and under a size budget (300 kB by default).
import matplotlib
matplotlib.use("Agg")
from nefi import viz
from nefi.instances.toy1d import Toy1D
from nefi.solve.callbacks import FieldSnapshots
snaps, stages = FieldSnapshots(every=20), viz.StageSnapshots()
out = Toy1D().run(seed=0, device="cpu", callbacks=[snaps, stages])
viz.savefig(viz.compare_fields(out.gt, {"nefi": out.result}, out.measurement), "runs/toy/compare")
viz.savefig(viz.plot_fit(out.result.pred, out.measurement), "runs/toy/fit")
viz.savefig(viz.plot_history(out.result, noise_std=out.measurement.noise_std), "runs/toy/history")
viz.savefig(viz.plot_multiscale(out.result, stages, gt=out.gt), "runs/toy/multiscale")
viz.animate_snapshots(snaps, "runs/toy/evolution.gif", gt=out.gt["x"], result=out.result)
3-D fields use the same calls (compare_fields switches to the volume layout of §2.1); the
instance's display hints (§3) are picked up when instance= is passed:
from nefi.viz._instances import resolve_instance
inst, _ = resolve_instance("thermal_tomography") # smoke preset
out = inst.run(seed=0, device="cpu")
mask = out.measurement.meta["defect_mask"]
fig = viz.compare_fields(out.gt, {"nefi": out.result}, out.measurement, instance=inst,
mask=mask, panels="overlay") # mosaic + GT outline
viz.savefig(fig, "runs/tt/compare")
viz.savefig(viz.voxel_compare(out.gt, {"nefi": out.result}, domain=inst.domain()),
"runs/tt/voxels")
1. Style guide¶
Color¶
| Job | Rule | Implementation |
|---|---|---|
| identity (methods, loss terms, stages) | eight categorical hues in a fixed order, never cycled; a ninth series is folded ("+k more") | viz.PALETTE / viz.PALETTE_DARK, theme().palette |
| magnitude of a density / source / intensity | magma |
cmap_for("rho") |
| material coefficient (diffusivity, conductivity, permeability, velocity, refractive index) | viridis |
cmap_for("alpha"), cmap_for("sigma") |
| signed fields, errors, residuals, wavefields | RdBu_r, symmetric limits centred at 0 |
cmap_for(quantity="error") |
| wrapped phase | twilight on [-π, π] |
cmap_for("phase") |
| unwrapped phase with a range < π | centred RdBu_r (a full circle would waste the contrast) |
automatic in cmap_for(name, data) |
| measurements | viridis, or centred RdBu_r when signed — distinct from the unknown's map |
cmap_for(quantity="measurement", data=...) |
| complex data (fields and measurements) | magnitude (magma) and phase (twilight, or centred RdBu_r for a range < π) |
plot_complex, draw_measurement |
| a hint says otherwise | the measurement_cmap / field_cmap display hint (§3) |
viz.spec_from_hint |
Signed data (viz.is_signed): data count as signed when min < 0 < max beyond noise —
the robust extremes (0.5 / 99.5 percentiles) have opposite signs and the smaller one reaches
10 % (SIGNED_FRACTION) of the larger. Noise dipping below a zero background (sinograms, counts,
spectra: a few % of the peak) stays sequential; a stray-field map with a 15 % negative lobe, a
zero-mean phase or a boundary voltage pattern is drawn with the diverging map centred at 0.
This applies to measurements and fields alike (a sequential quantity whose data turn out signed
switches to the centred map).
The categorical palette is validated for color-vision deficiencies (adjacent ΔE ≥ 8 under
protan/deutan simulation, ≥ 15 in normal vision) in both themes; three light-mode slots are below
3:1 contrast on the light surface, which is why every multi-series plot carries a legend and
every report carries the numbers as a table. The quantity is guessed from the field name
(FIELD_QUANTITIES: rho, f, x, mu → magma; alpha, sigma, k, c → viridis;
chi, dn, u, bz → centred; phi, phase → twilight); anything unknown falls back to the
data (centred diverging when positive and negative parts are comparable, viridis otherwise).
Marks and layout¶
- Thin lines (1.1–1.4 pt), solid hairline gridlines, no top/right spines, sans-serif 7.5–11 pt.
- Ground truth is drawn in ink with a dashed line (1-D) or as its own panel (2-D / 3-D); the reconstruction takes the first categorical slot; measurements are muted dots.
- No dual y-axes. Quantities with different units get stacked panels that share the x axis
(e.g. loss / β/K / learning rate in
plot_history). - Images share one color scale per comparison (GT and every reconstruction), errors share their own symmetric scale; every image scale has a colorbar. Image panels keep a hairline frame so near-zero error maps stay visible on the light surface.
- Light and dark themes:
viz.use_style(dark=True, context="paper"|"notebook"|"talk")as a context manager, orviz.set_default_style(dark=True)for a whole script. - Sizes:
viz.figsize(ncols, nrows, cbar=..., title=...),viz.paper_figsize(columns=1|2)for 3.4 in / 7.0 in journal widths.
Saving¶
viz.savefig(fig, "runs/x/compare", formats=("png", "svg"), dpi=130, max_kb=300) writes every
requested format (the path suffix counts), creates directories, uses deterministic metadata, and
keeps PNGs under max_kb by palette quantization, then by lowering the dpi (≥ 55).
2. What every plot shows¶
Fields (nefi.viz.fields)¶
| Function | Shows |
|---|---|
compare_fields(gt, {method: recon}, measurement, panels=...) |
The standard reconstruction figure: measurement (compact view) · ground truth · each reconstruction (metrics in the title) · extra panels. kind is detected: 1-D → overlaid lines + error panel; 2-D → images (optional peaks=True markers: GT = hollow circles, recovered = crosses; mask= contour); 3-D → the volume layout of §2.1; complex → magnitude and phase rows. Reconstructions at another resolution are resampled to the GT grid. metrics="auto" (PSNR/SSIM or PSNR/rel. error, computed on the displayed maps), {name: fn}, or precomputed {method: {metric: value}} (e.g. the instance's own metrics). instance= / hints= apply the display hints (§3: field_transform, field_label, field_cmap, volume_axis); transform= / cmap= override them. |
plot_field(x) |
One field: line, image, 3-D depth mosaic, or magnitude/phase. |
image_panels({label: image}) |
A grid of 2-D panels with one shared scale. |
depth_mosaic(vol, n_slices=8, mask=...) |
Depth slices of a 3-D field, one shared scale, physical depths from a Domain, optional mask outline. Thin volumes are fine: with fewer slices than n_slices every slice is shown, a squeezed single slice (nz = 1) becomes a one-panel mosaic. |
orthoslices(vol, index=...) |
XY / XZ / YZ slices through index (default: the most anomalous voxel; index=viz.anomaly_centroid(gt) cuts through the GT anomaly). |
projections(vol, mode="auto") |
Max / min / mean projections along each axis (auto projects the anomaly side: min for low-diffusivity defects). |
voxel_view(vol) |
Isosurface-free 3-D scatter of thresholded voxels (depth axis exaggerated for thin slabs); spec / vmin / vmax / threshold to share scales. |
voxel_compare(gt, {method: recon}) |
GT and reconstructions as thresholded voxels side by side: one threshold (half-way from the GT background to its anomaly extreme), one colour scale, one camera and one set of limits; titles give the voxel count and the IoU with the GT voxel set. |
plot_complex(x) |
Magnitude (magma) and phase (twilight). |
anomaly_centroid(vol, mask=None) |
Grid index of the dominant anomaly: the given mask, else the voxels whose absolute difference to the median reaches half the largest difference; the connected component with the largest deviation wins; deviation-weighted centroid (2-D and 3-D). |
Extra panels of compare_fields (panels=("measurement", "gt", "recon", <extra>...), or
panels="overlay" as a shorthand for the default plus one extra; error=False drops the error):
| panel | 2-D | 3-D | 1-D |
|---|---|---|---|
"error" (default) |
signed error recon − GT, RdBu_r centred at 0, own colorbar |
one error row per method (mosaic, cross-sections, mean projection) | error line panel |
"overlay" |
the reconstruction with the GT contours (solid white, dark halo — visible on every colormap; a mask= is outlined instead) and the reconstruction's own contour at the same level (dashed aqua). Levels: GT background ± ½ of the largest deviation on each side |
GT outlined on every reconstruction panel (slices, cross-sections, projection) — the gallery's default for 3-D systems | — (the main panel already overlays) |
"profile" |
a line cut through the GT anomaly centroid (profile_axis=0: along x), GT dashed vs every reconstruction; the cut is marked on the image panels. Great for blurred / 1-D-like cases |
cuts along x, y and depth through the centroid (three line panels) | — |
With several methods, error and overlay become extra rows under the reconstructions and
profile one panel with every method. Field transforms (e.g. zero_mean for a phase defined up
to a constant) are applied to GT and reconstructions separately and every panel title says so
("ground truth · mean removed").
2.1 3-D fields (nefi.viz.volume)¶
A field with three non-singleton dimensions (nx, ny, nz) is never reduced to one slice. Every
3-D view combines three readings of the volume, all on one shared colour scale:
- a depth mosaic — up to 6 evenly spaced slices along the volume axis (default: the last
one, depth
z; thevolume_axishint changes it) at the centres of equal depth bins (mosaic_slices(16, 6)→ 1, 4, 6, 9, 12, 14; thin volumes, e.g.nz < 4, show every slice), with the depth of each slice as a label (physical with aDomain); - the two cross-sections through the anomaly centroid (
anomaly_centroid: from the mask — e.g. the thermaldefect_mask— or from|gt − median|),x–zandy–z, depth pointing down (the first slice — the observed face of surface-measurement problems — on top), a+at the centroid; - the anomaly-side projection along the volume axis:
minwhen the anomalies lie below the background (low-diffusivity defects),maxotherwise (sources, absorbers); error rows use themean.
Where these appear:
| Figure | 3-D content |
|---|---|
gallery.png (overview) |
1-D / 2-D systems keep their three-panel tiles; every 3-D system gets a full-width row: GT mosaic (2 × 3) · measurement (compact view) · reconstruction mosaic with the GT outlined · cross-sections GT / reconstruction · projection GT / reconstruction (draw_volume_tile). |
gallery_3d.png |
One block per 3-D system: larger GT and reconstruction mosaics (up to 8 slices, GT outlined in white, the reconstruction's own contour dashed), contrast contours of GT (solid) and reconstruction (dashed) at 25 / 50 / 75 % of the GT contrast on the central slice and the depth cross-section, and shaded isosurfaces — the GT at its threshold rule, the reconstruction at the volume-matched level, same camera, levels / IoU / Dice in the titles (§2.2; physics_volumes, draw_volume_block); with the edge refinement (refine=True) every reconstruction panel comes twice — smooth and refined — and the block title carries the refinement's verdict, IoU / Edge-F1 and χ before → after. The static fallback of the "3-D systems" section of index.html, where every system's row (<name>/block3d.png) is followed by its interactive viewer (§2.3). |
compare_fields / <name>/compare.png |
Rows = ground truth, each reconstruction (and each signed error with "error"), columns = mosaic · cross-sections · projection; the measurement spans the first two rows. The gallery uses panels=(..., "overlay") for 3-D systems (GT outlined on the reconstruction panels); "profile" adds x / y / depth line cuts through the centroid. |
<name>/mosaic.png, <name>/voxels.png |
The reconstruction's 8-slice depth mosaic (GT mask outlined) and iso_compare(GT, reconstruction): contrast contours + shaded isosurfaces at the GT rule / volume-matched level (the file keeps its old name). |
<name>/block3d.png, <name>/viewer.html, <name>/viewer.json |
The system's gallery_3d row as its own figure, the standalone interactive viewer and its data (what index.html embeds) — written when the gallery runs with interactive (default). |
<name>/evolution.gif |
animate_snapshots(..., volume="mosaic"): 4 depth slices of the evolving field above the same slices of the GT, loss curve with a moving marker (coarse curriculum stages are sliced at the same relative depths); volume="slice" animates the slice through the GT anomaly centroid. |
examples/visualize_result.py |
compare (overlay), mosaic, orthoslices through the GT anomaly centroid, projections, isosurfaces (GT vs reconstruction, field_isosurfaces.png); with --html the report embeds the interactive viewer (also viewer.html). |
Building blocks for custom layouts (all draw into caller-provided axes): draw_slices,
draw_sections, draw_projection, draw_voxels, Outline (GT mask or anomaly level set),
section, project, projection_mode, mosaic_slices, mosaic_shape.
2.2 Isosurfaces and matched levels (nefi.viz.isosurface)¶
Reconstructions of volumetric problems recover the structure of the ground truth with softer edges, so cutting them at the GT's own threshold under- or over-segments them and the voxel view looks worse than the field is. Level sets are therefore compared with explicit rules:
- GT threshold rule (
voxel_rule(instance, gt)): the instance's IoU rule — an IoU metric that is afunctools.partialofiou_below/iou_abovewith atau(thermal tomography:α < 0.03; ct3d:μ > 0.25), or a half-maximum rule fromcfg.iou_fractionand one*backgroundconfig value (dot3d: excess overμ_a,bgbeyond 50 % of each volume's own peak excess, a relative rule) — else half-way from the GT background (median) to its extreme. Thevoxel_thresholddisplay hint (a number,(side, value)or a rule mapping) overrides it.select_mask(values, rule)applies a rule (it reproduces the instances' IoU masks exactly). - reconstruction level rules (
iso_levels(gt, recon, rule)returns all of them with the voxel count, IoU and Dice against the GT set):"fixed"— the GT rule itself (what the instance's IoU metric does);"matched"— the volume-matched level, enclosing as many voxels as the GT does at its threshold (matched_level: half-way between the two sorted values that bracket the GT count);"otsu"— Otsu's threshold of the reconstruction's histogram (256 bins; the middle of the tied splits when the classes are separated by a gap);"manual"— a given value. - display transforms (
volume_transform(a, spec), display only):smooth(Gaussian, σ in voxels),sharpen(unsharp maskx + amount (x − G_σ x), clipped to the input range),edge_preserve(5 Perona–Malik diffusion iterations: smooths inside regions, keeps steep edges). They are applied to reconstructions (never to the GT) before iso-extraction, and every title saysdisplay: …. Choose them per instance with thevolume_transformdisplay hint ("sharpen",{"name": "smooth", "sigma": 2}or{field: spec}) or thetransform=keyword; metrics reported as the reconstruction's (manifest, tables) never use them. - meshes (
iso_mesh(values, level, side)):skimage.measure.marching_cubeson the volume padded with an outside value (surfaces close at the box), faces oriented outward, decimated bystep_sizeto a face budget;Nonewithout scikit-image — the figures then fall back to the voxel scatter.
| Function | Shows |
|---|---|
iso_compare(gt, {method: recon}, instance=..., how="matched") |
The voxels.png figure: contrast contours on the central slice and the depth cross-section (GT solid, reconstruction dashed, 25 / 50 / 75 % of the GT contrast — spread-out dashed contours are soft edges), then the shaded isosurfaces of the GT (its rule) and of each reconstruction (how level), one camera, titles with the levels, IoU, Dice and the IoU at the GT threshold. |
draw_isosurfaces(axes, [(title, volume, level, side)]) |
Shaded Poly3DCollections (two-sided Lambert headlight, depth stretched for thin slabs as in voxel_view); GT neutral, reconstructions in the categorical slots. |
draw_level_contours(axes, gt, recon, center=...) |
The two contour panels. |
examples/gallery.py prints the reconstruction's IoU / Dice at the GT threshold and at the
volume-matched level and tabulates them in the "3-D systems" section (manifest.json:
entries[i].iso). At the gallery budget (--budget 0.1, CPU, seed 0):
| system | GT rule | IoU at GT threshold → matched | Dice at GT threshold → matched | IoU Otsu |
|---|---|---|---|---|
| thermal_tomography | α < 0.03 (IoU rule) |
0.984 → 1.000 | 0.992 → 1.000 | 0.218 |
| ct3d | μ > 0.25 (IoU rule) |
0.638 → 0.660 | 0.779 → 0.795 | 0.297 |
| deconvolution3d | half-way, x > 0.464 |
0.149 → 0.423 | 0.260 → 0.594 | 0.241 |
| dot3d | half max of the excess (IoU rule) | 0.587 → 0.796 | 0.740 → 0.886 | 0.741 |
| photoacoustic3d | half-way, p0 > 0.500 |
0.663 → 0.678 | 0.798 → 0.808 | 0.676 |
The IoU at the GT threshold is the instance's own IoU metric where it has one; the gain of the matched level measures how much of the apparent error is edge softness rather than misplaced structure (deconvolution3d: the blurred filaments are found but under-segmented at the GT's half-way level).
2.3 Interactive 3-D viewer (nefi.viz.interactive)¶
Every 3-D system of the gallery's index.html gets a drag-to-rotate viewer right under its
static row: ground truth and reconstruction side by side with one camera (with the gallery's edge
refinement: GT | smooth | refined, each reconstruction with its own level and IoU / Dice),
rendered by a
dependency-free script (nefi/viz/assets/volume_viewer.js + .css, vanilla JavaScript on a 2-D
canvas — a small software rasterizer with a depth buffer; no libraries, no CDN, no network).
from nefi import viz
html = viz.volume_viewer_html({"ground truth": gt, "reconstruction": rec}, # tensors / arrays
extent=inst.domain(), threshold=viz.voxel_rule(inst, gt),
mode="iso", field="alpha") # an HTML fragment
viz.save_volume_viewer("runs/x/viewer.html", {"ground truth": gt, "reconstruction": rec},
extent=inst.domain(), mode="iso") # a standalone page
viz.viewer_size_bytes({"ground truth": gt, "reconstruction": rec}) # bytes it adds to a page
Controls. Drag (one finger) orbits (turntable about the vertical axis; the depth axis points
down so the observed face z = 0 is on top), wheel / pinch zooms, right-drag or shift-drag pans,
double-click resets; with a focused canvas: arrows, + / −, 0. An axes triad, the physical
extents at the box corners and a depth cue (farther voxels smaller and faded toward the
background) keep the orientation readable. "link cameras" (on by default) keeps both panels on
one camera; "reset view" resets it.
Views (mode=): iso — shaded isosurfaces (lit triangles: headlight Lambert + Blinn–Phong,
smooth (Gouraud) or flat shading, back faces culled; "GT overlay" draws the GT surface
semi-transparent over each reconstruction); voxels — thresholded voxel centres as squares in
the colormap of the static figures (magma / viridis / RdBu_r, … as 33-stop tables) with an
opacity slider; slice — a movable x / y / z slice drawn as a textured quad in the rotated frame
(default: through the GT anomaly centroid); iso+slice, voxels+slice (alias points+slice).
Levels and readout. The GT threshold slider (and < / > side toggle) starts at the GT rule
(§2.2; relative rules become a "fraction of the peak" slider), the reconstruction's level follows
the selected rule — volume-matched (default), GT threshold, Otsu, or manual (slider); ↺ returns
to the defaults. Each panel shows its level and rule, the GT its voxel count, each reconstruction
the IoU and Dice with the GT set, computed in the browser; the colour bar marks every level. The
display transform select (raw, smooth σ = 1 / 2, sharpen, edge-preserving) re-computes the
reconstruction and its isosurface live and labels it "display: …".
Data and sizes. Volumes are area-averaged to at most max_side = 64 voxels per axis
(≤ 64³; the info line says so) and quantized to integer codes lo + q · step — 16-bit up to
48³ voxels, 8-bit beyond (bits=) — base64 in a <script type="application/json"> block; shape,
physical extent and axis names make the aspect ratio physical (thin slabs get the static
figures' depth stretch, labelled z ×k, adjustable). Codes are nudged so the GT rule selects
exactly the voxels it selects in the full-precision arrays: without downsampling, the in-browser
IoU at the GT threshold equals the instance's IoU metric. The default isosurfaces are marching-cubes meshes from Python (vertices
uint16, faces uint16 / uint32, ≤ max_faces = 20000 faces each); other levels and transforms are
re-extracted in the browser (marching tetrahedra, ≈ 20 ms at 64³). A viewer adds its fragment
plus, once per page, the script and stylesheet (≈ 62 kB): the gallery's 3-D systems (smoke grids,
16-bit, with meshes) take thermal_tomography 17 kB, dot3d 28 kB, photoacoustic3d 77 kB,
deconvolution3d 134 kB and ct3d 250 kB; two 96³ volumes (→ 64³, 8-bit) with meshes ≈ 0.95 MB. Several viewers coexist on one page (unique element
ids; include_assets=False for all but the first).
Plotly mode (optional, not self-contained). plotly_volume_html(...) /
save_volume_viewer(..., kind="plotly") / examples/gallery.py --interactive plotly render
plotly.graph_objects.Isosurface scenes (same rules and levels, cameras synchronized) with
include_plotlyjs="cdn": the page loads plotly.js from cdn.plot.ly and needs network access
(volumes ≤ 32³: plotly writes every grid point as JSON text). Never the default; the canvas viewer
is.
Measurements (nefi.viz.measurement)¶
The layout is taken from Measurement.meta["layout"], then from instance display hints, then
detected from the shape relative to the field (detect_layout):
| Layout | Typical shape | Compact view (tiles, compare_fields) |
Full view (plot_measurement) |
|---|---|---|---|
signal |
(n,) for a 1-D field |
dots | line + ±σ band |
vector |
(n_sensors,) |
dots vs sensor | same |
image |
field-shaped | image | image + colorbar |
points |
field-shaped + sparse mask | scatter of observed pixels | same |
matrix / sinogram |
(n_views, n_det) |
image, rows vertical | plot_sinogram: sinogram + projection profiles |
spectra |
(n_freq, H, W) |
noise map Σ_ω S | plot_spectra: noise map, frequency slices, pixel spectra (NeTMY) |
frames |
(n_t, H, W) + frame_times |
time mean | plot_frames: geometric-time frame strip (per-frame scale) + decay curves (NeFTY) |
stack |
(n, H, W) |
mean over the stack (masked entries ignored) | channel slices + channel profiles |
traces |
(n_src, n_rec, n_t) |
gather of the middle source | plot_traces: image gather (time down) + wiggle plot |
kspace |
masked Fourier samples | log-magnitude | plot_kspace: sampling mask + log-magnitude |
complex |
complex / stacked re-im (complex_stack hint) |
magnitude and phase (two insets) | magnitude + phase |
volume |
field-shaped 3-D (nx, ny, nz) (e.g. a blurred stack) |
max projection along the volume axis (signed data: mean; reduce = index → that slice) |
depth mosaic |
sinogram stack |
(n_views, n_det, n_z) (+ angles) |
the middle slice; the slice axis is the stack_axis hint, else the axis whose length equals the field's depth, else the last |
plot_sinogram of the middle slice |
Colours follow §1: signed data (e.g. a stray-field map, boundary voltages, pressure traces) get
the centred diverging map, everything else viridis; complex data show magnitude and phase.
Instance hooks: when an instance implements measurement_image(measurement), its result is the
compact view — a 1-D / 2-D result is drawn as it is (an image with its own label), even when
the layout hint describes the raw data as a stack (e.g. a boundary-difference view of EIT
patterns). The hook may return (tensor, "label"); otherwise the measurement_image_label hint
names it. plot_fit never calls the hook (data, prediction and residual stay in data space).
plot_fit(pred, measurement) puts data, prediction and residual side by side with the noise
floor: 1-D → overlay + residual with ±σ / ±2σ bands; images → data | prediction | residual |
residual histogram against N(0, σ²); stacks → data | prediction | RMS residual per slice against
the σ line | histogram. The title reports RMSE, σ and RMSE/σ: ≈ 1 means the fit reached the
noise level (Morozov); ≪ 1 over-fits the noise; ≫ 1 under-fits. residual_stats returns the
same numbers.
Training dynamics (nefi.viz.training)¶
| Function | Shows |
|---|---|
plot_history(result, noise_std=σ) |
Loss components on a log scale (total in ink, data loss in slot 1, identical terms merged), curriculum stage boundaries (labelled with the stage grid), shaded annealing windows (β < K), the annealing progress β/K and the learning rate in stacked panels, and a σ² floor line (only meaningful for a plain MSE data loss — the gallery draws it only then). Components switched off in some stages are aligned with NaN (history_arrays). |
plot_stage_summary(result) |
Steps, seconds (ms/step) and final data loss per stage; early stops are named. |
plot_multiscale(result, StageSnapshots) |
End-of-stage fields at their own resolution (32² → 64²), the final and GT fields, above the loss curve. |
animate_snapshots(FieldSnapshots, "x.gif") |
GIF (or MP4 with ffmpeg) of the field during optimization with the GT and a moving marker on the loss curve; 3-D fields: an evolving depth mosaic above the GT mosaic (volume="mosaic", default) or the slice through the GT anomaly (volume="slice"). The layout is frozen after the first frame (fast), and the GIF is kept under 300 kB by thinning frames (the last one is kept), shrinking the palette, then downscaling. |
StageSnapshots is a solver callback that keeps every field at the end of each stage; the
library's FieldSnapshots(every=k) keeps the primary field every k steps.
Qualitative comparisons (nefi.viz.qualitative)¶
| Function | Shows |
|---|---|
method_grid(rows, row_labels) |
The classic scenes × methods figure: GT and measurement first, one shared color scale per row, headline metrics under each method, optional signed-error rows. Cells may be tensors, Results, Measurements, error strings or None. |
failure_modes(gt, {method: recon}) |
Per method: reconstruction with the GT support outlined · signed error · leakage map (mass outside the dilated support, % in the title) · log error power spectrum with the axis excess — error power on the Fourier axes relative to an isotropic error with the same radial spectrum (Hann-windowed, lowest frequencies ignored): ≈ 1 isotropic, ≫ 1 cross / streak artifacts, < 1 diagonal structure. failure_stats / axis_excess return the numbers. |
baselines_panel(instance, budget_scale=0.1) |
Runs the instance's own method and every baselines() entry on one measurement (via the benchmark protocol, so closed-form and ADMM baselines work) and draws the grid; returns (figure, runs). run_methods returns the raw runs. |
Performance (nefi.viz.performance)¶
All plots take rows (lists of dicts): collect_performance output, BenchmarkResult /
benchmark.json / rows.csv, a RuntimeTable, or a gallery manifest.json (load_rows
normalizes aliases such as name → instance, derives n from shape and ms_per_step from
time_s / steps). Row keys: instance, method, mode, device, class, stage, shape,
n, params, steps, ms_per_step, fwd_ms, bwd_ms, opt_ms, time_s, per_stage_s,
peak_mem_mb (CUDA), saved_mb, metric columns, error.
| Function | Shows |
|---|---|
collect_performance(instances, device, steps) |
Re-implements tools/profile_instance.py as rows: smoke-sized problem, finest stage, forward / backward / optimizer split, peak CUDA memory and the autograd saved-tensor volume of one step (a device-agnostic memory proxy that separates adjoint from unrolled gradients). variants={"adjoint": {"grad_mode": "adjoint"}, ...} adds one row per variant; collect_scaling(name, [32, 64, 128], key="n") sweeps sizes. |
plot_step_time(rows) |
ms/step per instance; stacked forward / backward / optimizer when available, grouped by device otherwise. |
plot_scaling(rows) |
Step time and memory vs grid points, log-log, fitted slopes in the legend and an O(N) guide. |
plot_memory(rows) |
Memory per gradient mode (adjoint vs checkpoint vs autograd) and grid size. |
plot_wallclock_breakdown(results) |
Stacked bars of where the time went (data generation, each stage, overheads, evaluation, figures). |
plot_bench_table(rows, "psnr") |
Mean ± Student-t CI per method, one small multiple per scene class; best mean in bold. |
load_profiles("runs/_perf") → profile_speedups(rows) → plot_speedup(rows) |
Speedup vs eager per instance from tools/profile_instance.py --json profiles (*.jsonl): every accelerated variant (compile=field/step, autocast=bf16, cuda graphs, --set overrides such as solver=chebyshev) against the latest plain eager solver-loop run of the same instance, device and thread count; bars start at 1× (log axis), > 1× is faster; harness variants (--minimal) are skipped. The gallery adds the panel when profiles exist and skips it silently otherwise. |
plot_step_time appends the profiled grid to every label (ct3d · 32×32×16), so volumetric
instances stand out; the gallery's scaling plot includes the 3-D heat solver (thermal tomography,
adjoint gradients) next to toy1d and deconvolution.
Multi-physics gallery (nefi.viz.multiphysics)¶
physics_gallery(instances=None, budget_scale=0.15, device="auto", out_dir="runs/gallery") runs
every registered instance (or the given names / classes / objects) and writes
gallery.png— one tile per physical system: ground truth | measurement (compact view) | reconstruction, with the headline metric (PSNR > SSIM > HF1 > rel. error > …), solve time and steps; failed systems become a tile with the error text; 3-D systems get a full-width row below the tiles (§2.1). The instance's display hints are applied (e.g. holography's phase is anchored to zero mean by the model'sZeroMeanhead, current_density shows|J|);gallery_3d.png— larger mosaics and GT vs reconstruction voxels of every 3-D system (§2.1);<name>/{tile,compare,measurement,fit,history,stages,multiscale,mosaic,voxels}.pngandevolution.gif(animate=True);compareuses the signed error, 3-D systems the GT overlay (thecompare_panelhint overrides both);voxels.pngholds the isosurfaces of §2.2;- 3-D systems with
interactive=True(default;"plotly", orFalseto skip):<name>/block3d.png(the system'sgallery_3drow),<name>/viewer.html(standalone) and<name>/viewer.json(embedded bygallery_sections, §2.3); - 3-D systems with
refine=True(examples/gallery.py: on by default,--no-refine): after the smooth solve,nefi.solve.refine_run_output(entry)runs the edge refinement ofdocs/refinement.md(refine_entry); the refined field becomes a third column — GT | smooth | refined — in the overview's 3-D row, ingallery_3d.png, as a second method incompare.png/voxels.pngand as a third panel of the viewer (same camera and level rule). The tile title addsrefined ✓ IoU a → b · Edge-F1 c → d · χ e → f(IoU / Edge-F1: the instance's own metrics where it has them, else the refinement's at the GT threshold; χ = RMSE/σ, or the RMSE without a noise level); a refused refinement keeps the smooth result and its candidate is drawn labelled "refused refinement (not used)". The headline metric stays the smooth result's; the report'ssummary()is the caption (manifest:entries[i].refine; the "3-D systems" section gets an "Edge refinement" table); - 3-D budgets: at
budget_scale ≥ 0.2a 3-D system runs at leaststeps_3d_multiplier(default 2) × its smoke preset's steps, abovemax_steps(they are under-converged at the plain rule; most of ct3d's refinement gain at the plain budget was just more steps) —budgetin the manifest recordssteps_3d_floor; manifest.json— per system: status, error + traceback, scene class, smoke preset, budget, grid / measurement shapes, detected layout, metrics, timings (generate_s,solve_s,per_stage_s,evaluate_s,plots_s,volume_extras_s,total_s), ms/step, parameters, config hash, figures, and for 3-D systemsiso(GT rule, levels, IoU / Dice at the GT threshold, volume-matched and Otsu levels) andviewer(files, bytes, grid); top level:overview,overview_3d,volumetric(names of the 3-D systems),interactive.
Budget rule. Problem sizes come from each instance's smoke preset (exactly the
nefi run --smoke lookup: smoke_overrides, configs/<name>_smoke.yaml, PRESETS["smoke"]).
Steps = budget_scale × the instance's default curriculum, floored by the preset's own
(validated) budget and capped by max_steps; time_budget_s bounds the solve. Every instance
runs in its own try block — an import, build, data, solve or plotting failure never stops the
gallery.
Reports (nefi.viz.report)¶
html_report(out_dir, title, sections, config_hash=..., metrics=rows) writes one
self-contained HTML file: figures embedded as base64 (PNG / SVG / GIF, animations included),
metric and timing tables (tabular figures, status glyphs), a table of contents, the config hash,
the environment (versions, device, CUDA, git revision read from .git files) and light / dark page
chrome. Section(title, text, images, table, code, subsections); images may be paths,
(path, caption), {"path", "caption", "wide"} or figures. markdown_report writes the same
content as Markdown with relative image links.
3. Instance hooks (optional)¶
Instances need nothing special, but three hooks improve their figures:
measurement_image(measurement) -> Tensor | (Tensor, label)— the compact view of the measurement in display units (e.g. deconvolution converts Poisson counts to intensity, EIT shows a boundary-difference image, ct3d the most structured sinogram slice). A 1-D / 2-D result is drawn as it is, with its own label.viz_hints = {...}(class attribute, or a method returning the dict) — display hints, see below.Measurement.metakeyslayout,frame_times,freqs,angles,dt(always win), and a field-shaped mask underdefect_mask/anomaly_mask/support_mask/support/inclusion_mask(outlined on 3-D views and used for the anomaly centroid).
Display hints (viz_hints, nefi.viz.hints)¶
Hints are resolved as INSTANCE_HINTS[name] (built-in defaults) < instance.viz_hints < the
hints= argument of a plotting call; meta["layout"] always wins for the layout. Field-level
keys accept one value for every field or a {field_name: value} mapping ("*" = default).
viz.HINT_KEYS holds this table.
| Key | Meaning | Example |
|---|---|---|
layout |
measurement layout (viz.LAYOUTS) |
"sinogram" |
word |
name of the stacked axis | "patterns" |
reduce |
compact view of a stack / volume: "sum", "mean", "max", "std" or an index |
0 |
complex_stack |
real and imaginary parts stacked along the "first" / "last" axis |
"first" |
axis_values |
instance method returning the stacked axis values | "angles" |
row, col |
axis labels of matrix / sinogram layouts | "view", "detector" |
stack_axis |
slice axis of a 3-D matrix / sinogram measurement | -1 for (n_views, n_det, n_z) |
measurement_cmap |
colormap of measurement views: a matplotlib name, "signed" (centred RdBu_r), "sequential", "cyclic", a quantity ("density"), or "auto" |
"signed" |
measurement_transform |
transform of the compact view (and of the noise map of spectra): "log" (noise-aware floor: 3σ of the negative noise values), "abs", "zero_mean", … |
"log" |
measurement_label |
display name of the measurement in panel titles | "B_z" |
measurement_image_label |
label of the measurement_image hook's view |
"boundary difference" |
field_cmap |
colormap of the fields (values as measurement_cmap) |
{"chi": "signed"} |
field_transform |
display transform of GT and reconstructions (applied to each map separately; panel titles say so): "zero_mean", "abs", "log", "grad_magnitude", "curl_magnitude" (‖∇×(g ẑ)‖ of a stream function, zero outside the field of view), "real", "imag", "phase", a callable, or the name of an instance method |
"zero_mean" |
field_label |
display name of the (transformed) field | {"g": "sheet current"} |
volume_axis |
slicing / projection axis of 3-D fields | -1 (depth z) |
voxel_threshold |
GT threshold rule of voxel / isosurface views and of the viewer (§2.2; default: the instance's IoU rule) | ("below", 0.03) |
volume_transform |
display transform of 3-D reconstructions before iso-extraction (§2.2), str / mapping or {field: spec} |
"sharpen" |
compare_panel |
extra panel(s) of the gallery's compare figure: "error", "overlay", "profile" |
"profile" |
Built-in defaults (viz.INSTANCE_HINTS) cover the current instances, among them:
holography → no display transform (its phase gauge is fixed in the model by a ZeroMean head; use field_transform="zero_mean" for phase-like fields of your own instances);
current_density → signed B_z (measurement_cmap="signed", measurement_label="B_z") and the
sheet current |J| = |∇×g| instead of the stream function (field_transform={"g":
"curl_magnitude"}); nv_relaxometry → log-scaled noise map (measurement_transform="log").
Instances override them with their own viz_hints.
4. Paper-style figures per instance¶
| Instance | Figure | Calls |
|---|---|---|
nv_relaxometry (NeTMY) |
density maps with peak markers, noise map / spectra, Larmor map | compare_fields(gt, {"NeTMY": res, "Tikhonov": grid}, meas, field="rho", peaks=True); plot_measurement(meas, instance=inst) (noise map, frequency slices, pixel spectra); compare_fields(..., field="omega_L"); failure_modes(gt["rho"], {...}) for cross / leakage artifacts |
thermal_tomography (NeFTY) |
depth-slice comparison (Fig. 4), surface frames, 3-D view | compare_fields(gt, {"NeFTY": res, "Grid": grid}, meas, slices=6, mask=meas.meta["defect_mask"]); depth_mosaic(res, mask=...); plot_frames via plot_measurement; voxel_view, projections(mode="min") |
deconvolution, sparse_view_ct, poisson_source, holography, current_density, diffraction_tomography, reaction_diffusion, eit, darcy_flow, wave_fwi |
method comparison | baselines_panel(name, budget_scale=1.0, device="cuda") or method_grid over several scenes; plot_fit for the data fit |
| any | training dynamics | plot_history, plot_multiscale(res, StageSnapshots), animate_snapshots |
| any | benchmarks | plot_bench_table(BenchmarkResult.load(dir), "psnr"); efficiency: plot_step_time, plot_scaling, plot_memory from collect_performance / collect_scaling |
For publication, wrap the calls in with viz.use_style(context="paper"): and save with
formats=("pdf", "png") (fonts stay text in SVG / PDF).
5. Scripts¶
The demo gallery¶
python examples/gallery.py --budget 0.15 --device cpu --out runs/gallery --html
python examples/gallery.py --instances toy1d,deconvolution,nv_relaxometry --no-perf
python examples/gallery.py --device cuda --budget 1.0 --max-steps 0 --time-budget 0 --html # server
python examples/gallery.py --instances thermal_tomography,dot3d --budget 0.1 --html --no-interactive
python examples/gallery.py --budget 0.2 --no-refine --steps-3d-multiplier 0 # plain 3-D budgets
Writes physics/ (gallery, per-instance figures, manifest), baselines/ (panel, failure modes,
runs.json), performance/ (step time, scaling, memory, wall-clock, benchmark CI plot,
performance.json), report.md and, with --html, the self-contained index.html (its "3-D
systems" section interleaves every static row with its interactive viewer; report.md keeps
gallery_3d.png). --no-interactive skips the viewers; --interactive plotly uses the plotly
mode of §2.3 (then index.html loads plotly.js from its CDN). --refine (default) adds the
edge-refined column of every 3-D system (--no-refine skips it); --steps-3d-multiplier
(default 2) is the 3-D step floor at --budget ≥ 0.2; the step counts and the refinement
captions are printed per system.
Figures for a finished run (on the server)¶
nefi run configs/nv_relaxometry_paper.yaml --device cuda --out runs/nv_paper # result.pt, config.yaml
python examples/visualize_result.py runs/nv_paper --html # → runs/nv_paper/figures
python examples/visualize_result.py runs/x/result.pt --instance thermal_tomography \
--config configs/thermal_tomography_paper.yaml --seed 0 --out runs/x/figs
python examples/visualize_result.py runs/x/result.pt --gt runs/x/gt.pt --measurement runs/x/meas.pt
For a nefi run directory the ground truth and measurement are regenerated deterministically
from the saved instance config, seed and scene; --gt / --measurement load torch.saved tensors,
{name: tensor} dicts or Measurement objects instead. Output: compare*.png, field*.png
(3-D: mosaic, orthoslices, projections, isosurfaces), history.png, stages.png, wallclock.png,
measurement.png, fit.png, metrics.json, and report.html with --html (3-D results: with the
interactive viewer, also written as viewer.html; --interactive plotly|none).
6. Limitations¶
- 3-D fields are shown as slices, cross-sections, projections, isosurfaces (marching cubes; without scikit-image the static figures fall back to voxel scatters) and the interactive viewer; there is no ray-cast volume rendering. The viewer is a software rasterizer: smooth up to ≈ 50k triangles per panel, slower for very large or noisy level sets at 64³ (interaction frames render at 1× device pixels, refined when idle). The volume-matched level assumes the GT set is the right size; it fixes the soft-edge bias, not a wrong shape.
- The anomaly centroid follows the dominant anomaly (largest total deviation); a second defect elsewhere shows up in the mosaic and the projection but not in the cross-sections. Extended "anomalies" (a skull, a filament network) are not outlined on projections (too cluttered).
- Signed-data detection is a threshold (the minority sign must reach 10 % of the majority);
genuinely signed data with a weaker second polarity need
measurement_cmap="signed"/field_cmap="signed". - Reports are static HTML except for the 3-D viewers; 2-D figures are not interactive (no hover read-outs, no zoom). The viewer's wheel zoom captures the wheel over its canvases.
- CPU memory is reported as the autograd saved-tensor volume, not the process peak; peak memory is exact on CUDA only.
- Measurement layouts without metadata rely on shape heuristics and the built-in hint table; new
instances with ambiguous shapes should set
viz_hintsormeta["layout"]. - The gallery runs instances sequentially in one process; a hard crash (segfault) or a hang in
data generation cannot be isolated (the solve itself is bounded by
time_budget_s).