Skip to content

(feat): introduce _CachedRangeunified Range normalizer (fixes #85)#87

Merged
mgyoo86 merged 14 commits intomasterfrom
feat/cached_range
Mar 18, 2026
Merged

(feat): introduce _CachedRangeunified Range normalizer (fixes #85)#87
mgyoo86 merged 14 commits intomasterfrom
feat/cached_range

Conversation

@mgyoo86
Copy link
Copy Markdown
Member

@mgyoo86 mgyoo86 commented Mar 18, 2026

Issue (#85)

Julia's StepRangeLen uses TwicePrecision internally. On ARM (Apple Silicon) this is nearly free, but on Intel x86 the extended-precision arithmetic costs ~9ns per first(x) / last(x) call — making Range grids ~2 times slower than Vector despite O(1) index lookup.

Solution

New internal type _CachedRange{T} <: AbstractRange{T} that caches first, last, step, inv(step) as plain T fields. All Range inputs are normalized to _CachedRange via _to_float at every public API boundary. After normalization, only two grid types exist: _CachedRange{T} and Vector{T} — eliminating per-Range-type dispatch across the codebase.

Intel x86_64: TwicePrecision bypass

On x86_64 only (@static if Sys.ARCH === :x86_64), a specialized _to_float dispatch extracts endpoints via plain-T muladd instead of TwicePrecision arithmetic (~5ns vs ~27ns). This may introduce ±1 ULP rounding, so dedicated domain_lo/domain_hi fields (widened by prevfloat/nextfloat) ensure safe domain checks without affecting index computation accuracy.

On ARM (Apple Silicon), TwicePrecision is natively fast — the exact generic path is used and domain_lo == lo exactly.

Benchmark (Intel x86_64, N=1000)

Range search master this PR change
_search_direct ~20 ns 3.5 ns 5.7x faster
vs Vector 2.4x slower 2.4x faster inverted

Known limitation

Oneshot scalar linear_interp with Range is on par with Vector (~10.6 vs ~10.2ns) due to redundant inv(h) in the eval path. Tracked for follow-up optimization.

mgyoo86 added 9 commits March 17, 2026 14:09
…id/periodic

- `_to_float`: OrdinalRange → plain StepRangeLen{FT,FT,FT} (no range(), no TwicePrecision)
- `_to_float_adding_endpoint`: new helper for exclusive periodic extension (n→n+1)
  - StepRangeLen{FT,FT,FT}: direct field copy (zero arithmetic, no first()/step())
  - OrdinalRange: plain StepRangeLen extended by one
  - Other AbstractRange (TwicePrecision, etc.): range() fallback — preserves structure
- `_convert_grid`: delegates to _to_float for AbstractRange type conversion
- `_extend_exclusive` (periodic.jl): uses _to_float_adding_endpoint instead of range()
- `_check_domain` (AbstractRange): 1-ULP slack via prevfloat/nextfloat to handle
  plain StepRangeLen endpoint precision (muladd last() may be 1 ULP below exact)
…alizer

- _to_float now converts ALL AbstractRange types to _CachedRange{FT},
  not just TwicePrecision and OrdinalRange. After normalization, the grid
  type space is exactly {_CachedRange{T}, Vector{T}}.
- Extract _CachedRange definition + _to_float conversions into dedicated
  src/core/cached_range.jl (included between grid_spacing.jl and search.jl)
- Rename CachedRange → _CachedRange (internal implementation, not exported)
- Remove StepRangeLen/LinRange-specific dispatches from cubic_cache_pool.jl
  (now unreachable — all callers pass normalized types)
- Add AbstractRange fallback dispatches in cache impl as safety net
- Replace all direct range()/StepRangeLen() construction in periodic
  extension paths with _to_float_adding_endpoint (prevents Union return
  types in ND periodic exclusive paths)
- Add early _to_float normalization in cubic series constructor
…oat entry points

- Add x = _to_float(x, Tg) at all public oneshot API methods with
  Tg<:AbstractFloat constraint (linear, constant, quadratic, cubic).
  Previously these accepted raw StepRangeLen/LinRange without normalizing
  to _CachedRange, bypassing the _to_float normalizer.
- Add x_targets = _to_float(x_targets, Tg) for vector query entry points.
- Add _search_direct(_CachedRange, ScalarSpacing, xq) → 2-arg delegation.
  _CachedRange already has inv_h built in, so spacing arg is redundant.
  Fixes 1D cubic (and ND) falling through to generic AbstractRange dispatch.
…omain checks

- Add domain_lo/domain_hi fields: equal to lo/hi in exact path (ARM),
  widened by prevfloat/nextfloat on x86_64 TwicePrecision fast path.
- x86_64 @static dispatch: bypass TwicePrecision arithmetic via plain-T
  muladd for lo/hi (~22ns savings on Intel). lo/hi stay exact for index
  computation; domain_lo/domain_hi absorb ±1 ULP for _check_domain.
- 5-arg convenience constructor: domain_lo=lo, domain_hi=hi (zero overhead
  for non-TwicePrecision paths).
- Add _CachedRange-specific _check_domain dispatches in utils.jl using
  domain_lo/domain_hi instead of first(x)/last(x).
…_domain

Keep error message string interpolation + throw out of @inline hot path.
All 4 _check_domain methods now delegate to shared @noinline helper,
matching existing _throw_length_mismatch / _throw_grid_too_small pattern.
- Fix incorrect include-order comment in grid_spacing.jl (before → after)
- Add Base.size(::_CachedRange) for complete AbstractArray interface
- Document domain_hi reasoning in _to_float_adding_endpoint and type-mismatch _to_float
- Add @BoundsCheck checkbounds to getindex (zero-cost under @inbounds,
  safe for show/collect/broadcasting)
- Docstring: reflect that _CachedRange benefits all architectures
  (cached inv_h, unified dispatch), not just Intel TwicePrecision avoidance
…ience methods

- Add _to_float(x, FT) before _get_derivative_cache_impl / _get_periodic_cache_impl
  in ZeroCurvBC, ZeroSlopeBC, PeriodicBC, BCPair, and PointBC convenience methods.
- Fix include-order comment in grid_spacing.jl (before → after).
- Previously these passed raw Range to cache impl, relying on the AbstractRange
  fallback to normalize. Now consistent with 3-arg methods.
@mgyoo86 mgyoo86 requested a review from Copilot March 18, 2026 05:32
@mgyoo86 mgyoo86 changed the title (feat): introduce _CachedRangeunified Range normalizer (fixes #82) (feat): introduce _CachedRangeunified Range normalizer (fixes #85) Mar 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 18, 2026

FastInterpolations.jl Benchmarks

All benchmarks (42 total, click to expand)
Benchmark Current: 7ad6533 Previous Imm. Ratio Grad. Ratio
10_nd_construct/bicubic_2d 36859.0 ns 37861.0 ns 0.974 1.016
10_nd_construct/bilinear_2d 610.9 ns 567.9 ns 1.076 1.084
10_nd_construct/tricubic_3d 355828.0 ns 353275.0 ns 1.007 1.013
10_nd_construct/trilinear_3d 1702.8 ns 1656.5 ns 1.028 1.042
11_nd_eval/bicubic_2d_batch 1644.0 ns 1647.1 ns 0.998 1.038
11_nd_eval/bicubic_2d_scalar 16.1 ns 26.8 ns 0.6 0.6
11_nd_eval/bilinear_2d_scalar 11.5 ns 23.2 ns 0.496 0.501
11_nd_eval/tricubic_3d_batch 3465.6 ns 3480.6 ns 0.996 1.036
11_nd_eval/tricubic_3d_scalar 33.6 ns 47.7 ns 0.704 0.684
11_nd_eval/trilinear_3d_scalar 19.1 ns 33.1 ns 0.579 0.666
12_cubic_eval_gridquery/range_random 4739.1 ns 4742.1 ns 0.999 1.059
12_cubic_eval_gridquery/range_sorted 4755.3 ns 4728.7 ns 1.006 1.065
12_cubic_eval_gridquery/vec_random 9220.5 ns 10155.5 ns 0.908 0.974
12_cubic_eval_gridquery/vec_sorted 3500.2 ns 3464.1 ns 1.01 1.089
1_cubic_oneshot/q00001 444.2 ns 442.0 ns 1.005 0.996
1_cubic_oneshot/q10000 61896.1 ns 61945.6 ns 0.999 1.0
2_cubic_construct/g0100 1302.2 ns 1248.1 ns 1.043 1.036
2_cubic_construct/g1000 12608.6 ns 12036.6 ns 1.048 1.051
3_cubic_eval/q00001 19.5 ns 34.0 ns 0.575 0.578
3_cubic_eval/q00100 507.1 ns 513.8 ns 0.987 1.068
3_cubic_eval/q10000 48042.2 ns 48046.4 ns 1.0 1.068
4_linear_oneshot/q00001 26.8 ns 30.8 ns 0.87 0.718
4_linear_oneshot/q10000 29003.4 ns 33340.7 ns 0.87 0.784
5_linear_construct/g0100 36.1 ns 33.4 ns 1.081 3.418
5_linear_construct/g1000 235.0 ns 259.6 ns 0.905 23.012
6_linear_eval/q00001 12.9 ns 18.7 ns 0.689 0.713
6_linear_eval/q00100 374.9 ns 442.6 ns 0.847 0.933
6_linear_eval/q10000 35447.5 ns 41042.2 ns 0.864 0.935
7_cubic_range/scalar_query 8.3 ns 22.6 ns 0.367 0.369
7_cubic_vec/scalar_query 11.4 ns 12.5 ns 0.912 0.928
8_cubic_multi/construct_s001_q100 569.9 ns 560.4 ns 1.017 0.976
8_cubic_multi/construct_s010_q100 4346.4 ns 4341.8 ns 1.001 0.999
8_cubic_multi/construct_s100_q100 39721.5 ns 39716.7 ns 1.0 1.001
8_cubic_multi/eval_s001_q100 1085.8 ns 1362.4 ns 0.797 0.787
8_cubic_multi/eval_s010_q100 2076.3 ns 2336.0 ns 0.889 0.865
8_cubic_multi/eval_s010_q100_scalar_loop 2637.9 ns 3617.8 ns 0.729 0.727
8_cubic_multi/eval_s100_q100 11850.2 ns 12135.8 ns 0.976 0.929
8_cubic_multi/eval_s100_q100_scalar_loop 3544.7 ns 4632.7 ns 0.765 0.771
9_nd_oneshot/bicubic_2d 37978.2 ns 37292.1 ns 1.018 1.051
9_nd_oneshot/bilinear_2d 1051.2 ns 1081.6 ns 0.972 1.042
9_nd_oneshot/tricubic_3d 373381.5 ns 360562.4 ns 1.036 1.041
9_nd_oneshot/trilinear_3d 1671.1 ns 1698.2 ns 0.984 1.052

⚠️ Performance Regression Confirmed ⚠️

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

Benchmark Current Previous Imm. Ratio Grad. Ratio Tier
5_linear_construct/g0100 36.1 ns 33.4 ns 1.081 3.418 gradual
5_linear_construct/g1000 235.0 ns 259.6 ns 0.905 23.012 gradual

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

This comment was automatically generated by Benchmark workflow.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an internal _CachedRange grid type and normalizes AbstractRange inputs to it at API boundaries to avoid StepRangeLen/TwicePrecision overhead (notably on Intel x86_64), while keeping O(1) uniform-grid indexing and improving cache/search performance.

Changes:

  • Add src/core/cached_range.jl defining _CachedRange, _to_float (Range → _CachedRange), and _to_float_adding_endpoint for periodic-exclusive extension.
  • Normalize range grids via _to_float across series constructors, one-shot APIs, periodic extension paths, and cubic cache pooling.
  • Update tests to assert _CachedRange preservation instead of StepRangeLen.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/test_series_range_grid.jl Updates series tests to expect _CachedRange for range grids.
test/test_linear.jl Updates linear range-preservation test to expect _CachedRange{Float64}.
test/test_cubic_interpolant.jl Updates cubic autocache test to expect _CachedRange in cache grid.
src/quadratic/quadratic_series_interp.jl Normalizes copied grids via _to_float in series constructor.
src/quadratic/quadratic_oneshot.jl Normalizes x / x_targets via _to_float in one-shot paths.
src/linear/linear_series_interp.jl Normalizes copied grids via _to_float in series constructor.
src/linear/linear_oneshot.jl Normalizes x / x_targets via _to_float in one-shot paths.
src/cubic/nd/cubic_nd_adjoint.jl Uses _to_float_adding_endpoint for periodic-exclusive grid extension.
src/cubic/cubic_series_interp.jl Normalizes x early via _to_float before downstream processing.
src/cubic/cubic_oneshot.jl Normalizes x / x_query and uses _to_float_adding_endpoint for periodic extension.
src/cubic/cubic_cache_pool.jl Normalizes grids for cache lookup/insertion and adds _CachedRange-keyed banks.
src/cubic/cubic_adjoint.jl Uses _to_float_adding_endpoint for periodic-exclusive adjoint grid extension.
src/core/utils.jl Moves range _to_float definitions out; adds _CachedRange domain-check specializations.
src/core/search.jl Adds _search_direct specializations for _CachedRange using cached inv_h.
src/core/periodic.jl Uses _to_float_adding_endpoint for Range periodic-exclusive extension (1D + ND).
src/core/nd_utils.jl Normalizes ND range grids through _to_float (→ _CachedRange).
src/core/grid_spacing.jl Notes _CachedRange integration point; defers specialization to cached_range.jl.
src/core/core.jl Updates include order to load cached_range.jl before search/utils.
src/core/cached_range.jl New file: defines _CachedRange, _to_float, _to_float_adding_endpoint, and _create_spacing specialization.
src/constant/constant_series_interp.jl Normalizes copied grids via _to_float in series constructor.
src/constant/constant_oneshot.jl Normalizes x / x_targets via _to_float in one-shot paths.
Comments suppressed due to low confidence (1)

test/test_cubic_interpolant.jl:248

  • The surrounding test comments still describe the old behavior (“Range is kept as Range” / “same objectid → hit”). With _to_float normalization, itp.cache.x is now a _CachedRange value (not the original StepRangeLen), and cache hits will be driven primarily by isequal rather than “same objectid”. Updating these comments will prevent future confusion when interpreting the test’s intent.
        # Range-preserving cache: Range is kept as Range for O(1) index lookup!
        # This provides significant performance benefit over Vector (binary search)
        itp = cubic_interp(x_range, y; autocache = true)
        @test itp.cache.x isa FastInterpolations._CachedRange  # Range preserved for O(1) index calculation

        # Verify correctness
        @test itp(0.5) ≈ sin(2π * 0.5) atol = 0.01

        # Verify cache hit works with same Range (Julia interns Ranges)
        clear_cubic_cache!()
        result1 = cubic_interp(x_range, y, 0.5; autocache = true)  # First call: miss
        result2 = cubic_interp(range(0.0, 1.0, 11), y, 0.5; autocache = true)  # Same params → same objectid → hit!
        @test result1 ≈ result2  # Same results from cache hit

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/core/cached_range.jl Outdated
Comment thread src/core/cached_range.jl Outdated
Comment thread src/core/cached_range.jl Outdated
Comment thread src/cubic/cubic_cache_pool.jl Outdated
Comment thread test/test_series_range_grid.jl Outdated
mgyoo86 added 3 commits March 17, 2026 22:57
- Add @BoundsCheck to getindex (was missing despite prior commit message)
- Pin endpoints in getindex: i==1 → lo, i==len → hi (prevents ULP drift)
- Delegate _to_float_adding_endpoint(AbstractRange) through _to_float
  to leverage x86_64 TwicePrecision bypass on StepRangeLen
- Use FI._CachedRange alias in test_series_range_grid.jl for consistency
- Update outdated comments in test_cubic_interpolant.jl
- Fix muladd formula: `x.offset - 1` → `1 - x.offset` for computing lo
  (offset=1 only works when ref==first, but Julia uses midpoint ref)
- Update test_nd_comprehensive.jl: grid identity/equality tests now check
  first/last/length instead of === or == (grids normalized to _CachedRange)
…alization

_CachedRange uses muladd (plain Float64) vs StepRangeLen TwicePrecision,
causing 1-2 ULP differences in middle elements. Tests now use ≈ with
tight tolerance (rtol=8eps or default) instead of == or === for grid
value comparisons.
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 97.05882% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.85%. Comparing base (5faa726) to head (7ad6533).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/cubic/cubic_cache_pool.jl 87.50% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #87      +/-   ##
==========================================
- Coverage   95.87%   95.85%   -0.02%     
==========================================
  Files          94       95       +1     
  Lines        7973     8034      +61     
==========================================
+ Hits         7644     7701      +57     
- Misses        329      333       +4     
Files with missing lines Coverage Δ
src/constant/constant_oneshot.jl 98.30% <100.00%> (+0.09%) ⬆️
src/constant/constant_series_interp.jl 97.26% <100.00%> (ø)
src/core/cached_range.jl 100.00% <100.00%> (ø)
src/core/grid_spacing.jl 100.00% <ø> (ø)
src/core/nd_utils.jl 96.64% <100.00%> (-0.02%) ⬇️
src/core/periodic.jl 96.47% <100.00%> (ø)
src/core/search.jl 93.75% <100.00%> (+0.21%) ⬆️
src/core/utils.jl 92.85% <100.00%> (+0.66%) ⬆️
src/cubic/cubic_adjoint.jl 100.00% <100.00%> (ø)
src/cubic/cubic_oneshot.jl 99.05% <100.00%> (+0.02%) ⬆️
... and 7 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

mgyoo86 added 2 commits March 18, 2026 09:41
Tests Float64→Float32 downcast, Float32→Float64 upcast, and same-type
pass-through. Verifies domain widening is dropped on type conversion
and same-type returns identical object (===).
…allback

Direct call with raw StepRangeLen verifies normalization to _CachedRange,
cache hit on second call, and separate bank for different BC types.
@mgyoo86 mgyoo86 merged commit 91452ba into master Mar 18, 2026
2 checks passed
@mgyoo86 mgyoo86 deleted the feat/cached_range branch March 18, 2026 16:47
mgyoo86 added a commit that referenced this pull request Mar 18, 2026
After _CachedRange normalization (PR #87), several dispatch methods are
unreachable or redundant:

- _create_spacing(::LinRange): always normalized to _CachedRange before
  reaching _create_spacing; redundant with AbstractRange fallback
- _create_spacing_pooled(::LinRange): same reason
- _linear_interp_loop!(::AbstractRange, ::WrapExtrap): identical logic
  to the AbstractVector version; _CachedRange dispatches to AbstractVector
mgyoo86 added a commit that referenced this pull request Mar 19, 2026
* (refac): remove dead LinRange dispatches and duplicate WrapExtrap loop

After _CachedRange normalization (PR #87), several dispatch methods are
unreachable or redundant:

- _create_spacing(::LinRange): always normalized to _CachedRange before
  reaching _create_spacing; redundant with AbstractRange fallback
- _create_spacing_pooled(::LinRange): same reason
- _linear_interp_loop!(::AbstractRange, ::WrapExtrap): identical logic
  to the AbstractVector version; _CachedRange dispatches to AbstractVector

* (perf): unify Range/Vector eval via 3-arg _get_h/_get_inv_h accessors

Introduce 3-arg grid-based accessors that dispatch on grid type:
- _get_h(x::_CachedRange, xR, xL) → x.h  (cached, zero cost)
- _get_h(x::AbstractVector, xR, xL) → xR - xL  (computed)
- _get_inv_h(x::_CachedRange, xR, xL) → x.inv_h  (cached, avoids fdiv)
- _get_inv_h(x::AbstractVector, xR, xL) → inv(xR - xL)

This eliminates the need for separate _CachedRange specializations:
- _constant_eval_at_point: 2 methods → 1 (uses _get_h)
- _linear_eval_at_point: 2 methods → 1 (uses _get_inv_h)
- _eval_linear_series_point_extrap!: 2 methods → 1 (uses _get_inv_h)

* (refac): flatten linear eval layers — remove _linear_with_extrap

Remove 2 pure delegation layers from the linear scalar eval path:
- linear_interp(positional 6-arg) → pure delegation to _linear_with_extrap
- _linear_with_extrap (4 methods) → pure delegation to _linear_eval_at_point

Now _linear_eval_at_point dispatches directly on extrap type:
- AbstractExtrap (NoExtrap/ExtendExtrap): direct search + kernel
- _ClampOrFill: boundary check → extrap value or kernel
- WrapExtrap: wrap query → search + kernel

Call chain: 4 layers → 2 layers
  Before: linear_interp(kwargs) → linear_interp(pos) → _linear_with_extrap → _linear_eval_at_point → kernel
  After:  linear_interp(kwargs) → _linear_eval_at_point(extrap dispatch) → kernel

* (refac): flatten quadratic eval layers — remove _quadratic_eval_with_extrap

Merge _quadratic_eval_at_point, _quadratic_eval_with_extrap, and
_quadratic_eval_core into a single _quadratic_eval_at_point with
3 extrap dispatch methods (same pattern as linear).

Call chain: 3 layers → 1 layer
  Before: _quadratic_eval_at_point → _quadratic_eval_with_extrap → _quadratic_eval_core → kernel
  After:  _quadratic_eval_at_point(extrap dispatch) → kernel

* (refac): rename + relocate _eval_extrapolation to core/utils.jl

- Rename _constant_extrap_result → _eval_extrapolation (clearer intent,
  avoids confusion with ConstantInterpolant)
- Move _eval_extrapolation + _promote_extrap_* from cubic_eval.jl to
  core/utils.jl (shared by all interpolation methods, not cubic-specific)
- Simplify dispatch: DerivOp generic + EvalValue specific (was 5 methods
  with EvalDeriv1/2/3 each, now 4 methods via DerivOp parametric dispatch)
- Replace @inbounds(y[1])/y[end] with first(y)/last(y) at OOB call sites

* test: add inference test for cubic_interp with exclusive endpoint

* (refac): flatten cubic eval layers + unify extrap helpers across all methods

- Remove _eval_cubic_with_extrap indirection: inline kernel at each extrap
  dispatch site in _eval_cubic_at_point (matching linear/quadratic pattern)
- Add @BoundsCheck _check_domain to cubic AbstractExtrap catch-all
- Inline _linear_eval_constant_extrap at callsites, remove dead function
- Fix stale comment in quadratic_oneshot.jl (cubic_eval.jl → core/utils.jl)
- Update _eval_with_bc caller and test imports

* (refac): narrow 3-arg _get_h/_get_inv_h signatures from ::Any to ::Real

* (feat): introduce InBounds() extrap type for batch domain-check elision

Vector _check_domain(::NoExtrap) now wraps validation in @BoundsCheck
and returns InBounds(), so per-element scalar evals see a no-op check
regardless of call depth. Fixes cubic batch path where @inbounds didn't
propagate through _eval_with_bc → _eval_cubic_at_point (2 levels deep).

- New InBounds <: AbstractExtrap type (exported)
- Vector loops: `extrap = _check_domain(x, xq, extrap)` captures return
- Remove redundant protocol-layer vector @BoundsCheck checks
- Revert @propagate_inbounds on _eval_with_bc (no longer needed)
- Remove debug artifacts (Main.@infiltrate, Main.@count_here)

* refac: optimize index calculation in _search_direct and related functions using muladd

* (docs): fix 5 stale comments referencing renamed/relocated functions

- cubic_eval.jl: replace orphaned _constant_extrap_result comment block
  with cross-reference to core/utils.jl
- nd_utils.jl: _promote_extrap_val/zero is in core/utils.jl, not cubic_eval.jl
- test_mixed_precision_extrap.jl: _constant_extrap_result → _eval_extrapolation
- test_derivatives.jl: _linear_eval_constant_extrap → _eval_extrapolation
- type_promotion_rules.md: _constant_extrap_result → _eval_extrapolation

* (refac): flatten constant eval layers — remove _constant_eval_extrap

- 3-dispatch pattern (AbstractExtrap, _ClampOrFill, WrapExtrap) matching
  linear/quadratic/cubic — no intermediate _constant_eval_extrap indirection
- Eliminates InBounds MethodError on OOB path (catch-all goes straight to
  search+kernel, no extrap-type-specific branch for OOB)
- Remove union-splitting warning from AbstractExtrap docstring (all
  interpolants store concrete type params, union-splitting is irrelevant)

* (docs+test): add InBounds() to docs index and extrapolation guide, add unit tests

- docs/src/api/types.md: add InBounds to @docs block (fixes Documenter error)
- docs/src/extrapolation.md: add InBounds to overview table, type hierarchy, summary
- docs/src/architecture/type_promotion_rules.md: fix fill_value promotion wording
- test/test_inbounds_extrap.jl: scalar/vector/interpolant tests for all methods,
  NoExtrap→InBounds batch conversion, type stability (@inferred)

* runic formatting

* (fix): add ExtendExtrap dispatch for constant eval — fix OOB + LeftSide/RightSide

Constant kernel is side-dependent (discrete left/right selection), so OOB
queries with dL > h return the wrong side value. Unlike polynomial methods
where ExtendExtrap naturally extends via kernel, constant must delegate to
ClampExtrap (slope=0 → extend = clamp). Regression from e811849.
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.

2 participants