← Back to the validation dossier

Tier X (cross-code) — priority P0 Tolerances frozen on 2026-08-16. Nabla 0.1.0, solver licence mode enforcing, run in 315.3 s.

1. Problem

This is the comparison every prospective user runs anyway, so it is better done here, in public, with the models downloadable.

Four physics, six models, two codes, one process. Each model is built in Nabla, solved, read — and then, in the same run and the same minute, rebuilt in FEMM 4.2 through pyfemm, solved there, and read at the same points.

Sub-model Physics What it exercises
ccore nonlinear magnetostatic, planar BH inversion, coil linkage, body force, stored energy
round_50Hz / round_500Hz / round_5kHz time-harmonic with eddy currents, planar AC resistance, field displacement
spm magnetostatic with motion, planar, anti-periodic sliding band vs air-gap element, machine torque, cogging, back-EMF
annulus steady heat conduction, axisymmetric the thermal module, and the r weighting of an axisymmetric kernel

None of the four models is defined by this case. All four already exist in the dossier, each validated against a closed form in its own chapter, so V10 imports the sibling case's builder rather than typing the geometry a third time:

Sub-model Nabla side built by FEMM side built by
ccore V02_ccore/build.py V02_ccore/reference/femm_ccore.py
spm V05_spm_pmsm/build.py V05_spm_pmsm/reference/femm_spm.py
round_* V12_skin_effect/build.py this case's femm_side.py
annulus V08_heat_conduction/build.py this case's femm_side.py

The judged row shared_source_files_missing is what turns "nothing was re-typed" from a sentence into a check: it counts the eleven sibling files the chapter's models are built from and fails the case if one of them is not where the case says it is. It reads 0.

Only two FEMM models belong to V10 — the wire and the cylinder wall — because V12's and V08's references are closed forms and neither case had a FEMM script to borrow.

What V10 owns for all six is in models.json: the operating point of each family, the length of the machine's rotor sweep, and where each field map is sampled. That last one turned out to matter more than anything else in the case; §5.1 is about it.

2. Reference

FEMM 4.2 (28 Feb 2018 build), http://www.femm.info, driven live.

Every other cross-code case in this dossier freezes its FEMM numbers — the pattern V02 established, and what §3.5 of the plan asks for, so the nightly gate needs no FEMM installation and a third-party answer cannot move under a case between runs. §3.5 names exactly one exception, and it is this chapter:

Reserve the live-FEMM form for V10, whose subject is the side-by-side run.

So expected.json holds no reference values at all. Every quantity is declared "reference": "computed", and what was frozen before Nabla was pointed at anything is the list of compared quantities, the sampling rule for each field map, and the tolerance on each row — 1 % on integral quantities, 2 % on force and torque, 2 % L2 on field maps, transcribed from the plan. One tolerance is deliberately different and says so in its own row: torque ripple is judged at 5 %, because a ripple is a difference of two torques over their mean and at this machine's 4.5 % ripple it carries each code's torque error amplified twentyfold.

On a host without FEMM the case reports SKIP, never a silent pass.

The conventions each code's numbers are read under — FEMM's harmonic amplitudes against time-averaged block integrals, its air-gap-element torque covering the whole machine rather than the sector, its heat-flow line integral already being a revolved power — are set out in the case's reference notes rather than assumed here.

3. Nabla model

Sub-model Element order Nodes Elements
ccore P1 5196 10260
round_50Hz P1 1803 3541
round_500Hz P1 1805 3545
round_5kHz P1 5712 11359
spm P1 3122 6009
annulus P1 1770 3400

Everything geometric and material is the sibling case's. Three run settings are V10's own and are worth naming:

  • The machine runs 36 steps per electrical period, not V05's 180 — 2.5 mechanical degrees per step, six samples per 15-degree cogging period, which resolves the cogging harmonic h6 and its first overtone and nothing above. V05 owns the fine sweep and the spectrum. FEMM is driven at steps 0, 5, 10 … of its own 180-step convention so that step k is the same physical angle in both codes; the judged row spm_femm_angle_match_max_deg is what says so rather than leaves it to arithmetic done twice, and it reads 0.
  • The wire runs at 50, 500 and 5000 Hz, which is the plan's list and not V12's decade ladder. The mesh rule is V12's — 16 elements per skin depth, which is V12's own measured requirement for a loss integral rather than for a field.
  • The C-core runs at 5000 A-turns, V02's sweep point ni5: past the knee, so both codes are solving a nonlinear problem rather than two linear ones that happen to share a permeability.

4. Results

Quantity Metric Nabla Reference Error Tolerance Verdict
ccore_B_gap_T rel 1.55937 T 1.55578 T +0.231 % 1.000 % PASS
ccore_flux_linkage_Wb rel 0.810007 Wb 0.810767 Wb -0.094 % 1.000 % PASS
ccore_field_energy_J rel 3.09643 J 3.08774 J +0.281 % 1.000 % PASS
ccore_force_x_N rel -2017.78 N -2013.43 N -0.216 % 2.000 % PASS
ccore_B_map_T profile min=0.004373, max=1.998, rms=1.046 T min=0.003781, max=2, rms=1.045 T L2 +0.747 %, max +4.619 % L2 2.000 %, max - PASS
round_R_ac_vs_f_ohm_per_m profile min=0.002457, max=0.002755, rms=0.002561 ohm/m min=0.00244, max=0.002744, rms=0.002546 ohm/m L2 +0.601 %, max +0.623 % L2 1.000 %, max - PASS
round_L_int_vs_f_H_per_m profile min=4.72e-08, max=5.016e-08, rms=4.918e-08 H/m min=4.69e-08, max=4.987e-08, rms=4.889e-08 H/m L2 +0.597 %, max +0.593 % L2 1.000 %, max - PASS
round_J_profile_per_amp profile min=1.33e+05, max=1.657e+05, rms=1.407e+05 A/m^2 per A rms min=1.326e+05, max=1.653e+05, rms=1.402e+05 A/m^2 per A rms L2 +0.326 %, max +0.600 % L2 2.000 %, max - PASS
round_J_map_per_amp profile min=1.33e+05, max=1.662e+05, rms=1.419e+05 A/m^2 per A rms min=1.326e+05, max=1.654e+05, rms=1.415e+05 A/m^2 per A rms L2 +0.319 %, max +0.561 % L2 2.000 %, max - PASS
spm_flux_linkage_peak_Wb rel 0.218489 Wb 0.218662 Wb -0.079 % 1.000 % PASS
spm_bemf_fundamental_V rel 132.679 V 133.473 V -0.594 % 1.000 % PASS
spm_average_torque_Nm rel 37.412 N.m 37.4816 N.m -0.186 % 2.000 % PASS
spm_torque_ripple_pct rel 4.4696 % 4.46673 % +0.064 % 5.000 % PASS
spm_cogging_pp_Nm rel 0.665943 N.m 0.653244 N.m +1.944 % 2.000 % PASS
spm_cogging_h6_Nm rel 0.263681 N.m 0.259093 N.m +1.771 % 2.000 % PASS
spm_arc_Br_T profile min=-0.2394, max=1.024, rms=0.604 T min=-0.2396, max=1.027, rms=0.6039 T L2 +1.062 %, max +3.088 % L2 2.000 %, max - PASS
spm_coil_Jz_abs_A_per_m2 rel 1.06458e+07 A/m^2 1.06458e+07 A/m^2 -0.000 % 1.000 % PASS
spm_B_map_T profile min=0.003631, max=1.941, rms=0.7259 T min=0.004021, max=1.969, rms=0.7674 T L2 +18.210 %, max +38.828 % L2 -, max - REPORT
annulus_T_rise_profile_K profile min=1.463, max=74.51, rms=37.1 K min=1.462, max=74.51, rms=37.1 K L2 +0.015 %, max +0.038 % L2 1.000 %, max - PASS
annulus_T_rise_map_K profile min=1.462, max=74.54, rms=37.43 K min=1.461, max=74.51, rms=37.42 K L2 +0.016 %, max +0.046 % L2 1.000 %, max - PASS
annulus_heat_flow_W rel 108.789 W 108.814 W -0.023 % 1.000 % PASS
spm_phase_current_max_err_A abs 4.26326e-14 A 0 A 4.263e-14 0.001 PASS
spm_femm_angle_match_max_deg abs 0 degrees 0 degrees 0 1e-09 PASS
shared_source_files_missing abs 0 - 0 - 0 0 PASS
spm_femm_cogging_pp_vs_frozen_pct report -14.4454 % - % - - REPORT
spm_femm_cogging_h6_vs_frozen_pct report -9.42147 % - % - - REPORT
spm_femm_torque_vs_frozen_pct report 0.137958 % - % - - REPORT
spm_femm_linkage_vs_frozen_pct report 0.193932 % - % - - REPORT
round_current_ratio_at_profile_f report 0.150698 - - - - - REPORT
annulus_nabla_vs_exact_l2_pct report 0.022385 % - % - - REPORT
annulus_femm_vs_exact_l2_pct report 0.00848607 % - % - - REPORT
annulus_nabla_vs_exact_flow_pct report 0.0110999 % - % - - REPORT
annulus_femm_vs_exact_flow_pct report 0.0343397 % - % - - REPORT
round_current_ratio_vs_f report (series) - - - REPORT
round_kr_vs_f report (series) - - - REPORT
spm_cogging_vs_angle_Nm report (series) - - - REPORT
spm_torque_vs_angle_Nm report (series) - - - REPORT
  • spm_femm_cogging_pp_vs_frozen_pct: How far THE REFERENCE moved. This run's live FEMM peak-to-peak cogging against the value committed in validation/cases/V05_spm_pmsm/reference/femm_reference.json on 2026-08-09, at the same rotor angles, from the same script on the same unmodified inputs. Nothing about Nabla is measured here
  • spm_femm_cogging_h6_vs_frozen_pct: The same comparison on the physical cogging harmonic, which is the quantity V05's cross-code claim is quoted on
  • spm_femm_torque_vs_frozen_pct: The CONTROL for the two rows above: the same two FEMM runs' mean rated torque. A quantity that reproduces where cogging does not is what makes the finding a statement about cogging rather than about the machine
  • spm_femm_linkage_vs_frozen_pct: The second control: peak open-circuit flux linkage from the same two FEMM runs
  • round_current_ratio_at_profile_f: The current Nabla's wire actually carried at 5 kHz, as a fraction of the 1 A the coil source asked for. A time-harmonic coil source in Nabla is a VOLTAGE source (V12's finding), so a solid conductor does not carry its source current; FEMM's series circuit is 1.000 by construction. Every judged row of this family normalises by the carried current, which is why the difference is reportable rather than fatal
  • annulus_nabla_vs_exact_l2_pct: Nabla's own error against the closed form, as a percentage of the L2 norm of the temperature rise
  • annulus_femm_vs_exact_l2_pct: FEMM's own error against the same closed form, computed the same way
  • annulus_nabla_vs_exact_flow_pct: Nabla's heat flow through r = 25 mm against 2pikH(T1-T2)/ln(r2/r1)
  • annulus_femm_vs_exact_flow_pct: FEMM's heat flow through the same contour against the same closed form
  • round_current_ratio_vs_f: extracted but not frozen in expected.json - reported only
  • round_kr_vs_f: extracted but not frozen in expected.json - reported only
  • spm_cogging_vs_angle_Nm: extracted but not frozen in expected.json - reported only
  • spm_torque_vs_angle_Nm: extracted but not frozen in expected.json - reported only

4.1 The integrals

The quantities a 2D code is actually bought for agree across the board:

Nabla − FEMM Tolerance
C-core gap flux density +0.231 % 1 %
C-core coil flux linkage -0.094 % 1 %
C-core stored field energy +0.281 % 1 %
C-core armature force -0.216 % 2 %
SPM peak flux linkage -0.079 % 1 %
SPM back-EMF fundamental -0.594 % 1 %
SPM mean rated torque -0.186 % 2 %
SPM torque ripple +0.064 % 5 %
Wire AC resistance, 3 frequencies L2 +0.601 % 1 %
Wire internal inductance, 3 frequencies L2 +0.597 % 1 %
Cylinder-wall heat flow -0.023 % 1 %

Two of these are worth a sentence each. The stored field energy is the plan's first named quantity and the only integral here that reads the nonlinear energy density rather than a flux or a force; it lands at +0.281 %. And the armature force is compared like for like — Nabla's eggshell region integral against FEMM's weighted stress tensor over the same block, both weighted volume integrals over the body — for the reason V02 established: a single closed-line stress contour does not converge where a volume integral does, in either code, and pairing one against the other measures the two methods rather than the two codes.

The AC resistance rows carry a wrinkle that had to be dealt with rather than assumed away. The two codes are not driven the same way and cannot be: a time-harmonic coil source in Nabla is a voltage source, so a solid conductor given a 1 A source carries 0.150698 of it at 5 kHz, while FEMM's series circuit imposes the current exactly. That is V12's own finding, and the answer is the same one V12 used — every judged quantity is normalised by the current each code actually carried, measured from the slope of A against ln r outside the wire. R_ac, L_int and J per ampere are all drive-independent; the raw loss is not, and is not judged.

4.2 The field maps

An integral comparison is blind to a local disagreement, which is exactly what this chapter is for.

C-core field map

Current density in the wire at 5 kHz

Temperature in the cylinder wall

Map L2 worst point Tolerance
C-core, |B| +0.747 % +4.619 % L2 2 %
Wire, |J| per ampere +0.319 % +0.561 % L2 2 %
Cylinder wall, temperature rise +0.016 % +0.046 % L2 1 %
SPM sleeve arc, B_r +1.062 % +3.088 % L2 2 %

Radial flux density through the stator sleeve

The machine's flux wave is read through the stator sleeve at r = 34.5 mm and not in the airgap, and the reason is structural rather than a preference: FEMM's air-gap element replaces the whole 33 → 34 mm annulus with a boundary, so there is no mesh there and no field to read. The sleeve is non-magnetic in both codes, so it is the same flux wave half a millimetre further out. The chapter says so rather than calling it the airgap field.

Both codes' radial component is assembled from Bx and By here, never from either code's own normal-component convention — the same discipline V02 used on its Ampère contour, and for the same reason: a sign convention is exactly the kind of thing that makes a waveform comparison silently compare a curve against its own reflection.

4.3 The one map that is reported and not judged

SPM stator field map

spm_B_map_T runs a polar grid over the whole stator annulus and reads +18.210 % L2, worst point +38.828 %. It was declared REPORT before the run, and it is in the chapter as the control for §5.1: it is what a field map looks like when it is not kept clear of material interfaces. The grid crosses a tooth/slot boundary at every tooth, and a sample on such a line reads 1.9 T or 0.05 T depending on which side its element fell on, independently in each code. Nothing about the two solutions is in disagreement there; the sampling is.

4.4 The one sub-model where "which code is right" has an answer

The cylinder wall has a closed form — T(r) = T1 + (T2 − T1)·ln(r/r1)/ln(r2/r1) — so both codes can be scored against it rather than only against each other:

vs the closed form
Nabla, temperature profile 0.022385 %
FEMM, temperature profile 0.00848607 %
Nabla, heat flow through r = 25 mm 0.0110999 %
FEMM, heat flow through r = 25 mm 0.0343397 %

Both are inside a twentieth of a percent on a logarithmic solution that is genuinely outside the P1 space, and the two conservation numbers straddle the exact answer. This is the strongest form the comparison takes anywhere in the chapter, and it is worth noting that it is the thermal module — the newest physics in Nabla — that supplies it.

5. Discussion

5.1 A field map is a sampling problem before it is a comparison

The first run of this case failed two rows, and both were the case's own construction rather than either solver's.

models.json originally dropped map points within 2.5 mm of a material corner, which is the plan's words — "away from material corners" — taken literally. That is not what the physics asks for. |B| is discontinuous across the whole length of an iron/air interface, not only where two of them meet: at 5000 A-turns this model carries 2.0 T on the iron side of x = 0 and 0.04 T on the air side. The grid landed exactly on the core's x = 0 and x = 20 faces, each code returned whichever side its own element fell on, and the row read +17.8 % L2 and +58 % max about two solvers that were in fact agreeing to a few tenths of a percent everywhere they were legitimately sampled. Excluding a strip around every interface — a strictly stronger rule, since every corner lies on a boundary — takes the same row to +0.747 %.

The lesson generalises past this case: a cross-code field-map comparison measures the sampling rule until the sampling rule is right, and a rule stated in terms of corners will pass a coarse grid and fail a fine one for no physical reason. §4.3 is that effect left visible on purpose.

5.2 Which solution is live is not obvious, and now has a witness

The second failing row read +39 % L2 on the machine's sleeve arc, and the cause was that Nabla's machine pipeline runs the no-load sweep and then the loaded one, and the solution file left live at the end is the second. So record 0 of a Nabla machine result is the loaded field at the as-drawn angle, not the open-circuit field — while getCoggingTorque and getFromPhase read stored per-stage data and are unaffected. The case was comparing Nabla's loaded field against FEMM's no-load field, and every waveform row still passed.

That is now impossible to repeat: spm_coil_Jz_abs_A_per_m2 probes the source current density at the centre of the first phase-A coil side in both codes, at the record the maps are read from, and a stage mismatch would take it from 1.06 × 10⁷ A/m² to zero. It reads -0.000 %. Magnitudes are compared so that neither code's current-direction convention can fail it for a reason that is not a disagreement.

5.3 The finding: FEMM's cogging torque does not reproduce

The four spm_femm_*_vs_frozen_pct rows measure nothing about Nabla. They compare this run's live FEMM answer against the FEMM answer V05 committed on 2026-08-09, at the same rotor angles, from the same script, on inputs that git says have not been touched since:

FEMM quantity live vs V05's committed
cogging, peak to peak -14.4454 %
cogging, physical harmonic h6 -9.42147 %
mean rated torque 0.137958 %
peak open-circuit flux linkage 0.193932 %

FEMM is bit-reproducible today — three consecutive rebuild-and-solve trials in one session returned identical torques to every printed digit — and the two control rows reproduce V05's committed values to under 0.2 %. Only cogging has moved, and it has moved by 9–14 %.

This matters because V05's headline cross-code finding is quoted on that number. V05 reports a converged −15 % between Nabla's cogging and FEMM's, and concludes that with Nabla's band and torque instrument exonerated, FEMM's air-gap element is the remaining hypothesis. Against a FEMM run made in the same minute, this case reads +1.771 % on h6 and +1.944 % peak-to-peak — inside the 2 % the plan sets for a torque. The discrepancy V05 reports is of the same order as the run-to-run scatter in the reference it is measured against, and is therefore not established at the precision it is quoted to.

What has not been established is what changed. The only difference this case could find between the two FEMM runs is that the meshed areas of the three arc-bounded regions differ by up to 1.6 × 10⁻⁴ relative while every straight-edged region matches to under 10⁻⁹ — so the two runs did not discretise the arcs identically, and cogging is a 0.65 N·m residual of a 40 N·m integrand and is hypersensitive to exactly that. The evidence, the ranked hypotheses, what would falsify each and a one-line reproduction apiece are in case V10.

The generalisable point is the one this chapter is best placed to make: a frozen third-party number is a measurement with its own reproducibility, and a cross-code case should say what that is. Freezing is still the right default — it is what stops a reference drifting silently — but a frozen number quoted to better than its own run-to-run scatter is over-quoted, and the only way to find out is to run it twice. That is the argument for keeping one live-FEMM case in the dossier, and it is now backed by a measurement rather than by the plan's say-so.

5.4 What the comparison is actually evidence for

Nabla reproduces FEMM where FEMM applies — nonlinear magnetostatics, harmonic eddy currents, a slotted machine with motion, axisymmetric heat conduction — to a few tenths of a percent on every integral quantity and to about a percent on a field map. That is the whole of the claim, and it is deliberately modest: two 2D finite-element codes solving the same weak form on the same geometry should agree, and the value of showing it is that a reader can check rather than believe.

The product argument sits next to it rather than inside it: transient motion, circuit coupling, the thermal chain, second-order elements and the automation this very case is written in are all outside what FEMM does, so they are validated elsewhere in this dossier — against closed forms — and not here.

6. Limitations

  • This is not a benchmark of FEMM. FEMM is a well-made free code that this audience already trusts, which is exactly why it is the reference. Where the two disagree, §4.4 is the only place this chapter is entitled to say which one is right, and there they agree.
  • Every model here is one FEMM can represent, which excludes most of what Nabla is for. There is no transient with a time derivative, no circuit coupling, no second-order element, no coupled thermal run, no multi-motion model, and no skew. Those are validated in V04, V08, V09, V11, V12, V19, V22 and V23 against references that are not another code.
  • One mesh per model. Nothing here is a convergence study; both codes are run at the mesh their sibling case fixed. V09 is the convergence case.
  • The machine's cogging is sampled at 36 points, which resolves h6 and h12 and nothing above. V05 owns the 180-point sweep and the spectrum.
  • The stator field map is not judged, by declaration and for the reason in §4.3.
  • spm_femm_*_vs_frozen_pct is a two-point comparison, not a distribution. It says the reference moved; it does not say how often, or how far it can.
  • FEMM's build is not pinned. The case records the FEMM installation it found but cannot make a third-party binary reproducible, which is half of §5.3's point.

7. Reproduce

cd validation
python -m harness.run_case V10

Needs triangle.exe (32-bit on Windows), a FEMM 4.2 installation and the pyfemm package. Without FEMM the case reports SKIP. About six minutes: six Nabla models — one of which is a 72-step two-stage rotor sweep — plus 77 FEMM solves, of which 72 are the same two sweeps.

--keep-work leaves every model, mesh and solution behind under artifacts/work/. The published .nbl models are in artifacts/models/, and every compared series — including both cogging waveforms and all four field maps — is written out point by point under artifacts/series/.

8. Frozen-reference changelog

Date Change Reason
2026-08-16 The C-core field map's exclusion rule changed from 'within 2.5 mm of a material CORNER' to 'within 2.5 mm of a material BOUNDARY'. Two rows were added that did not exist at the freeze: spm_coil_Jz_abs_A_per_m2 (judged, 1 %) and the four spm_femm_*_vs_frozen_pct report rows. No reference value moved, because this case has none - every reference is the live FEMM run - and no tolerance on a pre-existing row was touched. Both are construction defects in the case, found on its first run and fixed before it was published, and they are recorded here rather than quietly because the freeze date is stated. (1) The corner rule was a mis-implementation of the plan's 'away from material corners':