# Validation

Cross-validation of the Elmer solve against analytical magpylib fields, a sanity bound on the result.

## femforge.validation

Cross-validation of the Elmer solve against magpylib's analytical fields, a sanity bound rather than an equality check.

Modules:

- **`compare`** –

  Spot-check error metrics between two vector fields sampled at the same points.
- **`field`** –

  Sample the magnetic flux density **B** from magpylib or from a solved Elmer VTU at a common set of points.
- **`sources`** –

  Analytical magnet sources: a `DomainConfig` and built geometry turned into SI `MagnetSource`s for magpylib.
- **`viz`** –

  PyVista rendering of the analytical magpylib field, drawn like the solved Elmer result so both can share a `viz.Scene`.

Classes:

- **`MagnetSource`** –

  One uniformly-magnetized cuboid magnet, in SI units, ready to hand to magpylib.
- **`FieldComparison`** –

  Aggregate error of a test field versus a reference field sampled at the same points.
- **`MagpylibField`** –

  A `viz.scene.Renderable` that draws the analytical magpylib field (|B| streamlines + magnet boxes) for a set of `MagnetSource`s.
- **`GridBoundsMode`** –

  Whether a sampling grid stays inside its target bounds or covers them.

Functions:

- **`magnet_source`** –

  Build the `MagnetSource` for one body, or None if it is not a magnet; a magnet with no usable `Magnetization` direction raises `ValueError`.
- **`sources_from_assembly`** –

  Walk a built build123d assembly and return a `MagnetSource` per magnet leaf, reading only its label and bounding box (mm → m).
- **`material_for_label`** –

  The material whose `tag` equals (casefold) the label's `Material` tag value, or None — mirrors `MeshEntity._match_material`.
- **`magnetization_for_label`** –

  The usable (directed) `Magnetization` carried by an `EntityTag` matching the label's tags, or None — mirrors `elmer.sim`'s magnet wiring.
- **`gap_line`** –

  A line of `samples` points along Z through the centroid of the magnet centres — for a dual-Halbach array it threads the air gap.
- **`sample_magpylib_b`** –

  Evaluate the analytical **B** (tesla) of `sources` at each point: one SI magpylib `Cuboid` per source, superposed via `Collection.getB`.
- **`sample_vtu_b`** –

  Interpolate the solved Elmer **B** (tesla) at `points` from a `case.vtu` (pyvista `sample`; elemental arrays are projected to point data first).
- **`extract_vtu_b`** –

  Extract the solved Elmer **B** (tesla) from a `case.vtu` as `(positions, B)` arrays of shape `(n_points, 3)`.
- **`compare_fields`** –

  Compare two `(N, 3)` fields sampled at the same `points` and return a `FieldComparison`; raises `ValueError` on mismatched or empty inputs.
- **`grid_spec`** –

  The `(origin, spacing, dimensions)` of a `pv.ImageData` sampling `bounds` with near-cubic cells; raises `ValueError` for fewer than 8 samples or a zero-volume box.
- **`bounds_of_sources`** –

  The axis-aligned bounding box (metres) enclosing every source's full extent, grown by `padding` on each side; raises `ValueError` for no sources.
- **`bounded_grid`** –

  A `pv.ImageData` grid spanning `bounds` with \~cubic cells and `samples` in total, ready to carry a sampled vector field as point data.
- **`magpylib_field_grid`** –

  Sample the analytical **B** of `sources` onto a regular grid over their padded bounding box, a `pv.ImageData` ready for the streamline path.
- **`load_gmsh_mesh`** –

  Load a Gmsh `.msh` (via meshio) into a `pv.UnstructuredGrid` with the region names attached as the `"Region_Name"` cell-data array.

Attributes:

- **`DEFAULT_PADDING`** –

  `[m]`: The padding around the magnets on each axis, so the field grid covers the surrounding air and not only the magnet volume.

### DEFAULT\_PADDING

```python
DEFAULT_PADDING = Vec3(0.05, 0.05, 0.01)
```

`[m]`: The padding around the magnets on each axis, so the field grid covers the surrounding air and not only the magnet volume.

### MagnetSource

Bases: `BaseModel`

One uniformly-magnetized cuboid magnet, in SI units, ready to hand to magpylib.

`polarization` is J = Bᵣ · direction̂ (tesla) — the same physical drive Elmer applies via `Magnetization` (J = μ₀·M).

Config:

- `arbitrary_types_allowed`: `True`
- `use_attribute_docstrings`: `True`

Fields:

- `center` (`Vec3Field`)
- `size` (`Vec3Field`)
- `polarization` (`Vec3Field`)

#### center

```python
center: Vec3Field
```

Magnet centre in the world frame (metres).

#### size

```python
size: Vec3Field
```

Full extents along x/y/z (metres) — magpylib's cuboid `dimension`.

#### polarization

```python
polarization: Vec3Field
```

Magnetic polarization J = Bᵣ · direction̂ (tesla).

### FieldComparison

Bases: `BaseModel`

Aggregate error of a test field versus a reference field sampled at the same points.

Errors are on |B\_test − B\_ref| in tesla; the headline metric is the field-weighted `rel_rms_error`.

Config:

- `arbitrary_types_allowed`: `True`
- `use_attribute_docstrings`: `True`

Fields:

- `n_points` (`int`)
- `rms_error` (`float`)
- `max_abs_error` (`float`)
- `max_error_point` (`Vec3Field`)
- `ref_rms` (`float`)
- `test_rms` (`float`)
- `rel_rms_error` (`float | None`)
- `median_relative_error` (`float | None`)
- `mean_relative_error` (`float | None`)

#### n\_points

```python
n_points: int
```

Number of sample points compared.

#### rms\_error

```python
rms_error: float
```

Root-mean-square of |B\_test − B\_ref| over the points (tesla).

#### max\_abs\_error

```python
max_abs_error: float
```

Largest single |B\_test − B\_ref| (tesla).

#### max\_error\_point

```python
max_error_point: Vec3Field
```

The sample point where `max_abs_error` occurred (metres) — where to look first when the fields disagree.

#### ref\_rms

```python
ref_rms: float
```

RMS |B\_ref| over the points (tesla): the reference field scale, so `rms_error` is interpretable.

#### test\_rms

```python
test_rms: float
```

RMS |B\_test| over the points (tesla).

#### rel\_rms\_error

```python
rel_rms_error: float | None = None
```

**The headline metric.** Field-weighted relative error rms(|ΔB|) / rms(|B\_ref|); robust to low-field points. None if every reference point is zero.

#### median\_relative\_error

```python
median_relative_error: float | None = None
```

Median of |B\_test − B\_ref| / |B\_ref| over points with non-zero reference, or None if every reference point is zero. The robust middle of the per-point distribution.

#### mean\_relative\_error

```python
mean_relative_error: float | None = None
```

Mean of |B\_test − B\_ref| / |B\_ref| over points with non-zero reference, or None if every reference point is zero. Outlier-sensitive — prefer `rel_rms_error`.

#### summary

```python
summary() -> str
```

A human-readable multi-line report for the spot-check script.

### MagpylibField

```python
MagpylibField(sources: list[MagnetSource], options: FieldLineOptions | None = None, padding: Vec3 = DEFAULT_PADDING, samples: int = 32, show_magnets: bool = True)
```

A `viz.scene.Renderable` that draws the analytical magpylib field (|B| streamlines + magnet boxes) for a set of `MagnetSource`s.

Sampling and streamline integration run in `__post_init__` (whichever worker thread constructs it); `render` only draws precomputed actors, keeping the plotter thread free.

Methods:

- **`render`** –

  Draw the precomputed magpylib field lines (and, when `show_magnets`, the magnet bodies) into `plotter` (on the plotter thread).

Attributes:

- **`sources`** (`list[MagnetSource]`) –

  The magnets whose analytical field is drawn.
- **`options`** (`FieldLineOptions | None`) –

  The streamline options; None uses the defaults.
- **`padding`** (`Vec3`) –

  `[m]`: The padding around the magnets on each axis that the field grid covers.
- **`samples`** (`int`) –

  The approximate total point count of the field grid.
- **`show_magnets`** (`bool`) –

  Whether to draw the magnet boxes alongside the streamlines.

#### sources

```python
sources: list[MagnetSource]
```

The magnets whose analytical field is drawn.

#### options

```python
options: FieldLineOptions | None = None
```

The streamline options; None uses the defaults.

#### padding

```python
padding: Vec3 = DEFAULT_PADDING
```

`[m]`: The padding around the magnets on each axis that the field grid covers.

#### samples

```python
samples: int = 32
```

The approximate total point count of the field grid.

#### show\_magnets

```python
show_magnets: bool = True
```

Whether to draw the magnet boxes alongside the streamlines.

#### render

```python
render(plotter: Plotter) -> None
```

Draw the precomputed magpylib field lines (and, when `show_magnets`, the magnet bodies) into `plotter` (on the plotter thread).

### GridBoundsMode

Bases: `Enum`

Whether a sampling grid stays inside its target bounds or covers them.

Attributes:

- **`INNER`** –

  The grid stays inside the target bounds: its point counts round down, so it never overflows them.
- **`OUTER`** –

  The grid covers the target bounds: its point counts round up, so it never falls short of them.

#### INNER

```python
INNER = 'inner'
```

The grid stays inside the target bounds: its point counts round down, so it never overflows them.

#### OUTER

```python
OUTER = 'outer'
```

The grid covers the target bounds: its point counts round up, so it never falls short of them.

### magnet\_source

```python
magnet_source(label: str, center_m: Vec3, size_m: Vec3, domain: DomainConfig, *, at: Mapping[str, Quantity] | None = None) -> MagnetSource | None
```

Build the `MagnetSource` for one body, or None if it is not a magnet; a magnet with no usable `Magnetization` direction raises `ValueError`.

Parameters:

- **`label`** (`str`) –

  The body's label, carrying its tags.
- **`center_m`** (`Vec3`) –

  The body's center, in metres.
- **`size_m`** (`Vec3`) –

  The body's axis-aligned extent, in metres.
- **`domain`** (`DomainConfig`) –

  The domain config whose materials and tags the label matches.
- **`at`** (`Mapping[str, Quantity] | None`, default: `None` ) –

  The operating point the remanence is evaluated at; None is room temperature.

### sources\_from\_assembly

```python
sources_from_assembly(assembly: Compound, domain: DomainConfig, *, at: Mapping[str, Quantity] | None = None) -> list[MagnetSource]
```

Walk a built build123d assembly and return a `MagnetSource` per magnet leaf, reading only its label and bounding box (mm → m).

Parameters:

- **`assembly`** (`Compound`) –

  The built assembly, whose magnets turn only by multiples of 90° and so stay axis-aligned.
- **`domain`** (`DomainConfig`) –

  The domain config whose materials and tags the leaf labels match.
- **`at`** (`Mapping[str, Quantity] | None`, default: `None` ) –

  The operating point the remanence is evaluated at; None is room temperature.

### material\_for\_label

```python
material_for_label(label: str, materials: list[MaterialProperties]) -> MaterialProperties | None
```

The material whose `tag` equals (casefold) the label's `Material` tag value, or None — mirrors `MeshEntity._match_material`.

### magnetization\_for\_label

```python
magnetization_for_label(label: str, tags: list[EntityTag]) -> Magnetization | None
```

The usable (directed) `Magnetization` carried by an `EntityTag` matching the label's tags, or None — mirrors `elmer.sim`'s magnet wiring.

### gap\_line

```python
gap_line(sources: list[MagnetSource], samples: int) -> list[Vec3]
```

A line of `samples` points along Z through the centroid of the magnet centres — for a dual-Halbach array it threads the air gap.

Returns a single mid-point when `samples < 2`; raises `ValueError` for an empty list.

### sample\_magpylib\_b

```python
sample_magpylib_b(sources: list[MagnetSource], points: NDArray[float64]) -> NDArray[float64]
```

Evaluate the analytical **B** (tesla) of `sources` at each point: one SI magpylib `Cuboid` per source, superposed via `Collection.getB`.

### sample\_vtu\_b

```python
sample_vtu_b(vtu_path: str | Path, points: list[Vec3]) -> list[Vec3]
```

Interpolate the solved Elmer **B** (tesla) at `points` from a `case.vtu` (pyvista `sample`; elemental arrays are projected to point data first).

### extract\_vtu\_b

```python
extract_vtu_b(vtu_path: str | Path) -> tuple[NDArray[float64], NDArray[float64]]
```

Extract the solved Elmer **B** (tesla) from a `case.vtu` as `(positions, B)` arrays of shape `(n_points, 3)`.

### compare\_fields

```python
compare_fields(points: NDArray[float64], b_reference: NDArray[float64], b_test: NDArray[float64]) -> FieldComparison
```

Compare two `(N, 3)` fields sampled at the same `points` and return a `FieldComparison`; raises `ValueError` on mismatched or empty inputs.

Parameters:

- **`points`** (`NDArray[float64]`) –

  The `(N, 3)` sample points, in metres.
- **`b_reference`** (`NDArray[float64]`) –

  The reference field at `points`; points where it is exactly zero are left out of the relative errors.
- **`b_test`** (`NDArray[float64]`) –

  The field under test at `points`.

### grid\_spec

```python
grid_spec(bounds: Bounds, samples: int, mode: GridBoundsMode = OUTER) -> tuple[Point3, Point3, tuple[int, int, int]]
```

The `(origin, spacing, dimensions)` of a `pv.ImageData` sampling `bounds` with near-cubic cells; raises `ValueError` for fewer than 8 samples or a zero-volume box.

Parameters:

- **`bounds`** (`Bounds`) –

  The target box, as `(xmin, xmax, ymin, ymax, zmin, zmax)`.
- **`samples`** (`int`) –

  The approximate total point count.
- **`mode`** (`GridBoundsMode`, default: `OUTER` ) –

  Keep the sampled extent inside the bounds (`INNER`) or covering them (`OUTER`).

### bounds\_of\_sources

```python
bounds_of_sources(sources: list[MagnetSource], *, padding: Vec3) -> Bounds
```

The axis-aligned bounding box (metres) enclosing every source's full extent, grown by `padding` on each side; raises `ValueError` for no sources.

### bounded\_grid

```python
bounded_grid(bounds: Bounds, samples: int) -> ImageData
```

A `pv.ImageData` grid spanning `bounds` with \~cubic cells and `samples` in total, ready to carry a sampled vector field as point data.

### magpylib\_field\_grid

```python
magpylib_field_grid(sources: list[MagnetSource], *, padding: Vec3 = DEFAULT_PADDING, samples: int = 32) -> ImageData
```

Sample the analytical **B** of `sources` onto a regular grid over their padded bounding box, a `pv.ImageData` ready for the streamline path.

Parameters:

- **`sources`** (`list[MagnetSource]`) –

  The magnets.
- **`padding`** (`Vec3`, default: `DEFAULT_PADDING` ) –

  The air added around the magnets on each side, in metres.
- **`samples`** (`int`, default: `32` ) –

  The approximate total point count.

### load\_gmsh\_mesh

```python
load_gmsh_mesh(msh_path: str | Path) -> UnstructuredGrid
```

Load a Gmsh `.msh` (via meshio) into a `pv.UnstructuredGrid` with the region names attached as the `"Region_Name"` cell-data array.
