← Back to the validation dossier

Tier X (cross-code, against FEMM 4.2) - priority P2 Reference frozen on 2026-08-16. Nabla 0.1.0, solver licence mode enforcing, run in 400.2 s.

1. Problem

One flat linear machine, asked for its force pair at eight mover positions, by three different constructions and two different codes.

A slotted armature lies below; a full-coverage permanent-magnet array on a back-iron slab is the mover above. The machine is infinite in the direction of travel, so it is solved on a window: one pole pitch with anti-periodic sides, or two with periodic ones. The mover is carried across one pole pitch either by a linear sliding band - boundary-condition type 10 on the anti-periodic window and type 9 on the periodic one - or, in the control models, by being drawn where it is.

Pole pitch 30 mm, 3 slots per pole pitch (slot pitch 10 mm)
Slots semi-closed: 6 mm body, 2 mm opening through a 1 mm lip, root at -10 mm
Armature yoke -28 ... -10 mm, teeth to the tip line at y = 0, linear iron mu_r = 2000
Airgap 2.01 mm mechanical; band lines at y = 1.0 and 1.01 mm, the 10 um strip between them a HOLE
Mover magnets 2.01 ... 8.01 mm, B_r = 1.2 T, mu_r = 1.05, full pole coverage; back iron to 18.01 mm
Far field A = 0 at y = -48 and y = +48.01 mm
Winding single layer, full pitch, 50 turns per slot, phases held at DC 30 / -15 / -15 A
Depth 100 mm, planar 2D, P1, transient with no time derivative in it
Sweep 8 steps of 3.75 mm at 30 mm/s, plus record 8 - the mover one whole pole pitch on

The machine at half a pole pitch, with the mover carried by the band

The currents are DC and nothing in the model has a time derivative - no eddy currents, no circuit, linear iron - so every record of the "transient" is a magnetostatic solve at its own mover position. The band is the only thing that changes between them, which is the point.

The thrust therefore traces a sinusoid across the pole pitch and passes through zero twice, with a fundamental of 160.092 N. Every force row in this case is consequently normalised by the peak of its reference series rather than pointwise: a pointwise relative error at a zero crossing is a division by nothing, and it says nothing about the rest of the sweep.

Both irons are linear on purpose. A cross-code force comparison should not also be a BH-table transcription test; that is V02's subject, and V02 records that the two codes cannot even be handed byte-identical tables (FEMM requires a curve through the origin). Here they are handed the same two numbers.

The slots are semi-closed on purpose too. An earlier draft of this machine had fully open slots, and its detent force was as large as its useful thrust - the regime case V05 finding F2 and V14's unjudged cogging rows say is dominated by the instrument rather than by the solver. Narrowing the opening to below the airgap collapsed the slot harmonic; what is left is published as thrust_cogging_share = +16.26 % of the peak.

2. Reference

FEMM 4.2, driven by pyfemm, built by reference/femm_linear.py from the same geometry.json that Nabla's build.py reads. FEMM has no sliding band, which is exactly why it is the right partner for one: it is given the same machine with the mover drawn at each of the eight offsets on a conforming mesh, one pole pitch wide with anti-periodic sides (FEMM boundary format 5).

The force is a hand integral in both codes, on the same contour. With the mover above the plane y = 0.5 mm - in meshed air in every model, banded, conforming and FEMM - its outward normal is -y and the traction is

f_x = -(Bx By) / mu0                thrust
f_y = -(By^2 - Bx^2) / (2 mu0)      normal force, negative = attraction

times the model depth. That is term for term what Results.linearBandForce computes from Bn/Bt on the mid-band contour, and it is deliberately not either code's own stress-tensor call: two codes' normal and sign conventions are not the thing under test, which is the lesson V02's Ampère row was written to carry. FEMM's own mo_lineintegral(3) over the same line is recorded beside it in femm_reference.json and reproduces the hand integral to about 0.02 % with FEMM's opposite normal convention - the check that the hand integral is reading the field FEMM thinks it wrote.

The eight thrust and eight normal-force values are frozen into expected.json. extract.reference() reloads femm_reference.json every run, so an edit to the generated file that is not matched in the frozen one is reported as DRIFT rather than quietly becoming the new reference, and the nightly gate needs no FEMM installation. See the case's reference notes.

The other two references are Nabla itself, and they are exact rather than approximate. A periodic window twice as wide is the same infinite machine; a band at a given displacement is the conforming mesh drawn at that offset. The reference for those rows is exactly zero deviation - up to the unmeshed band strip, whose share of the magnetic circuit is arithmetic on the geometry and is published as band_gap_deficit_frac = +0.13 %.

3. Nabla model

Eleven sub-models, built, solved and read one at a time:

Sub-model Window Sides Band Mover carried by
conf_p0 ... conf_p7 30 mm type 8, anti-periodic none drawn at the offset
band_ap 30 mm type 8 type 10, anti-periodic the sliding band
band_ap_alt 30 mm type 8 type 10 the sliding band, on a different mesh
band_p 60 mm type 7, periodic type 9, periodic the sliding band
Sub-model Element order Nodes Elements
conf_p0 P1 38451 76397
conf_p1 P1 38527 76550
conf_p2 P1 38420 76336
conf_p3 P1 38337 76170
conf_p4 P1 38435 76366
conf_p5 P1 38293 76082
conf_p6 P1 38490 76476
conf_p7 P1 38402 76300
band_ap P1 38746 76386
band_ap_alt P1 50837 100406
band_p P1 76964 152142

The whole geometry is generated once as a list of lines carrying their own mesh edge length and a role; _draw draws that list and _subdivide refines it, so a shape cannot be drawn under one rule and meshed under another. The band strip is a createHole, never a region (rule M16), and the motion zone ends between the two band lines (rule M17).

Three instruments, deliberately not two. getResultsInMotion(motion, "Force" / "Force Normal") is the product's own force-pair path and is what the FEMM row judges - it is the thing this case exists to validate. The hand-integrated contour is what every sub-model has, banded or not, so it is the only instrument that can compare the band against a conforming mesh, because a model with no motion has no band force to read. They agree to +0.40 % on thrust and +0.13 % on the normal force, on one solution, across two contours separated by nothing but current-free air.

Two things about this model had to be found rather than looked up, and both are in §5.

4. Results

Quantity Metric Nabla Reference Error Tolerance Verdict
thrust_vs_position_N profile min=-249.8, max=126, rms=169.6 N min=-249.6, max=125.1, rms=169.8 N L2 +0.405 %, max +0.487 % L2 2.000 %, max 2.000 % PASS
normal_force_vs_position_N profile min=-1124, max=-527, rms=916.9 N min=-1121, max=-524.2, rms=913.8 N L2 +0.336 %, max +0.323 % L2 2.000 %, max 2.000 % PASS
thrust_periodic_N profile min=-249.3, max=125.4, rms=165.3 N min=-249.8, max=126, rms=165.2 N L2 +0.269 %, max +0.307 % L2 0.500 %, max 0.500 % PASS
normal_force_periodic_N profile min=-1124, max=-461.3, rms=877.9 N min=-1124, max=-461.9, rms=878 N L2 +0.041 %, max +0.066 % L2 0.500 %, max 0.500 % PASS
thrust_band_probe_N profile min=-250.1, max=125.4, rms=170 N min=-249.4, max=124.9, rms=169.6 N L2 +0.245 %, max +0.255 % L2 1.000 %, max 1.000 % PASS
normal_force_band_probe_N profile min=-1124, max=-525.6, rms=916.2 N min=-1120, max=-523.9, rms=913.7 N L2 +0.283 %, max +0.312 % L2 1.000 %, max 1.000 % PASS
thrust_vs_femm_max_rel abs 0.00486514 - 0 - 0.004865 0.02 PASS
normal_force_vs_femm_max_rel abs 0.00323269 - 0 - 0.003233 0.02 PASS
periodic_vs_antiperiodic_thrust_max_rel abs 0.00307427 - 0 - 0.003074 0.005 PASS
periodic_vs_antiperiodic_normal_max_rel abs 0.000662063 - 0 - 0.0006621 0.005 PASS
band_vs_conforming_thrust_max_rel abs 0.00254797 - 0 - 0.002548 0.01 PASS
band_vs_conforming_normal_max_rel abs 0.00311665 - 0 - 0.003117 0.01 PASS
band_force_vs_probe_thrust_max_rel report 0.00395324 - - - - - REPORT
band_force_vs_probe_normal_max_rel report 0.00127174 - - - - - REPORT
conforming_vs_femm_thrust_max_rel report 0.00330305 - - - - - REPORT
conforming_vs_femm_normal_max_rel report 0.00054354 - - - - - REPORT
gap_By_vs_femm_l2 report 0.00365421 - - - - - REPORT
gap_By_conforming_vs_femm_l2 report 0.00322282 - - - - - REPORT
band_gap_deficit_frac report 0.00129462 - - - - - REPORT
thrust_cogging_share report 0.162586 - - - - - REPORT
thrust_fundamental_N report 160.092 N - N - - REPORT
band_wrap_thrust_N report -125.532 N - N - - REPORT
mesh_nodes_band_ap report 38746 - - - - - REPORT
mesh_nodes_band_p report 76964 - - - - - REPORT
mesh_nodes_conforming report 38451 - - - - - REPORT
mesh_noise_thrust_max_rel report 0.00301804 - - - - - REPORT
mesh_noise_normal_max_rel report 0.000742146 - - - - - REPORT
mesh_nodes_band_ap_alt report 50837 - - - - - REPORT
gap_By_band_T report (series) - - - REPORT
gap_By_conforming_T report (series) - - - REPORT
normal_force_antiperiodic_N report (series) - - - REPORT
normal_force_conforming_N report (series) - - - REPORT
normal_force_mesh_control_N report (series) - - - REPORT
thrust_antiperiodic_N report (series) - - - REPORT
thrust_conforming_N report (series) - - - REPORT
thrust_mesh_control_N report (series) - - - REPORT
  • band_force_vs_probe_thrust_max_rel: Nabla's own linear-band force pair against this case's hand-integrated stress contour, on ONE solution. Reported rather than judged: it compares two instruments and so says nothing about the band, the window or the code - but it is the number that says whether the instrument this case built is the one the product reports. The two contours are separated by nothing but current-free air, so they integrate the same flux
  • band_force_vs_probe_normal_max_rel: The same for the normal force
  • conforming_vs_femm_thrust_max_rel: Nabla WITHOUT a band against FEMM: the same conforming geometry, the same instrument, two codes. Reported rather than judged because the plan's 2 % row is the banded one; this is the number that separates a cross-code disagreement into the part the band owns and the part the codes owe each other
  • conforming_vs_femm_normal_max_rel: The same for the normal force
  • gap_By_vs_femm_l2: Relative L2 of the airgap flux density along the shared contour, banded Nabla against FEMM, with the mover half a pole pitch on. Reported rather than judged: the plan's tolerances are on the force, and a pointwise field comparison across two meshers on a semi-closed slot opening would be measuring the meshes
  • gap_By_conforming_vs_femm_l2: The same field comparison for the conforming Nabla model, which is geometrically the same drawing FEMM was given
  • band_gap_deficit_frac: The unmeshed strip between the two band lines as a fraction of the magnetic circuit it is missing from (2.01 mm of airgap plus 6 mm of magnet at mu_r = 1.05). Arithmetic on the geometry, not a measurement: it is the systematic bias every band-against-something row is read through, and it is why the FEMM and conforming rows are expected to sit slightly low rather than symmetrically about zero
  • thrust_cogging_share: Amplitude of the slot-pitch (third) harmonic of the thrust as a fraction of its peak. Published because it is what decides whether the tolerances in this case are measuring the solver or the instrument: with fully open slots this machine's detent force was as large as its useful thrust, which is the regime V05's F2 and V14's cogging rows say not to judge. The slots are semi-closed for that reason
  • thrust_fundamental_N: Amplitude of the fundamental (one period per pole pitch) of the thrust - the current-driven force the machine is for, set beside the cogging share
  • band_wrap_thrust_N: Thrust at record 8, the mover one whole pole pitch on: the anti-periodic band's constraint has wrapped all the way round with its sign flip, and the machine is its own anti-image (magnets reversed, currents unchanged). Published as a number because there is no closed form for it; it is JUDGED, against the periodic window, inside thrust_periodic_N
  • mesh_nodes_band_ap: Mesh nodes of the anti-periodic banded model
  • mesh_nodes_band_p: Mesh nodes of the periodic banded model, whose window is twice as wide
  • mesh_nodes_conforming: Mesh nodes of one conforming model - the same window as band_ap without the two band lines and the strip between them
  • mesh_noise_thrust_max_rel: The case's own mesh noise on a thrust row: band_ap solved again with every mesh edge scaled by 0.87, so no division count and no interior element is shared, at the same nine displacements of the same band. Reported rather than judged - there is no tolerance on a mesh - and it is the number the periodic-against-anti-periodic row must be read through, because those two windows are different lengths and cannot carry the same node spacing either
  • mesh_noise_normal_max_rel: The same for the normal force
  • mesh_nodes_band_ap_alt: Mesh nodes of the mesh-noise control model
  • gap_By_band_T: extracted but not frozen in expected.json - reported only
  • gap_By_conforming_T: extracted but not frozen in expected.json - reported only
  • normal_force_antiperiodic_N: extracted but not frozen in expected.json - reported only
  • normal_force_conforming_N: extracted but not frozen in expected.json - reported only
  • normal_force_mesh_control_N: extracted but not frozen in expected.json - reported only
  • thrust_antiperiodic_N: extracted but not frozen in expected.json - reported only
  • thrust_conforming_N: extracted but not frozen in expected.json - reported only
  • thrust_mesh_control_N: extracted but not frozen in expected.json - reported only

Thrust over one pole pitch, three ways

Normal force over one pole pitch

Airgap flux density along the shared contour, Nabla against FEMM

5. Discussion

The case's own claim is comfortable. The plan asks for the force pair against FEMM at 2 %; Nabla's linear-band force path lands at +0.49 % on thrust and +0.32 % on the normal force, worst position, across a sweep in which the thrust changes sign and the normal force changes by a factor of two. The airgap field along the shared contour agrees to +0.37 % in relative L2.

The band is not where the cross-code difference lives. The conforming Nabla models - the same drawing FEMM was given, no band anywhere - differ from FEMM by +0.33 % on thrust, which is most of the +0.49 % the banded model shows. What the band adds on top is the band-against-conforming row: +0.25 % and +0.31 %, both inside the 1 % V14 fixed for the same comparison on the rotary band. Splitting the cross-code number that way is the reason the eight conforming models exist; without them, 0.6 % against FEMM would be an unattributed 0.6 %.

The periodic-against-anti-periodic row passes, and getting it there took a convergence study rather than an argument. The plan fixes 0.5 % and the measurement is +0.31 % on thrust (+0.07 % on the normal force). The case's first draft failed this row at 1.33 %, and the two explanations it offered were both wrong; case V22 finding F3 is the full account, and the short version is worth keeping here because it is the only row of this case whose value depended on a decision about the mesh:

  1. It was not the wrap. The deviation is spread across the sweep and is smallest at record 8 - the mover one whole pole pitch on, where the anti-periodic band has carried its constraint all the way round with the sign flip that makes it anti-periodic, and the periodic band on a window twice as wide has not wrapped at all. A wrap defect would show up there and nowhere else; this is the opposite pattern.
  2. It was not the two windows being incommensurable. The first draft rounded every division count up to a prime (a workaround for the Triangle crash below) and blamed the row on the resulting different node spacing. With that workaround removed the two windows became commensurate - band_p at exactly 2× band_ap's node count - and the row got worse, 1.33 % to 1.66 %. Triangle's Steiner insertion is not a function of the boundary counts, so the two interiors are independent either way.
  3. It was the mesh being too coarse, and it converges. Refining band_ap and band_p uniformly: 1.655 % at the first draft's mesh, 0.307 % at half the edge length, 0.356 % at 0.4. The row falls by 5× and then sits on a floor of about 0.3 %.

So the mesh is refined by 2× (build.NABLA_MESH_REFINE) and the tolerance is not moved: it is still the 0.5 % the validation plan fixed before this case was ever pointed at Nabla. The honest reading is that the linear band's periodic and anti-periodic forms agree to about 0.3 %, and that the case cannot say what that remaining 0.3 % is - its own mesh-noise control (band_ap_alt, the same band and the same window on an independently scaled mesh) reads +0.30 % on thrust and +0.07 % on the normal force, which is the same number as the row itself. The two windows' residual difference and the cost of any two meshes of this machine are indistinguishable here, and both are inside the tolerance.

That control is also what the first draft got wrong. It read 0.81 % of mesh noise beside a 1.33 % row and concluded the tolerance was below the instrument's resolution. Both numbers were properties of the same coarse mesh, and refining it moved them together - 1.33 % to 0.31 %, 0.81 % to 0.30 %. A noise floor measured at one mesh bounds that mesh, not the case.

Two findings the case made in the product, both since fixed.

F1 - a linear motion profile's speed was metres per second, whatever the model's length unit. UnifiedFormat.saveMotionMFS sent the profile speed out unscaled while multiplying the same motion's initialPosition by lengthUnitScaling, and Results.interpolateLinearOffset, the solver's applyBC and MotionChecks.strokeInModelUnits all divided the speed back - three readers agreeing with each other and with nothing else in the model. On a millimetre model a speed of 30 therefore meant 30 m/s. The symptom was not an error: 30 mm/s written as 30 advanced the mover by exactly one pole pitch per step, so the sweep alternated between two positions - each a perfectly converged, perfectly plausible solution - instead of walking. Fixed: a linear profile speed is now model length units per time unit, like the initial position beside it and like the circular peer's degrees per second. The wire is unchanged, so this case's .mfs and its results are byte-identical across the fix; a model written against the old behaviour needs its linear speeds multiplied by 1000 on a millimetre model.

F2 - a Triangle crash looked like a missing file. Nabla runs Triangle with -X (no exact arithmetic), and every boundary of this machine is axis-aligned, so two parallel lines whose division counts share a factor put nodes at the same abscissa and four such nodes are an exactly degenerate incircle test. The divide-and-conquer Delaunay does not warn: the process died with 0xC00000FD, a stack overflow, and Nabla reported "Node or edge file not found" - because the exit status was logged and dropped, and the mesh files Triangle leaves under a fixed name may still be the previous mesh. Fixed: Mesher.GenerateMesh now names a non-zero status, retries the same input with -i (incremental Delaunay, which meshes it), and refuses a mesh that was never written. The prime division counts the first draft used to route around the crash are gone with it.

The unmeshed band strip is 0.13 % of the magnetic circuit and is the one systematic bias in the band's favour or against it. It is why the band rows are expected to sit slightly low rather than symmetrically about zero, and at this size it is well under every row it could move.

6. Limitations

  • One machine, one operating point, one excitation. The currents are DC and fixed for the whole sweep. Nothing here tests a linear machine driven by a circuit, by eddy currents, or at a synchronous operating point.
  • Linear iron. No saturation anywhere, in either code. V02 is the BH case.
  • P1, planar, static physics. The linear band is not tested at polyOrder = 2 here, nor with eddy currents, nor with more than one motion zone. The linear peer of V14's mover/mover interface is untested.
  • The force pair comes off a single mid-band contour. Unlike the rotary torque, which Results.bandGapLayerTorque averages over the whole airgap layer (Arkkio) precisely because a single circle reads the mesh (case V05 finding F2), the linear force is one contour. This case's thrust is dominated by a current-driven fundamental, so it does not expose that; a detent-force sweep of a linear machine, where the net is orders of magnitude below the integrand, would, and it is not attempted here.
  • The 0.5 % row needs this mesh. It passes at NABLA_MESH_REFINE = 0.5 and fails at 1.0 (see §5), so the claim this case supports is that the periodic and anti-periodic linear bands agree to about 0.3 % when both are resolved. The floor is the two windows' band rings, not either mesh, and this case does not establish how that floor scales with ring length or airgap.
  • The refinement is bounded above by the solver. At roughly 180 000 nodes solver.exe dies with a stack overflow of its own (0xC00000FD), which is why the study stops at 0.4 and the case runs at 0.5 (77 000 nodes on the widest model). That crash is unexplained and is not this case's subject.
  • The FEMM comparison is across two mesh densities now. geometry.json's mesh_edge_mm still describes the mesh FEMM's frozen reference was computed on; NABLA_MESH_REFINE refines Nabla only. Both codes are converged well inside the 2 % the row is judged at, but the two meshes are no longer the same drawing refined the same way.
  • FEMM is a code, not a measurement. Two 2D FEM codes agreeing to 0.6 % is evidence that neither has a gross error in this configuration; it is not evidence about reality. There is no measured linear machine in this chapter.

7. Reproduce

cd validation
python -m harness.run_case V22

Requires a built triangle.exe (Nabla does not distribute it) and a solver binary. No gated feature is used, but since the mesh was refined for §5 every model is over the free tier's 25 000-node cap (38 000 on the one-pole-pitch window, 77 000 on the periodic one), so the case needs a licence that lifts it. Artifacts, including all eleven models, land in cases/V22_linear_machine/artifacts/.

To regenerate the third-party reference (needs pyfemm and FEMM 4.2 on Windows):

cd validation/cases/V22_linear_machine/reference
python femm_linear.py

8. Changelog of the frozen reference

No entry: the reference values and tolerances are as first frozen.