Skip to content

feat(nd): unit axes for the ND solver families via dimensionless twins - #202

Merged
mgyoo86 merged 28 commits into
masterfrom
feat/nd-unit-forward
Aug 2, 2026
Merged

feat(nd): unit axes for the ND solver families via dimensionless twins#202
mgyoo86 merged 28 commits into
masterfrom
feat/nd-unit-forward

Conversation

@mgyoo86

@mgyoo86 mgyoo86 commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

Extends #201's unit-native solver core to the ND solver families (Cubic/Quadratic PreCompute): build, eval, derivatives, integrate (full + bounded), one-shot, gridded queries, GridIdx and FillExtrap all work on unit-carrying axes, mixed units included. The blocker was the shared 2^N nodal-derivative store, whose slots differ by derivative order ([Y], [Y/X₁], [Y/X₁X₂]) and so cannot live in one homogeneous array. Each axis is therefore reparameterized to its exact dimensionless twin t = x·inv(oneunit(x)), the store stays [Y]-homogeneous, and grid⁻ᵏ is restored at a single cell seam.

Mechanism

  • One witness. _reparam_op(x) = x * _deriv_oneunit(x, DerivOp(1)) — the gate probes it, the twin applies it, its eltype comes from it. Eval never builds a twin: it scales h/inv_h/dL per query and restores the grading with one scalar.
  • Lazy twin. The two array-consuming seams (fiber solve, separable integrate) ask an axis only for getindex and _get_h/_get_inv_h, so _ReparamAxis is a view over the parent plus two scalar witnesses — no per-call array, widths straight from the parent's cache. Range axes keep the arithmetic form (already isbits, and it preserves the _CachedRange cache-bank fast path).
  • Dispatch, not value branches. Solve frame, cell-seam restore, local params, integrate and fill-eltype all pick their arm from the promotion tag, with the Real arm an identity method — so Real executes the pre-existing bodies verbatim.

Rides along

  • GridIdx in ND batches carried its val = NaN poison into the kernel: on an interpolating axis, linear/cubic returned NaN and Constant a wrong cell — silently, on Real grids, since v0.4.17. Existing coverage only pinned GridIdx on NoInterp axes, whose kernel never reads the coordinate.
  • FillExtrap promotion space — 1-D constant_interp/cubic_interp, ConstantInterpolantND and gridded constant promoted the fill against the raw data eltype, so Int data plus FillExtrap(0.5) threw InexactError at construction.
  • Smaller: the solver gate now probes the transform it promises (an oneunit-less duck used to pass), Hermite ND gates non-Real axes at both entries, the gridded fused path declines them, and _normalize_bounds_nd unrolls its bounds tuple.

Numbers

Real is unchanged by A/B: linear 10 k batch 12.2 µs both sides with values ===, ND 20 k batch within noise, 0 B throughout. Unit axes now allocate nothing on the hot paths (one-shot 3328 B → 0, integrate full 7232 B → 0, bounded 7424 B → 0) and bounded integration went 245 µs → 114 µs, matching Real, because the lazy twin keeps the parent's cached widths. Eval values are bit-identical; bounded integrals move ≤ 1 ULP.

Deferred

Adjoints/AD, Hermite ND and the hetero families on unit axes all refuse with actionable errors. Two query shapes stay broken and are now recorded with @test_broken, both verified identical on the v0.4.17 tag: 1-D batch GridIdx (loud — the 1-D loops never resolve the wrapper) and SoA queries with a pinned scalar axis (no protocol arm for a mixed (vector, scalar) tuple; fixing it is a design choice since the NoInterp pre-slice entry uses the same shape).

mgyoo86 added 27 commits August 1, 2026 10:16
…params, grid⁻ᵏ restoration at the cell seam
…ll params, shared reparam helpers hoisted to nd_utils
…e + composition-gap pins

The ND-internal fiber entry (_deriv_1d!) never runs the 1D builders'
payload normalize, so a Real Deriv2(0.0) beside unit ND data wrote 0.0
straight into the [Y]-typed RHS (DimensionError at _compute_rhs_first!).
_scale_bcs_reparam(bcs, grids, data) now threads a data sample so Real
structural payloads take the canonical _payload_val rehydration — on the
dimensionless t-axis the payload space IS [Y].

New testitem pins the composition surface: 3D mixed-unit (s x m x kg)
build/eval/deriv/integrate vs twin, per-axis PolyFit{4}, Real-zero
BCPair (=== against the ZeroCurvBC mint), exclusive-periodic unit axis,
and gradient! into an eltype-compatible store.
…the pool pipeline, op-aware Tr

Cubic/quadratic ND one-shots (scalar, batch, in-place) now accept the same
Real-or-unit axes as the persistent builders:

- Gates unified: all five one-shot entries run the per-axis
  `_check_nd_reparam_grid` probe; `_check_nd_solver_grid` is deleted.
- Pool backends (cubic/quadratic x scalar/batch) grow a Tg-folded twin arm —
  `_reparam_grids` + `_scale_bcs_reparam` feed the solve, the pooled partials
  stay [Y]-homogeneous, eval keeps reading the unit axes via
  `_compute_all_local_params_reparam`, and the cell seam restores grid^-k
  units through `_nd_fill_deriv_scale`. Real bodies are branch-preserved
  verbatim (type-folded conditions only).
- `Tr` at the four public entries unified to linear's op-aware canonical
  (`_deriv_eltype_nd` over `_nd_value_eltype`) — unit derivative results and
  batch buffers are typed in value/coord^N space; identity on Real grids.
- Batch backends drop the common-`Tg` grids pin (mixed-unit axes arrive as a
  heterogeneous per-axis-float tuple; the linear sibling relaxed first).
- `_cache_axes_pooled` mirrors `_convert_cache_axes`' abstract-tag arm
  (mixed-unit axes wrap at their own eltype).
- AutoCoeffs + non-Real scalar query resolves to the PreCompute pool by
  dispatch (the OnTheFly arm is Real-tuple-only); explicit OnTheFly rides the
  hetero collapse and gates via `_check_nd_hetero_grid`.

Gotcha fixed along the way: `@with_pool`'s typed-checkpoint local scan only
recognizes plain-symbol assignments, so an `acquire!` eltype bound by tuple
destructuring is hoisted above its definition (runtime UndefVarError) — the
twin arms use per-branch simple assignments.

Tests: new P5b mirror testitem (scalar/batch/in-place vs persistent, deriv
units, same-unit concrete-Tg, unit Range axis, exclusive periodic); F17
narrowed to the hetero families; F20 one-shot pins flipped to parity.
…h seams

Every Real-vs-reparam decision introduced on this branch now lives in a
dispatch arm instead of a call-site ternary ("dispatch, not a boolean test" —
the codebase's own gate idiom):

- `_reparam_solve_frame(grids, bcs, data)` — solve-frame selection for the
  scaled-store families (persistent build x2 + one-shot pool x4); the Tz line
  collapses to the build's canonical
  `_promote_eltype(op, _promote_grid_eltype(grids_solve), Tv)` everywhere.
- `_compute_all_local_params` 5-arg is constrained to `Tg <: Real` (its body
  is Real-width semantics) and gains a `::Type` fallback that routes to the
  dimensionless collapse — the six local-params call sites call it
  unconditionally with the width tag in scope.
- `_restore_nd_deriv_scale(r, grids, ops)` — the cell-seam grid^-k
  restoration x6; the Real arm is an identity METHOD (branch preservation
  stays load-bearing, no x1.0).
- `_integrate_reparam` ternary becomes tag-dispatch arms.
- `_oneshot_fill_eltype(Tg, Tv)` — cubic one-shot fill/extrap space.

Real arms are the exact previous computations (per-site type-identity
checked); the per-branch assignment blocks that worked around @with_pool's
destructuring blind spot disappear with the branches themselves.
…asing

Comment-only pass over the branch's additions: gate/build/eval/one-shot blocks
tightened to their load-bearing constraints, and two sentences left stale by
the dispatch refactor ("type-folded branch at the call sites", the inline
`true`-fold note) rewritten against the current seams.
…ector{BCPair} solve arms

Two sibling arms skipped the grid-aware `_normalize_bc(::BCPair, x, y)`
rehydration, so structural Real zeros beside unit data died in the solve
(`DimensionError: K m⁻¹ and 0.0 m`):

- The solve-side `_normalize_bc_array` overload for `Vector{<:BCPair}`
  delegated to the type-only 3-arg path. Deleted — the general per-element
  grid-aware arm handles `Vector{BCPair}` correctly as-is.
- The 3-arg catch-all dropped `(x, y)` for bare `PointBC`s, so scalar
  `bc = Deriv2(0.0)` (plain and Series) never rehydrated. New one-line arm
  routes through the pair rehydration: `_normalize_bc(BCPair(bc, bc), x, y)`.

Nonzero unitless payloads now get the same actionable ArgumentError on these
arms as on the scalar BCPair path (previously a deep DimensionError). Pins:
four success shapes === the ZeroCurvBC mint + two rejection-contract pins.
…on + FillExtrap surfaces

F14 pinned only the Linear family. Extend it with the campaign's surfaces —
cubic/quadratic persistent eval/deriv/integrate (full + bounded), one-shot
scalar/PreCompute/deriv/batch, and FillExtrap in-domain/OOB/deriv-OOB — plus a
1D sibling pin for the rehydrated bare-PointBC / Series-array builds and unit
FillExtrap. All 16 audited call shapes infer concretely with the expected
unit-typed results today; these pins keep it that way.
…pace

`_scale_payload_reparam`'s Real arm witnessed against the data sample alone
([Y]), so Real data on unit axes silently accepted a dimensionally incomplete
nonzero payload (`Deriv1(0.25)` read as 0.25/axis-unit) that the 1D builders
reject. The witness is now `first(data) * inv(u)` — the payload's true [Y/Xᵏ]
space — rehydrated there and then scaled into the [Y] store. Structural zeros
mint identically; typed payloads are untouched.

Pins: nonzero Real payload beside Real data + unit axes → the 1D-actionable
ArgumentError; zero-mint === parity; typed [Y/X] payloads stay accepted.
… parity)

Real axes float Int/Rational/Complex{Int} data at the entry, so FillExtrap
payloads normalize into the float value space. The unit-axis arm of
`_value_type` fell to the duck passthrough and kept promotable data raw —
`FillExtrap(0.5)` then normalized into Int and threw InexactError (every ND
family; linear included). A layered dispatch arm now floats promotable values
for non-Real tags while duck REAL grids (Dual) keep the raw-value contract.
Float32/ComplexF64 are fixpoints of `float`, so only non-float promotable data
changes — and now converts at build exactly like the Real-axis entry.

Pins: Int data + FillExtrap(0.5) across linear/cubic persistent + cubic
one-shot on unit axes, plus in-domain === parity with the Real-grid build.
…tity coordinate

`_resolve_grididx` rebuilds the wrapper at the axis eltype, but the payload was
pinned `T <: Real`, so every unit-axis GridIdx query (persistent and one-shot,
all families) died in the constructor with a TypeError. The struct and inner
ctor now admit `T <: Number`.

The promotion contract deliberately stays `S <: Real`: Unitful's generic
Quantity-vs-Real rules see the wrapper (a Real) and nest the numeric type
(`Quantity{Quantity{…}}` + convert ambiguity) when promoted against Quantity.
Unit coordinates instead flow through the one value-consuming seam — a direct
`-(g::GridIdx, x::Number) = g.val - x` method covering every dL/α read; Real
payloads fold identically.

Pins: persistent all-GridIdx / mixed tuples (cubic + linear, === parity),
one-shot PreCompute + mixed-tuple Auto mirrors, and the all-GridIdx+Auto
hetero-collapse refusal contract.
…ore serves

The cubic/quadratic GriddedQuery fast path stores PHYSICAL h/inv_h/dL anchors
and calls the cell kernels directly, bypassing the scaled-store restore seam —
on unit axes the persistent form threw DimensionError and the unified one-shot
(`interp(grids, data, gq; method = CubicInterp())`) died on a Quantity cache
key. Dispatch now declines for non-Real tags:

- persistent `_gridded_eval_itp_methods!` cubic/quadratic arms are constrained
  to `Tg <: Real`; unconstrained sibling arms return `false`, so the shared
  restore-aware `_nd_batch_pointwise!` core serves unit axes.
- the one-shot hooks route through a tag-dispatched `_*_gridded_oneshot_try!`
  pair — non-Real declines to the query-protocol batch path.

Real axes keep the fused path untouched (specificity, not a boolean test).
Pins: persistent value === pointwise, deriv ≈ (two FP orderings), quadratic
parity, and the unified one-shot mirror.
…load space

The quadratic scalar and in-place entries passed raw `Tv` to the eager
`_resolve_extrap`, so Int data + `FillExtrap(0.5)` threw InexactError at
resolve time (even for in-domain queries) — on Real axes (pre-existing)
and on the newly opened unit axes alike. `_oneshot_fill_eltype` moves to
the shared ND home, parameterized by the family coeff witness
(`_coeff_op2` cubic / `_coeff_op` quadratic); the in-place entry reuses
`_nd_promote_grids`' promoted `Tv_p` exactly as the cubic mirror.
… refusal

Hermite ND has no scaled-store/reparam seam (user partials live per-axis
in [Y/Xᵈ]); unit axes died in deep MethodErrors — persistent ctor in
`_pack_and_extend_nodal_derivs`, one-shot in abstract-Tv coerces — on
master and this branch alike (A/B probed, not a branch regression). Both
entries now run `_check_nd_hetero_grid` (the F11/F17 refusal contract),
which also keeps the reparam params fallback structurally unreachable
from non-solver families. Real axes and the whole Hermite ND Real suite
are untouched.
…×deriv on unit axes

The reparam bounded arm's `sign == 0` short-circuit (`z_t * vol`) and the
GridIdx `-` seam feeding the reparam dLs had no unit-grid coverage — both
probe correct today; pin the zero's [Y·X₁·X₂] type, the one-flip sign, and
bit-identity of GridIdx derivs against flat coordinates (cubic + quadratic).
`_check_nd_reparam_eltype` probed `promote_op(_coeff_op, Tg, Tg)` — an op
that never calls `oneunit` (and misuses `Tg` in the value slot) — so a
`*`/`inv`-complete duck Number without `oneunit` passed the gate and died
deep in float-promotion MethodErrors. The probe now runs `_reparam_op`
(`x * _deriv_oneunit(x, DerivOp(1))`), the canonical per-element twin
transform `_reparam_grids` applies, so the gate checks exactly what its
message promises. Unitful/Dual populations unchanged.
The adapter also backs the scaled-store in-domain restore seam
(_restore_nd_deriv_scale), not just FillExtrap OOB zeros — say so where a
"fill" grep lands.
… stop allocating

The scaled-store families reparameterized by materializing a twin axis per
call (`x .* inv(oneunit(x))`), so unit Vector axes paid an O(n) array on
every one-shot query and every `integrate` — and the twin, being a plain
Vector, also lost the parent's cached h/inv_h, making bounded integrate 2.2×
slower than its Real sibling.

Both consumers (the fiber solve and the separable integrate engine) only ask
an axis for `getindex` and `_get_h`/`_get_inv_h`, so the twin needs no array:
`_ReparamAxis` is a view that applies `_reparam_op` per access and takes its
widths from the parent's cache times a scalar. Range axes keep the arithmetic
form (Base's range broadcast is already isbits and preserves the
`_CachedRange` objectid fast path in the solve cache banks).

While pinning the contract, bounded integrate still allocated on mixed-unit
axes: `_normalize_bounds_nd` indexed the bounds tuple with a runtime `d`,
leaking the element Union — the same class its neighbours already unroll.

Measured (n=400 Vector axes): one-shot 3328 B → 0 B, integrate full 7232 B →
0 B, bounded 7424 B → 0 B and 245 µs → 114 µs (Real: 114 µs). Eval values are
bit-identical; bounded integrals move ≤1 ULP because the cached physical width
is scaled once instead of differencing two scaled coordinates.
`Base.:-(::GridIdx, ::Number)` was ambiguous against Base's
`-(::Real, ::Complex)` arms (GridIdx <: Real) — Aqua caught 2, and
disambiguating would need a method per Complex arm. The seams read the
payload directly instead: `_coord_value` is identity for every non-GridIdx
query, so Real paths keep their promotion behaviour (A/B: linear 10k batch
12.2 µs both sides, 0 alloc, values `===`).

Applied at each family's canonical coordinate seam — `_alpha_of`
(linear/constant 1D+ND, plus the exclusive-periodic wrapper's own arm), the
reparam local params (cubic/quadratic ND), and the Constant ND kernel's
`dL`. The Constant ND seam had no unit-axis GridIdx coverage; the pin now
sweeps every family × (all-GridIdx, mixed, derivative).
…ding

`oneunit(eltype)` twice plus a `first(inner)` element read (dead — only its
type mattered) become a single `u`. Same codegen (3 pointer copies + two
constant stores), one less way to trip on an empty axis.
A bare `GridIdx(k)` carries `val = NaN` until `_resolve_grididx` meets it
with its axis. Scalar entries resolve at the door; the ND batch loops did
not — so a GridIdx on an INTERPOLATING axis evaluated the kernel at NaN:
linear/cubic returned NaN, constant silently returned a wrong cell, and
unit axes threw a DimensionError. Existing batch coverage only put GridIdx
on NoInterp axes, whose kernel never reads the coordinate.

The query protocol grows a grid-aware `_extract_query_point`; the eight ND
eval batch loops pass the axes they already evaluate against. Identity for
non-GridIdx elements — A/B on 20k-point batches: linear 376.8→377.4 µs,
constant 372.8→374.9 µs, cubic 582.5→564.7 µs, 0 B throughout.
…data eltype

`constant_interp`/`cubic_interp` (1D), `ConstantInterpolantND` and the
gridded constant entry promoted a `FillExtrap` payload against `eltype(y)`,
so Int data + `FillExtrap(0.5)` threw `InexactError` at construction — even
though those same interpolants already return Float64 for Int data (the
kernels float against the grid). They now use linear's spelling,
`_value_type(Tv, <float grid type>)`. Unit axes inherit it through the same
call. Pins sweep 1D persistent (5 families) + one-shot + ND + gridded.
Both verified identical on the v0.4.17 tag, so neither is a regression from
this branch:

- 1-D batch `GridIdx` — the 1-D vector loops never resolve the wrapper, so
  the domain check sees `val = NaN` and throws. Loud, never silent.
- SoA with a pinned scalar axis — a mixed `(vector, scalar)` tuple has no
  protocol arm, so `_query_length` reports the tuple arity. Loud except when
  the vector length equals N, where the size check passes and the kernel
  reads past a scalar under @inbounds.

Each pin states the intended contract, so whoever fixes it gets an
"Unexpectedly Pass" instead of having to rediscover the case.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

FastInterpolations.jl Benchmarks

All benchmarks (56 total, click to expand)
Benchmark Current: 4b77d83 Previous Imm. Ratio Grad. Ratio
10_nd_construct/bicubic_2d 37370 ns 37630.0 ns 0.993 0.979
10_nd_construct/bilinear_2d 615.96 ns 628.6 ns 0.98 0.959
10_nd_construct/tricubic_3d 354813 ns 356766.0 ns 0.995 1.0
10_nd_construct/trilinear_3d 1684.74 ns 1843.6 ns 0.914 0.959
11_nd_eval/bicubic_2d_batch 1388.6 ns 1396.6 ns 0.994 0.996
11_nd_eval/bicubic_2d_scalar 17.74 ns 17.9 ns 0.989 0.994
11_nd_eval/bilinear_2d_scalar 8.51 ns 8.6 ns 0.988 0.993
11_nd_eval/tricubic_3d_batch 3262.1 ns 3228.1 ns 1.011 1.008
11_nd_eval/tricubic_3d_scalar 34.16 ns 34.4 ns 0.994 1.001
11_nd_eval/trilinear_3d_scalar 14.02 ns 13.8 ns 1.014 0.997
12_cubic_eval_gridquery/range_random 4222.92 ns 4224.7 ns 1.0 1.0
12_cubic_eval_gridquery/range_sorted 4217.1 ns 4215.3 ns 1.0 1.0
12_cubic_eval_gridquery/vec_random 9108.82 ns 9102.6 ns 1.001 0.985
12_cubic_eval_gridquery/vec_sorted 3217.02 ns 3207.8 ns 1.003 0.999
13_nd_oneshot_gridquery/bicubic_2d_rand_rand 65367.4 ns 65354.0 ns 1.0 1.006
13_nd_oneshot_gridquery/bicubic_2d_sort_rand 61999 ns 61839.4 ns 1.003 0.998
13_nd_oneshot_gridquery/bicubic_2d_sort_sort 60175.7 ns 59929.8 ns 1.004 1.001
13_nd_oneshot_gridquery/bilinear_2d_rand_rand 16507.04 ns 16079.2 ns 1.027 0.973
13_nd_oneshot_gridquery/bilinear_2d_sort_rand 8669.4 ns 9341.2 ns 0.928 0.913
13_nd_oneshot_gridquery/bilinear_2d_sort_sort 5132.6 ns 5143.4 ns 0.998 0.984
14_series_oneshot_batch/constant_inplace_vec_k8_q1000_rand 14194.5 ns 14919.8 ns 0.951 0.759
14_series_oneshot_batch/linear_inplace_vec_k8_q1000_rand 14324.7 ns 14144.4 ns 1.013 0.784
15_gridded_query/cubic_oneshot_2d_40x32 44700.6 ns 45023.0 ns 0.993 0.858
15_gridded_query/cubic_persistent_2d_40x32 8498.9 ns 8534.0 ns 0.996 0.987
15_gridded_query/linear_oneshot_2d_40x32 1526.06 ns 1530.9 ns 0.997 0.959
15_gridded_query/linear_oneshot_3d_12x10x8 2004.94 ns 2078.3 ns 0.965 0.973
15_gridded_query/linear_persistent_2d_40x32 1491.4 ns 1521.8 ns 0.98 0.975
15_gridded_query/linear_persistent_3d_12x10x8 2028.6 ns 2011.8 ns 1.008 0.95
1_cubic_oneshot/q00001 452.84 ns 463.9 ns 0.976 0.93
1_cubic_oneshot/q10000 43412.2 ns 43541.2 ns 0.997 0.999
2_cubic_construct/g0100 1310.06 ns 1309.4 ns 1.0 0.974
2_cubic_construct/g1000 12637.6 ns 12782.8 ns 0.989 0.999
3_cubic_eval/q00001 18.13 ns 18.9 ns 0.958 0.887
3_cubic_eval/q00100 443.62 ns 442.6 ns 1.002 1.001
3_cubic_eval/q10000 42643.8 ns 42629.6 ns 1.0 1.0
4_linear_oneshot/q00001 25.84 ns 25.8 ns 1.0 0.984
4_linear_oneshot/q10000 18434.5 ns 18518.6 ns 0.995 0.999
5_linear_construct/g0100 37.77 ns 32.5 ns 1.164 1.089
5_linear_construct/g1000 273.81 ns 284.1 ns 0.964 0.99
6_linear_eval/q00001 10.51 ns 10.5 ns 1.0 1.006
6_linear_eval/q00100 193.96 ns 194.6 ns 0.997 0.992
6_linear_eval/q10000 18155.9 ns 18148.8 ns 1.0 0.996
7_cubic_range/scalar_query 8.01 ns 8.0 ns 1.0 0.993
7_cubic_vec/scalar_query 10.41 ns 10.9 ns 0.954 0.966
8_cubic_multi/construct_s001_q100 502.94 ns 502.3 ns 1.001 0.844
8_cubic_multi/construct_s010_q100 4297.42 ns 4274.0 ns 1.005 0.965
8_cubic_multi/construct_s100_q100 39646.1 ns 40400.4 ns 0.981 0.991
8_cubic_multi/eval_s001_q100 579.68 ns 585.3 ns 0.99 0.758
8_cubic_multi/eval_s010_q100 1453.92 ns 1488.8 ns 0.977 0.832
8_cubic_multi/eval_s010_q100_scalar_loop 2306.32 ns 2334.3 ns 0.988 0.997
8_cubic_multi/eval_s100_q100 10242.1 ns 10124.9 ns 1.012 0.899
8_cubic_multi/eval_s100_q100_scalar_loop 3442.4 ns 3356.2 ns 1.026 1.019
9_nd_oneshot/bicubic_2d 37453.1 ns 37520.1 ns 0.998 0.92
9_nd_oneshot/bilinear_2d 451.84 ns 450.8 ns 1.002 0.999
9_nd_oneshot/tricubic_3d 363681.7 ns 363046.7 ns 1.002 0.928
9_nd_oneshot/trilinear_3d 908.6 ns 911.7 ns 0.997 0.997

⚠️ Performance Regression Confirmed ⚠️

After re-running 2 flagged benchmark(s) 10 time(s), 1 regression(s) confirmed.

Benchmark Current Previous Imm. Ratio Grad. Ratio Tier
5_linear_construct/g0100 37.77 ns 32.5 ns 1.164 1.089 immediate

Thresholds: immediate > 1.1x (vs latest master), gradual > 1.1x (vs sliding window)

Runner: znver3|4c — AMD EPYC 7763 64-Core Processor, julia 1.12.6. Times are min-merged and compared only against this same machine's history.

This comment was automatically generated by Benchmark workflow.

@mgyoo86 mgyoo86 changed the title feat(nd): unit-carrying axes for the ND solver families via exact dimensionless reparameterization feat(nd): unit axes for the ND solver families via dimensionless twins Aug 2, 2026
…s branch moved

Three CI failures, all from tests that pinned behaviour this branch changed:

- **Constant's fill space** was mine to revert, not the test's to update.
  `ConstantInterp` returns data values verbatim, so its fill belongs in `Tv`
  and `FillExtrap(NaN)` beside Int data is rejected at construction — Int has
  no NaN. Only Cubic keeps the promotion fix (it solves, so its value space is
  float). The sweep now says which families accept a float fill and why, and
  pins NaN propagation wherever the value space holds NaN (Float64/Float32).
- **Duck-grid lint** — the scaled-store policy seams are dispatch PAIRS whose
  unconstrained sibling serves duck/unit grids, the same shape as the existing
  `_inv_const` allowlist class. Enumerated with a reason, ratchet updated.
- **Duck BC boundary** — the bare-PointBC arm now follows #201's rule that a
  structural Real zero mints into the payload space, so `Deriv1(0.0)` builds
  on duck `y` while a nonzero payload still fails.
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.44898% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.31%. Comparing base (37e9f32) to head (4b77d83).

Files with missing lines Patch % Lines
src/core/nd_utils.jl 92.00% 4 Missing ⚠️
src/gridded/gridded_partials.jl 87.50% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           master     #202    +/-   ##
========================================
  Coverage   96.31%   96.31%            
========================================
  Files         164      164            
  Lines       13852    13965   +113     
========================================
+ Hits        13341    13450   +109     
- Misses        511      515     +4     
Files with missing lines Coverage Δ
src/constant/constant_interpolant.jl 94.44% <ø> (ø)
src/constant/nd/constant_nd_eval.jl 95.74% <100.00%> (ø)
src/constant/nd/constant_nd_interpolant.jl 100.00% <ø> (ø)
src/constant/nd/constant_nd_oneshot.jl 100.00% <100.00%> (ø)
src/core/axis_types.jl 90.90% <100.00%> (+0.21%) ⬆️
src/core/bc_types.jl 97.18% <100.00%> (-0.71%) ⬇️
src/core/eval_ops.jl 94.44% <100.00%> (+0.21%) ⬆️
src/core/interpolant_protocol.jl 100.00% <100.00%> (ø)
src/core/periodic_axis.jl 91.95% <ø> (ø)
src/core/query_protocol.jl 96.80% <100.00%> (+1.10%) ⬆️
... and 21 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mgyoo86
mgyoo86 merged commit 3563274 into master Aug 2, 2026
26 checks passed
@mgyoo86
mgyoo86 deleted the feat/nd-unit-forward branch August 2, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant