Analytical NLFF Cases#
NF2 includes Low & Lou analytical magnetic fields for benchmark and regression use. Analytical cases use the same YAML-driven training path as observational Cartesian and spherical runs, but the boundary data are generated from a configured analytical field instead of read from FITS files.
Primary bundled config names:
nf2/benchmark/analytical_case1.yamlnf2/benchmark/analytical_case2.yaml
These examples are intended for fast smoke tests, documentation, and quantitative validation of the training and export pipeline.
Benchmark Analytical Commands#
The analytical benchmark examples generate Low & Lou magnetic fields internally, so no input data download is required. They are useful as smoke tests after installation and as small runs for checking training, callbacks, exports, and metrics.
The benchmark configs intentionally keep the run compact:
training.epochs: 15data.iterations: 1000no potential boundary
no height loss scaling
boundary validation plus height-slice validation
Case 1#
nf2-extrapolate \
--config "nf2/benchmark/analytical_case1.yaml" \
--run_path "./runs/benchmark/case1" \
--work_path "./runs/benchmark/case1/work"
Case 2#
nf2-extrapolate \
--config "nf2/benchmark/analytical_case2.yaml" \
--run_path "./runs/benchmark/case2" \
--work_path "./runs/benchmark/case2/work"
After Training#
The output is written to:
runs/benchmark/case1/extrapolation_result.nf2
runs/benchmark/case2/extrapolation_result.nf2
Run metrics or exports with the commands in Exporting and Quality metrics. For a quick check, use a small Cartesian export height range:
nf2-metrics "./runs/benchmark/case1/extrapolation_result.nf2" \
--Mm_per_pixel 0.05 \
--height_range 0 2
How The YAML Is Configured#
An analytical config is a Cartesian config with an analytical boundary:
path: "<<run_path>>"
work_path: "<<work_path>>"
data:
geometry: cartesian
normalization:
Mm_per_ds: 1
Gauss_per_dB: 1
boundaries:
- id: boundary
type: analytical
case: 1
boundary: full
resolution: 64
batch_size: 512
sampler:
type: height
batch_size: 1024
potential_boundary:
type: none
validation:
- id: boundary
type: analytical
case: 1
boundary: bottom
resolution: 64
batch_size: 512
- id: slices
type: slices
n_slices: 5
batch_size: 1024
iterations: 1000
z_range: [0, 2]
validation_pixel_per_ds: 32
Run the example by filling the path placeholders:
nf2-extrapolate \
--config nf2/benchmark/analytical_case1.yaml \
--run_path ./runs/analytical_case1 \
--work_path ./runs/analytical_case1/work
Use case: 1 or case: 2 to switch benchmark fields. Increase resolution for a denser boundary and validation grid, or reduce it for a faster smoke test. The default analytical bounds are [-1, 1] in x, [-1, 1] in y, and [0, 2] in z; with Mm_per_ds: 1, these coordinates are reported directly in Mm and are centered at (0, 0) in the boundary plane. The generated analytical field is normalized by the maximum absolute component value in the analytical cube, so with Gauss_per_dB: 1 the training and validation values are approximately in the range [-1, 1] G.
Losses And Validation#
Analytical examples usually disable the potential boundary and use a compact set of losses:
losses:
- type: boundary
name: boundary
weight: 1.0
datasets: boundary
- type: force_free
name: force_free
weight: 1.0e-3
datasets: [random]
loss_scaling: []
callbacks:
- type: boundary
dataset: boundary
- type: slices
dataset: slices
loss_scaling: [] disables the default Cartesian height scaling for these benchmarks, so the force-free loss is used directly. The boundary callback compares the predicted and true lower boundary on physical coordinates. The slices callback logs magnetic-field height slices, current-density height slices, and vertically integrated current density.
Python API#
You can launch the same benchmark without a YAML file:
import nf2
nf2.run(
path="./runs/analytical_case1",
data={
"geometry": "cartesian",
"normalization": {"Mm_per_ds": 1, "Gauss_per_dB": 1},
"boundaries": [
{
"id": "boundary",
"type": "analytical",
"case": 1,
"boundary": "full",
"resolution": 64,
"batch_size": 512,
}
],
"sampler": {"type": "height", "batch_size": 1024},
"potential_boundary": {"type": "none"},
"validation": [
{
"id": "boundary",
"type": "analytical",
"case": 1,
"boundary": "bottom",
"resolution": 64,
"batch_size": 512,
},
{"id": "slices", "type": "slices", "n_slices": 5, "batch_size": 1024},
],
"z_range": [0, 2],
"validation_pixel_per_ds": 32,
},
training={"epochs": 15},
loss_scaling=[],
callbacks=[
{"type": "boundary", "dataset": "boundary"},
{"type": "slices", "dataset": "slices"},
],
)
Load the result like any other Cartesian run:
import nf2
out = nf2.load("./runs/analytical_case1/extrapolation_result.nf2")
cube = out.load_cube(height_range=[0, 2], Mm_per_pixel=0.02, metrics=["j"])