Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dd6a7d2
(test): pin ND deriv-zero carrier propagation + cross-path invariants
mgyoo86 May 21, 2026
ca95cbf
(fix): carrier-aware deriv-zero fill via `_deriv_zero_value` helper
mgyoo86 May 21, 2026
e64ffa0
(refac): rename `_zero_ref` trait → `_value_sample`
mgyoo86 May 21, 2026
b50f56e
(refac): rename `_value_sample` trait → `_sample_data`
mgyoo86 May 21, 2026
e2e3881
(refac): rename `_deriv_zero_value` args `sample_data` / `sample_quer…
mgyoo86 May 21, 2026
fa9fa82
(fix): linear 1D kernel deriv branches thread query carrier via `* on…
mgyoo86 May 21, 2026
8fe5b5b
(fix): cubic 1D kernel deriv branches thread query carrier via `* one…
mgyoo86 May 21, 2026
6740df9
(fix): quadratic 1D kernel deriv branches thread query carrier via `*…
mgyoo86 May 21, 2026
8da21fa
(fix): hermite 1D kernel deriv branches thread query carrier via `* o…
mgyoo86 May 21, 2026
55200a7
Runic formatting
mgyoo86 May 21, 2026
364c3e4
(refac): cell-local carrier-aware deriv-zero — remove protocol short-…
mgyoo86 May 22, 2026
c03c492
(fix): cell-local NaN propagates through `_promote_extrap_zero` (Numb…
mgyoo86 May 24, 2026
5c9e23e
(test): pin Cat E helper-level OOB type stability via `@inferred`
mgyoo86 May 24, 2026
5798994
(fix): cell-local NaN at Constant 1D right-edge + Series boundary/fil…
mgyoo86 May 24, 2026
5de6ec2
(fix): cell-local NaN at Hetero NoInterp deriv-zero short-circuits
mgyoo86 May 24, 2026
3ac14d1
(revert): Series cell-local deriv sites — restore master broadcast pa…
mgyoo86 May 25, 2026
7f50a80
(chore): address pre-merge code review feedback
mgyoo86 May 25, 2026
fd60b55
(chore): drop redundant deriv-zero comment in `_eval_constant_series_…
mgyoo86 May 25, 2026
a6a11b2
(test): extend `@test (@inferred EXPR) isa T` to duck-typing tests
mgyoo86 May 25, 2026
483b48a
(fix): Constant right-edge short-circuit threads grid carrier (Tg)
mgyoo86 May 25, 2026
da84d34
(fix): Constant ND oneshot OOB FillExtrap deriv returns zero, not fil…
mgyoo86 May 25, 2026
82ea307
(fix): Linear ND `_linear_weight(::EvalDeriv1)` threads Tq carrier
mgyoo86 May 27, 2026
724bd6c
(fix): Constant Series deriv path — unified `_constant_kernel(op, ...…
mgyoo86 May 27, 2026
951defa
(fix): Hetero mixed-axes deriv-zero — `result * 0` carrier-preserving…
mgyoo86 May 27, 2026
70dd5df
(chore): pre-merge code-review followups
mgyoo86 May 27, 2026
cde46fb
(test): Cat B ND non-zero deriv — full method × path sweep
mgyoo86 May 27, 2026
c16d696
Runic formatting
mgyoo86 May 27, 2026
5f20143
(fix): Hetero mixed OOB + FillExtrap × NoInterp deriv must not leak f…
mgyoo86 May 27, 2026
81f221c
Revert "(fix): Hetero mixed OOB + FillExtrap × NoInterp deriv must no…
mgyoo86 May 27, 2026
a9b8458
(fix): Series oneshot batch — restore LTS @inferred via type-arg barrier
mgyoo86 May 27, 2026
5408d2c
(test): InferredCompat @testsnippet with `@maybe_inferred` macro
mgyoo86 May 27, 2026
4150335
(chore): drop unused `@maybe_inferred` / `InferredCompat` snippet
mgyoo86 May 27, 2026
2e1ff4e
(fix + refac): FillExtrap × deriv — cell-local "fill_value-as-data" c…
mgyoo86 May 27, 2026
e1b0633
(refac): rename `_extrap_deriv_source` → `_extrap_oob_data`
mgyoo86 May 27, 2026
5d655a3
(test): update remaining FillExtrap × deriv tests for option-B contract
mgyoo86 May 27, 2026
e13eff6
(refac): unify `_eval_extrapolation` value + deriv paths via `_extrap…
mgyoo86 May 27, 2026
12d9701
Runic formatting
mgyoo86 May 28, 2026
15de65e
(test): update ext/test_recipes.jl derivative view for option-B contract
mgyoo86 May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions src/constant/constant_anchor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,13 @@ end
# Used by both interpolant anchor dispatch AND series one-shot evaluation.

# Default case (extension, wrap, inbounds): kernel with right-boundary check.
# Short-circuits use `* one(aq.xq)` to match the kernel's `* one(dL)` carrier
# propagation — without it, Int y + Float xq returns Union{Int,Float}.
# Right-edge short-circuits thread both Tq (`one(aq.xq)`) and Tg (`one(x_last)`)
# carriers and use cell-local `y[aq.idxR]` so NaN at the queried cell propagates.
@inline function _constant_eval_at_anchor(
y::AbstractVector, x_last, aq::_ConstantAnchoredQuery,
op::AbstractEvalOp, side_param::AbstractSide, ::AbstractExtrap
)
aq.xq == x_last && return (op isa EvalValue ? (@inbounds y[aq.idxR] * one(aq.xq)) : 0 * first(y) * one(aq.xq))
aq.xq == x_last && return (op isa EvalValue ? (@inbounds y[aq.idxR] * one(aq.xq) * one(x_last)) : @inbounds 0 * y[aq.idxR] * one(aq.xq) * one(x_last))
@inbounds return _constant_kernel(op, y[aq.idxL], y[aq.idxR], aq.h, aq.dL, side_param)
end

Expand All @@ -290,7 +290,7 @@ end
op::AbstractEvalOp, side_param::AbstractSide, ::NoExtrap
)
aq.state != IN_DOMAIN && throw(DomainError(aq.xq, "query point outside domain"))
aq.xq == x_last && return (op isa EvalValue ? (@inbounds y[aq.idxR] * one(aq.xq)) : 0 * first(y) * one(aq.xq))
aq.xq == x_last && return (op isa EvalValue ? (@inbounds y[aq.idxR] * one(aq.xq) * one(x_last)) : @inbounds 0 * y[aq.idxR] * one(aq.xq) * one(x_last))
@inbounds return _constant_kernel(op, y[aq.idxL], y[aq.idxR], aq.h, aq.dL, side_param)
end

Expand All @@ -303,7 +303,7 @@ end
y_bnd = aq.state == OOB_LEFT ? first(y) : last(y)
return _eval_extrapolation(op, y_bnd, extrap, aq.xq)
end
aq.xq == x_last && return (op isa EvalValue ? (@inbounds y[aq.idxR] * one(aq.xq)) : 0 * first(y) * one(aq.xq))
aq.xq == x_last && return (op isa EvalValue ? (@inbounds y[aq.idxR] * one(aq.xq) * one(x_last)) : @inbounds 0 * y[aq.idxR] * one(aq.xq) * one(x_last))
@inbounds return _constant_kernel(op, y[aq.idxL], y[aq.idxR], aq.h, aq.dL, side_param)
end

Expand All @@ -319,21 +319,16 @@ end
# Interpolant Anchor Dispatch (thin wrappers)
# ========================================

# No extrapolation: enriched error message with domain bounds
# No extrapolation: enriched error message with domain bounds, then delegate
# to the shared cell-local path (right-edge & in-cell carrier handling).
@inline function _constant_anchor_dispatch(
itp::ConstantInterpolant{T},
aq::_ConstantAnchoredQuery{T},
op::O,
::NoExtrap
) where {T, O <: AbstractEvalOp}
itp::ConstantInterpolant, aq::_ConstantAnchoredQuery, op::AbstractEvalOp, ::NoExtrap
)
if aq.state != IN_DOMAIN
x_min, x_max = first(itp.x), last(itp.x)
throw(DomainError(aq.xq, "query point outside domain [$x_min, $x_max]"))
end
if aq.xq == last(itp.x)
return op isa EvalValue ? (@inbounds itp.y[aq.idxR] * one(aq.xq)) : zero(T) * one(aq.xq)
end
@inbounds return _constant_kernel(op, itp.y[aq.idxL], itp.y[aq.idxR], aq.h, aq.dL, itp.side)
return _constant_eval_at_anchor(itp.y, last(itp.x), aq, op, itp.side, NoExtrap())
end

# Inside domain or extension mode: delegate to shared
Expand Down
17 changes: 11 additions & 6 deletions src/constant/constant_oneshot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@
searcher::S
) where {Tg, Tv, Tq <: Real, S <: Searcher}
if _extract_primal(xi) == _extract_primal(last(x))
# `last(y)` covers both raw vectors and `_ExclusivePeriodicData` (cyclic
# `inner[1]`). `* one(xi)` propagates Tq carrier to match the kernel
# path below (without it, Int y + Float xq returns Union{Int,Float}).
return op isa EvalValue ? last(y) * one(xi) : 0 * first(y) * one(xi)
# `last(y)` for both raw vectors and `_ExclusivePeriodicData` (cyclic
# `inner[1]`). `one(Tq) * one(Tg)` threads both query and grid carriers
# — must match the kernel branch (which threads Tg via `dL`), else
# inference becomes `Union{Tv, Dual}` when grid is Dual and query is Float.
return op isa EvalValue ?
last(y) * one(Tq) * one(Tg) :
0 * last(y) * one(Tq) * one(Tg)
end
idx, idx_R, xL, xR = search_interval(searcher, x, xi)
dL = xi - xL
Expand Down Expand Up @@ -106,7 +109,9 @@ end
# the exact boundary. `last(_ExclusivePeriodicData) = inner[1]` so `:exclusive`
# cyclic wrap is preserved; raw Vector yields `y[n]`.
_extract_primal(xi_wrapped) == _extract_primal(last(x)) &&
return op isa EvalValue ? last(y) * one(xi_wrapped) : 0 * first(y) * one(xi_wrapped)
return op isa EvalValue ?
last(y) * one(Tq) * one(Tg) :
0 * last(y) * one(Tq) * one(Tg)
idx, idx_R, xL, xR = search_interval(searcher, x, xi_wrapped)
dL = xi_wrapped - xL
# Unwrap data once: `search_interval` already resolved the seam (idx_R = 1
Expand Down Expand Up @@ -148,7 +153,7 @@ Constant (step/piecewise constant) interpolation at a single point.
- `NearestSide()` (default): nearest neighbor (left tie-breaking at midpoint)
- `LeftSide()`: always use left value
- `RightSide()`: use right value (except at grid points)
- `deriv::DerivOp`: Derivative order (`EvalValue()`, `DerivOp(1)`, or `DerivOp(2)`). Derivatives are always 0.
- `deriv::DerivOp`: Derivative order `EvalValue()` for value, or any `DerivOp(n)` for nth derivative (constant interpolation has zero derivative at all orders ≥ 1).
- `search::AbstractSearchPolicy`: Search algorithm for interval finding
- `BinarySearch()` (default): O(log n) binary search, stateless
- `LinearBinarySearch(linear_window=0)`: O(1) if hint valid, O(log n) fallback
Expand Down
2 changes: 1 addition & 1 deletion src/constant/constant_oneshot_series.jl
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ function constant_interp(
K = n_series(s)
Tv = _series_eltype(s)
Tv_out = _output_eltype(_constant_kernel_shape, Tg, Tv, Tq)
outputs = [Vector{Tv_out}(undef, length(xqs)) for _ in 1:K]
outputs = _alloc_series_batch_outputs(Tv_out, K, length(xqs))
constant_interp!(outputs, x, s, xqs; bc, side, extrap, deriv, search)
return outputs
end
53 changes: 22 additions & 31 deletions src/constant/constant_series_interp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
# ║ Multiple y-data series sharing the same x-grid ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
#
# Phase E.2: Unified matrix storage for optimal performance.
# Key optimization: Adaptive layout with lazy transpose for scalar queries.
# Unified matrix storage with adaptive layout — lazy transpose for scalar queries.
#
# Include order: ... → constant_anchor.jl → constant_series_interp.jl
#
Expand Down Expand Up @@ -211,18 +210,16 @@ Outside-domain delegates to `_eval_series_at_anchor!` for extrapolation.
y_point = _ensure_point_layout!(sitp)
n_pts = n_points(sitp)

# Special case: at right boundary (use primal for comparison)
# Right boundary: yL == yR == y_point[k, n_pts]. `_constant_kernel(op, ...)`
# produces `y_at_max * one(dL)` for EvalValue, `0 * y_at_max * one(dL)` for
# deriv — cell-local NaN and Tq carrier propagate through both.
xq_primal = _extract_primal(xq)
if xq_primal == _extract_primal(last(sitp.x))
if op isa EvalValue
@inbounds @simd for k in axes(output, 1)
output[k] = y_point[k, n_pts]
end
else
z = 0 * first(y_point)
@inbounds @simd for k in axes(output, 1)
output[k] = z
end
h = aq.h
dL = aq.dL
@inbounds @simd for k in axes(output, 1)
y_at_max = y_point[k, n_pts]
output[k] = _constant_kernel(op, y_at_max, y_at_max, h, dL, sitp.side)
end
return output
end
Expand Down Expand Up @@ -273,13 +270,13 @@ end
n_pts::Int,
::Tg,
::Tg,
::_ConstantAnchoredQuery{Tg},
aq::_ConstantAnchoredQuery{Tg},
extrap::_ClampOrFill,
::AbstractSide,
op::AbstractEvalOp,
side::UInt8
) where {Tg, Tv}
return _fill_constant_extrap_simd!(out, y_point, side, n_pts, op, extrap)
return _fill_constant_extrap_simd!(out, y_point, side, n_pts, op, extrap, aq)
end

# ExtendExtrap - extend using same constant value at boundary interval
Expand Down Expand Up @@ -588,13 +585,12 @@ Internal: Evaluate single series at single query point with extrapolation handli
side_val::AbstractSide,
op::AbstractEvalOp
) where {Tg, Tv}
# Special case: at right boundary (MUST be preserved!)
# Right boundary: yL == yR == y[n_pts, k]. Kernel handles op-dispatch
# (value = y_at_max * one(dL), deriv = 0 * y_at_max * one(dL)) so the
# cell-local NaN and Tq carrier both propagate.
if aq.xq == x_max
if op isa EvalValue
@inbounds return y[n_pts, k]
else
return 0 * first(y) # Derivatives of step function are zero
end
@inbounds y_at_max = y[n_pts, k]
return _constant_kernel(op, y_at_max, y_at_max, aq.h, aq.dL, side_val)
end

# Inside domain: normal evaluation
Expand All @@ -606,14 +602,16 @@ Internal: Evaluate single series at single query point with extrapolation handli
if extrap isa ExtendExtrap || extrap isa WrapExtrap
return _eval_constant_series_anchored(y, k, aq, side_val, op)
elseif extrap isa _ClampOrFill
return _constant_extrap_boundary_value(y, aq.state, n_pts, k, op, extrap)
return _constant_extrap_boundary_value(y, aq.state, n_pts, k, op, extrap, aq)
else
_throw_extrap_domain_error(aq.xq, x_min, x_max)
end
end

"""
Internal: Core constant evaluation for series k at anchored query point.
Value and deriv share `_constant_kernel(op, ...)` — deriv returns
`0 * y_left * one(dL)` so cell-local NaN and Tq carrier propagate.
"""
@inline function _eval_constant_series_anchored(
y::Matrix{Tv},
Expand All @@ -622,16 +620,9 @@ Internal: Core constant evaluation for series k at anchored query point.
side_val::AbstractSide,
op::AbstractEvalOp
) where {Tg, Tv}
# Derivatives of constant (step) function are zero
if !(op isa EvalValue)
return 0 * first(y)
end

idxL = aq.idxL
idxR = aq.idxR
@inbounds begin
y_left = y[idxL, k]
y_right = y[idxR, k]
y_left = y[aq.idxL, k]
y_right = y[aq.idxR, k]
end
return _constant_kernel(EvalValue(), y_left, y_right, aq.h, aq.dL, side_val)
return _constant_kernel(op, y_left, y_right, aq.h, aq.dL, side_val)
end
37 changes: 25 additions & 12 deletions src/constant/nd/constant_nd_eval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ end
# In-place + allocating batch use the inherited `AbstractInterpolantND`
# protocol (trait-sized allocator via `_output_eltype`). Scalar routes
# through `_eval_nd_at_point`; Constant's "any derivative → 0" rule is
# wired via `_deriv_zero_fill` below.
# applied inside `_eval_at_cell` (see below) via `_constant_nd_evaluate`'s
# multi-dispatch — the cell-local kernel result is multiplied by `0` when
# any deriv operator is non-EvalValue, so NaN/Inf in the queried cell's
# data propagates through IEEE `NaN * 0 = NaN`.

# Derivative zero-fill trait: constant has zero derivative at all orders
@inline _deriv_zero_fill(::ConstantInterpolantND, ops::NTuple{N, AbstractEvalOp}, ::Val{N}) where {N} =
_has_any_derivative(ops, Val(N))

# Zero-ref for fill-value derivative computation (duck-typed zero via 0 * data_element)
@inline _zero_ref(itp::ConstantInterpolantND) = @inbounds first(itp.data)
# Per-method sample of `Tv` for fill-value paths (e.g. `_try_fill_oob`).
@inline _sample_data(itp::ConstantInterpolantND) = @inbounds first(itp.data)

# ========================================
# CELL LOCATION (locate once, evaluate many)
Expand Down Expand Up @@ -94,13 +93,27 @@ end
ops::NTuple{N, AbstractEvalOp}
) where {Tg, Tv, N}
data, stencils, hs, sides, q_eval, Ls = cell
if _has_any_derivative(ops, Val(N))
# `prod(one, q_eval)` folds carrier over every axis.
return 0 * first(itp.data) * prod(one, q_eval)
end
return _constant_nd_kernel(data, stencils, hs, sides, q_eval, Ls)
return _constant_nd_evaluate(data, stencils, hs, sides, q_eval, Ls, ops, Val(N))
end

# Deriv-aware Constant ND evaluation via two-method dispatch (mirrors Linear's
# `_linear_weight` pattern). `_constant_nd_kernel` only handles the EvalValue
# path; the deriv fallback multiplies the cell-local kernel result by `0`.
# The cell-local NaN/Inf carrier survives `* 0` via IEEE (`NaN * 0 = NaN`),
# so NaN data in the queried cell propagates through value and partials slots.
# Intentionally NOT a `fill!`/`zero(Tv)` shortcut: NaN propagation, Dual
# carrier, and duck-typed Tq all require running the kernel — the cost
# matches the value path.
@inline _constant_nd_evaluate(
data, stencils, hs, sides, q_eval, Ls,
::NTuple{N, EvalValue}, ::Val{N}
) where {N} = _constant_nd_kernel(data, stencils, hs, sides, q_eval, Ls)

@inline _constant_nd_evaluate(
data, stencils, hs, sides, q_eval, Ls,
::NTuple{N, AbstractEvalOp}, ::Val{N}
) where {N} = _constant_nd_kernel(data, stencils, hs, sides, q_eval, Ls) * 0

# ========================================
# Derivative Check
# ========================================
Expand Down
Loading
Loading