pub fn explain_difference(
a: &ResolvedSolveRequestV1,
b: &ResolvedSolveRequestV1,
ranges_m: &[f64],
) -> Result<SolutionDiffReportV1, KernelError>Expand description
Attribute the difference between two fully resolved solve results to the seven input groups (MBA-1345).
For each InputGroup g, the forward leg replaces g’s axes on a with b’s values and
measures the change against a; the backward leg replaces g’s axes on b with a’s
values and measures the change against b, negated. The group’s reported contribution is
the MEAN of the two, which is what makes the decomposition independent of which request is
treated as the “before” and which as the “after”. Whatever the seven groups do not explain
– genuine nonlinear interaction between them – is reported once per range, as
SolutionDiffRowV1::interaction_remainder, and is never distributed across the groups: for
correlated inputs there is no unique causal attribution, and pretending otherwise is the
failure this design exists to avoid.
ranges_m is passed straight through to two calls to evaluate per group (one per swap
direction), plus two more for a/b themselves – 16 solves for the fixed seven-group
taxonomy, each covering every range in ranges_m at once rather than once per range (see
evaluate’s own cost note). That 16 is NOT the total, though: building each group’s
counterfactual (swap_group) re-resolves via a full solve_v1 call after EVERY applied
axis, regardless of whether that specific axis is requires_rezero – not just the ones that
invalidate a zero – because with_axis always needs a freshly resolved request to apply
its NEXT write onto. For a request with N present axes (up to the full 32; 5-6 are commonly
absent, e.g. length_m/latitude_rad/the POI offsets, so N is often closer to 27), that is
up to N solves per direction, 2N total, ON TOP of the 16 – so a typical call costs on the
order of 16 + 2*27 = 70 solves, not 16. plan_exclusions itself adds no solves (it only
calls read_axis/with_axis, never solve_v1), so excluding an axis only ever reduces
this total, never increases it.
§Axes that cannot be swapped
See the module doc’s “Symmetric exclusion” section for the full explanation. In short:
with_axis can refuse an axis for a structural reason specific to a or b (a
QNH-referenced altitude, a compass-referenced shot azimuth, one of the three wind axes under
segmented wind), and an ordinarily-optional axis can be present on only one of a/b. An
axis affected by either is recorded in the returned report’s skipped_axes – one entry per
swap direction, always both together, even though only one direction may have hit the
refusal or absence directly – and excluded from BOTH legs of that group’s swap, so the two
legs keep measuring the same counterfactual; the rest of that axis’s group is still
attributed normally, and a refusal never aborts the comparison. An axis absent from BOTH a
and b (no length_m supplied on either, for instance) is skipped without being recorded:
there is nothing there to attribute either way.
§Errors
Returns whatever evaluate, with_axis, or plan_exclusions error on a, b, or
either swapped counterfactual, MINUS the refusal/absence cases above
(KernelError::AxisUnsupportedForRequest, the presence-asymmetry case, which never even
reaches a KernelError, and the two cross-axis conflicts plan_exclusions now also catches
up front – CoriolisEnabled differing while either request lacks latitude_rad, and
MagnusEnabled/EnhancedSpinDriftEnabled swapping between requests with opposite flags,
taxonomy.rs’s Known Limitation (d) – both F2, 0.33.0 final-review fix wave), which are
caught and recorded rather than returned. Every OTHER error – some other solve_v1
re-resolve failing validation partway through building a group’s counterfactual, not one of
the cases plan_exclusions already recognizes – propagates unchanged: it is a genuine
failure, not a structurally unrepresentable axis, and must not be silently absorbed into a
skip or a zero contribution.
a and b are read-only throughout: every counterfactual request is built from a clone
(swap_group), never a mutation of either input.