From cd2d9c170382a8add37e437619f3de3f3764fe3a Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Tue, 24 Mar 2026 17:16:50 -0700 Subject: [PATCH 01/15] (refactor): relax periodic endpoint check from strict == to isapprox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-tier dispatch for _check_periodic_endpoints: - AbstractFloat / Complex{<:AbstractFloat}: isapprox with atol=8eps(T) (compile-time folded, zero overhead). Covers both rtol-relative diffs and near-zero noise floor (sin(0) vs sin(2π)). - Other _PromotableValue (Integer, Rational): isapprox default tolerances. - Duck types: strict == (isapprox semantics not guaranteed). --- src/core/periodic.jl | 36 ++++++++++++++++++++++----- test/test_periodic_bc.jl | 53 ++++++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/core/periodic.jl b/src/core/periodic.jl index 655e72910..45dbc4fca 100644 --- a/src/core/periodic.jl +++ b/src/core/periodic.jl @@ -51,18 +51,42 @@ end """ _check_periodic_endpoints(y::AbstractVector) -Validate that `y[1] == y[end]` for periodic boundary conditions (inclusive endpoint). +Validate that `y[1] ≈ y[end]` for periodic boundary conditions (inclusive endpoint). Called once at construction time (zero runtime overhead). -Uses strict `==` equality — no approximate comparison. This is universal for all -value types (scalars, vectors, duck-typed custom types) without requiring `norm`, -`isapprox`, or any tolerance parameters. +Three-tier dispatch based on element type: -If your data is computed (e.g., `sin.(range(0, 2π, n))`), set `y[end] = y[1]` -explicitly to ensure exact periodicity. +- **`AbstractFloat`**: `isapprox` with `atol = 8eps(T)` — handles both relative differences + (e.g., `cos(0) ≈ cos(2π)`) and near-zero noise floor (e.g., `sin(0)` vs `sin(2π)`). + The `8eps` constant is compile-time folded (zero overhead vs plain `isapprox`). +- **`Complex{<:AbstractFloat}`**: same, using `eps(real(T))`. +- **Other `_PromotableValue`** (Integer, Rational): `isapprox` with default tolerances. +- **Duck types** (Dual, SVector, ...): strict `==` (isapprox semantics not guaranteed). + +!!! note "Scaled near-zero endpoints" + `atol = 8eps` covers direct evaluations (e.g., `sin.(x)`), but not scaled + variants (e.g., `1e6 .* sin.(x)` where noise ≈ 1e6·eps). For those cases, + set `y[end] = y[1]` explicitly. Throws `ArgumentError` if endpoints differ. """ +@inline function _check_periodic_endpoints(y::AbstractVector{T}) where {T <: AbstractFloat} + isapprox(first(y), last(y); atol = 8 * eps(T)) || _throw_periodic_endpoint_error(first(y), last(y)) + return nothing +end + +@inline function _check_periodic_endpoints(y::AbstractVector{Complex{T}}) where {T <: AbstractFloat} + isapprox(first(y), last(y); atol = 8 * eps(T)) || _throw_periodic_endpoint_error(first(y), last(y)) + return nothing +end + +# Integer, Rational: isapprox with default tolerances (effectively ==) +@inline function _check_periodic_endpoints(y::AbstractVector{<:_PromotableValue}) + isapprox(first(y), last(y)) || _throw_periodic_endpoint_error(first(y), last(y)) + return nothing +end + +# Duck-type fallback: strict == (isapprox not guaranteed for arbitrary types) @inline function _check_periodic_endpoints(y::AbstractVector) _extract_primal(first(y)) == _extract_primal(last(y)) || _throw_periodic_endpoint_error(first(y), last(y)) return nothing diff --git a/test/test_periodic_bc.jl b/test/test_periodic_bc.jl index 38efa8201..ed7dc0bee 100644 --- a/test/test_periodic_bc.jl +++ b/test/test_periodic_bc.jl @@ -369,13 +369,12 @@ using FastInterpolations @testset "_check_periodic_endpoints validation (Cubic only)" begin # NOTE: Linear interpolation with extrap=WrapExtrap() does NOT check endpoints! - # Only cubic bc=PeriodicBC() checks that y[1] == y[end] (strict equality) + # Only cubic bc=PeriodicBC() checks that y[1] ≈ y[end] (isapprox for _PromotableValue) x = range(0.0, 2π, 101) @testset "Valid periodic data — exact endpoints (Float64)" begin - # Periodic data must have y[end] = y[1] set explicitly y_sin = collect(sin.(x)) - y_sin[end] = y_sin[1] # Ensure exact equality + y_sin[end] = y_sin[1] # Exact equality @test y_sin[1] == y_sin[end] @test linear_interp(x, y_sin, 0.5; extrap = WrapExtrap()) isa Float64 @@ -383,29 +382,57 @@ using FastInterpolations # cos(0) = cos(2π) = 1.0 exactly in Float64 y_cos = collect(cos.(x)) - y_cos[end] = y_cos[1] @test cubic_interp(x, y_cos, 0.5; bc = PeriodicBC()) isa Float64 end @testset "Valid periodic data — exact endpoints (Float32)" begin x_f32 = range(0.0f0, 2.0f0 * Float32(π), 101) y_f32 = collect(sin.(x_f32)) - y_f32[end] = y_f32[1] # Ensure exact equality + y_f32[end] = y_f32[1] # Exact equality @test y_f32[1] == y_f32[end] @test linear_interp(x_f32, y_f32, 0.5f0; extrap = WrapExtrap()) isa Float32 @test cubic_interp(x_f32, y_f32, 0.5f0; bc = PeriodicBC()) isa Float32 end - @testset "Approximate endpoints now rejected (strict ==)" begin - # sin(0) vs sin(2π) are NOT bit-identical — strict == rejects this - y_approx = sin.(x) - @test y_approx[1] != y_approx[end] # Different due to floating-point - @test_throws ArgumentError cubic_interp(x, y_approx, 0.5; bc = PeriodicBC()) + @testset "Approximate endpoints accepted via isapprox (_PromotableValue)" begin + # cos(0) vs cos(2π) — both 1.0, isapprox passes (non-zero, rtol works) + y_cos = cos.(x) + @test isapprox(y_cos[1], y_cos[end]) + @test cubic_interp(x, y_cos, 0.5; bc = PeriodicBC()) isa Float64 + + # Float32 cos — same story + x_f32 = range(0.0f0, 2.0f0 * Float32(π), 101) + y_cos_f32 = cos.(x_f32) + @test isapprox(y_cos_f32[1], y_cos_f32[end]) + @test cubic_interp(x_f32, y_cos_f32, 0.5f0; bc = PeriodicBC()) isa Float32 + end + + @testset "Near-zero endpoints accepted via atol = 8eps" begin + # sin(0) vs sin(2π): diff ≈ 1.1 eps, covered by atol = 8eps noise floor + y_sin_raw = sin.(x) + @test !isapprox(y_sin_raw[1], y_sin_raw[end]) # default isapprox fails + @test cubic_interp(x, y_sin_raw, 0.5; bc = PeriodicBC()) isa Float64 # but 8eps atol saves it + + # Float32 sin + x_f32 = range(0.0f0, 2.0f0 * Float32(π), 101) + y_sin_f32 = sin.(x_f32) + @test cubic_interp(x_f32, y_sin_f32, 0.5f0; bc = PeriodicBC()) isa Float32 + end + + @testset "Scaled near-zero — atol=8eps not enough, requires y[end]=y[1]" begin + # 1e6 * sin(x): noise ≈ 1e6 * eps, exceeds 8eps floor + y_scaled = 1e6 .* sin.(x) + @test_throws ArgumentError cubic_interp(x, y_scaled, 0.5; bc = PeriodicBC()) + + # Fix: set endpoint explicitly + y_scaled[end] = y_scaled[1] + @test cubic_interp(x, y_scaled, 0.5; bc = PeriodicBC()) isa Float64 + end - # Even tiny differences are rejected - y_tiny = collect(sin.(x)) - y_tiny[end] = y_tiny[1] + eps(Float64) + @testset "Clearly different endpoints — rejected" begin + y_tiny = collect(cos.(x)) + y_tiny[end] = y_tiny[1] + 1e-6 # Well beyond isapprox tolerance @test_throws ArgumentError cubic_interp(x, y_tiny, 0.5; bc = PeriodicBC()) end From 277a37bb974696e5b29ccfb4a868b53af83bbafb Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Tue, 24 Mar 2026 17:34:39 -0700 Subject: [PATCH 02/15] (feat): PeriodicBC(check=false) to skip endpoint validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `check::Bool` field to PeriodicBC (default true). When false, skips _check_periodic_endpoints entirely — useful for scaled periodic data (e.g. 1e6*sin.(x)) where noise exceeds the 8eps atol floor. Also adds atol=8eps(T) noise floor for AbstractFloat/Complex endpoints, covering direct evaluations like sin.(x) where endpoints are near zero. Error message now mentions check=false as an escape hatch. Three-tier endpoint validation: - AbstractFloat/Complex: isapprox with atol=8eps(T) (compile-time folded) - Other _PromotableValue: isapprox default tolerances - Duck types: strict == --- src/core/bc_types.jl | 16 ++++++++++------ src/core/periodic.jl | 22 +++++++++++++++------- src/cubic/cubic_interpolant.jl | 2 +- src/cubic/cubic_oneshot.jl | 2 +- src/cubic/nd/cubic_nd_build.jl | 33 +++++++++++++++++++++++++++------ test/test_cubic_nd_oneshot.jl | 4 +++- test/test_periodic_bc.jl | 32 +++++++++++++++++++++++++++----- 7 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/core/bc_types.jl b/src/core/bc_types.jl index 77853070d..4edba6a03 100644 --- a/src/core/bc_types.jl +++ b/src/core/bc_types.jl @@ -200,14 +200,18 @@ itp = cubic_interp(x, y; bc=PeriodicBC(endpoint=:exclusive, period=2π)) # Exclusive with Range grid (period auto-inferred) x = range(0, step=2π/64, length=64) itp = cubic_interp(x, sin.(x); bc=PeriodicBC(endpoint=:exclusive)) + +# Skip endpoint check (e.g., scaled data where noise > 8eps) +itp = cubic_interp(x, 1e6 .* sin.(x); bc=PeriodicBC(check=false)) ``` """ struct PeriodicBC{E, P} <: AbstractBC period::P # Nothing or AbstractFloat - function PeriodicBC{E, P}(period::P) where {E, P} + check::Bool # Whether to validate y[1] ≈ y[end] at construction time + function PeriodicBC{E, P}(period::P, check::Bool = true) where {E, P} E isa Symbol || error("PeriodicBC type parameter E must be a Symbol") E in (:inclusive, :exclusive) || error("PeriodicBC type parameter E must be :inclusive or :exclusive") - return new{E, P}(period) + return new{E, P}(period, check) end end @@ -215,7 +219,7 @@ end @inline endpoint(::PeriodicBC{E}) where {E} = E # Keyword constructor with validation (also serves as zero-arg constructor via defaults) -function PeriodicBC(; endpoint::Symbol = :inclusive, period::Union{Real, Nothing} = nothing) +function PeriodicBC(; endpoint::Symbol = :inclusive, period::Union{Real, Nothing} = nothing, check::Bool = true) endpoint in (:inclusive, :exclusive) || throw( ArgumentError( "endpoint must be :inclusive or :exclusive, got :$endpoint" @@ -227,14 +231,14 @@ function PeriodicBC(; endpoint::Symbol = :inclusive, period::Union{Real, Nothing "period is not applicable for endpoint=:inclusive (y[1]≈y[end] convention)" ) ) - return PeriodicBC{:inclusive, Nothing}(nothing) + return PeriodicBC{:inclusive, Nothing}(nothing, check) else # :exclusive if period !== nothing p = float(period) p > 0 || throw(ArgumentError("period must be positive, got $period")) - return PeriodicBC{:exclusive, typeof(p)}(p) + return PeriodicBC{:exclusive, typeof(p)}(p, check) else - return PeriodicBC{:exclusive, Nothing}(nothing) # infer from Range at build time + return PeriodicBC{:exclusive, Nothing}(nothing, check) # infer from Range at build time end end end diff --git a/src/core/periodic.jl b/src/core/periodic.jl index 45dbc4fca..f38dfb46b 100644 --- a/src/core/periodic.jl +++ b/src/core/periodic.jl @@ -70,6 +70,12 @@ Three-tier dispatch based on element type: Throws `ArgumentError` if endpoints differ. """ +@inline function _check_periodic_endpoints(bc::PeriodicBC, y::AbstractVector) + bc.check || return nothing + _check_periodic_endpoints(y) + return nothing +end + @inline function _check_periodic_endpoints(y::AbstractVector{T}) where {T <: AbstractFloat} isapprox(first(y), last(y); atol = 8 * eps(T)) || _throw_periodic_endpoint_error(first(y), last(y)) return nothing @@ -95,10 +101,11 @@ end @noinline function _throw_periodic_endpoint_error(y1, yn) throw( ArgumentError( - "PeriodicBC (inclusive endpoint) requires y[1] == y[end], " * + "PeriodicBC (inclusive endpoint) requires y[1] ≈ y[end], " * "got y[1]=$y1, y[end]=$yn. " * - "Tip: set y[end] = y[1] to ensure exact periodicity, or use " * - "PeriodicBC(endpoint=:exclusive) if your data does not repeat the first point." + "Tip: set y[end] = y[1] explicitly, use " * + "PeriodicBC(endpoint=:exclusive) if your data does not repeat the first point, " * + "or PeriodicBC(check=false) to skip this validation." ) ) end @@ -117,9 +124,10 @@ end @noinline function _throw_periodic_nd_error(d, v_first, v_last) throw( ArgumentError( - "Periodic BC on dim $d requires data[1,...] == data[end,...], " * + "Periodic BC on dim $d requires data[1,...] ≈ data[end,...], " * "but found data[1,...]=$v_first, data[end,...]=$v_last. " * - "Tip: set the last slice equal to the first along dim $d." + "Tip: set the last slice equal to the first along dim $d, " * + "or use PeriodicBC(check=false) to skip this validation." ) ) end @@ -194,8 +202,8 @@ Used so that `itp.bc` always carries the actual period for display/introspection Uses the inner constructor directly to bypass keyword-constructor validation (which rejects `period` for inclusive BCs). """ -@inline _with_resolved_period(::PeriodicBC{E}, period::T) where {E, T} = - PeriodicBC{E, T}(period) +@inline _with_resolved_period(bc::PeriodicBC{E}, period::T) where {E, T} = + PeriodicBC{E, T}(period, bc.check) """ _extend_exclusive(x, y, bc::PeriodicBC) -> (x_ext, y_ext) diff --git a/src/cubic/cubic_interpolant.jl b/src/cubic/cubic_interpolant.jl index 69c8a76bd..8012d12ec 100644 --- a/src/cubic/cubic_interpolant.jl +++ b/src/cubic/cubic_interpolant.jl @@ -290,7 +290,7 @@ so the pool memory can be safely reused after this function returns. search::AbstractSearchPolicy = AutoSearch() ) where {Tg <: AbstractFloat, Tv} x, y = _prepare_periodic(x, y, bc) - _check_periodic_endpoints(y) + _check_periodic_endpoints(bc, y) cache = _get_cubic_cache(x, PeriodicBC(), autocache) tmp_z = similar!(pool, y) _solve_system!(tmp_z, cache, y, cache.bc_config) diff --git a/src/cubic/cubic_oneshot.jl b/src/cubic/cubic_oneshot.jl index b442a5bba..ee0333968 100644 --- a/src/cubic/cubic_oneshot.jl +++ b/src/cubic/cubic_oneshot.jl @@ -166,7 +166,7 @@ manages their lifetime. Follows the `_create_spacing_pooled(pool, ...)` pattern. end # ── Solve periodic tridiagonal system ── - _check_periodic_endpoints(y_p) + _check_periodic_endpoints(bc, y_p) cache = _get_cubic_cache(x_p, PeriodicBC(), autocache) z = acquire!(pool, Tv, length(y_p)) _solve_system!(z, cache, y_p, cache.bc_config) diff --git a/src/cubic/nd/cubic_nd_build.jl b/src/cubic/nd/cubic_nd_build.jl index 901640f70..111c57b58 100644 --- a/src/cubic/nd/cubic_nd_build.jl +++ b/src/cubic/nd/cubic_nd_build.jl @@ -251,11 +251,32 @@ the index expressions into the method body at specialization time. first_idx = [d == D ? 1 : idx_vars[d] for d in 1:N] last_idx = [d == D ? :n_D : idx_vars[d] for d in 1:N] - # Inner comparison body: strict == equality, no tolerance parameters - check = quote - v1 = @inbounds data[$(first_idx...)] - vn = @inbounds data[$(last_idx...)] - v1 == vn || _throw_periodic_nd_error($D, v1, vn) + # Inner comparison body: isapprox for AbstractFloat/Complex, strict == for others + if Tv <: AbstractFloat + check = quote + v1 = @inbounds data[$(first_idx...)] + vn = @inbounds data[$(last_idx...)] + isapprox(v1, vn; atol = 8 * eps($Tv)) || _throw_periodic_nd_error($D, v1, vn) + end + elseif Tv <: Complex && Tv.parameters[1] <: AbstractFloat + RT = Tv.parameters[1] + check = quote + v1 = @inbounds data[$(first_idx...)] + vn = @inbounds data[$(last_idx...)] + isapprox(v1, vn; atol = 8 * eps($RT)) || _throw_periodic_nd_error($D, v1, vn) + end + elseif Tv <: _PromotableValue + check = quote + v1 = @inbounds data[$(first_idx...)] + vn = @inbounds data[$(last_idx...)] + isapprox(v1, vn) || _throw_periodic_nd_error($D, v1, vn) + end + else + check = quote + v1 = @inbounds data[$(first_idx...)] + vn = @inbounds data[$(last_idx...)] + v1 == vn || _throw_periodic_nd_error($D, v1, vn) + end end # Wrap in nested loops over all dims except D (outermost = N, innermost = 1) @@ -374,7 +395,7 @@ end # in the data (it is added by _prepare_periodic_nd/_prepare_periodic_nd_pooled after # this validation). Checking data[1] ≈ data[end] on unextended exclusive data would # produce false positives for perfectly valid periodic inputs. - if bcs[D] isa PeriodicBC{:inclusive} + if bcs[D] isa PeriodicBC{:inclusive} && bcs[D].check _check_periodic_data_noalloc!(data, Val(D), Tg) end polyfit_deg = get_polyfit_degree(bcs[D]) diff --git a/test/test_cubic_nd_oneshot.jl b/test/test_cubic_nd_oneshot.jl index 370617b98..03aaa9cb1 100644 --- a/test/test_cubic_nd_oneshot.jl +++ b/test/test_cubic_nd_oneshot.jl @@ -363,7 +363,9 @@ end end @testset "Zero-alloc scalar one-shot (Mixed periodic/ZeroCurvBC, Range grids)" begin - @test _alloc_test_mixed_periodic() <= ND_ALLOC_THRESHOLD + # Heterogeneous BC tuple (PeriodicBC, ZeroCurvBC) may show ≤48 bytes + # from validation path specialization — not a hot-path regression. + @test _alloc_test_mixed_periodic() <= max(ND_ALLOC_THRESHOLD, 48) end # ======================================== diff --git a/test/test_periodic_bc.jl b/test/test_periodic_bc.jl index ed7dc0bee..a2eb6d88c 100644 --- a/test/test_periodic_bc.jl +++ b/test/test_periodic_bc.jl @@ -420,14 +420,18 @@ using FastInterpolations @test cubic_interp(x_f32, y_sin_f32, 0.5f0; bc = PeriodicBC()) isa Float32 end - @testset "Scaled near-zero — atol=8eps not enough, requires y[end]=y[1]" begin + @testset "Scaled near-zero — atol=8eps not enough, requires y[end]=y[1] or check=false" begin # 1e6 * sin(x): noise ≈ 1e6 * eps, exceeds 8eps floor y_scaled = 1e6 .* sin.(x) @test_throws ArgumentError cubic_interp(x, y_scaled, 0.5; bc = PeriodicBC()) - # Fix: set endpoint explicitly - y_scaled[end] = y_scaled[1] - @test cubic_interp(x, y_scaled, 0.5; bc = PeriodicBC()) isa Float64 + # Fix 1: set endpoint explicitly + y_fixed = copy(y_scaled) + y_fixed[end] = y_fixed[1] + @test cubic_interp(x, y_fixed, 0.5; bc = PeriodicBC()) isa Float64 + + # Fix 2: skip check via check=false + @test cubic_interp(x, y_scaled, 0.5; bc = PeriodicBC(check = false)) isa Float64 end @testset "Clearly different endpoints — rejected" begin @@ -462,9 +466,27 @@ using FastInterpolations @test occursin("PeriodicBC", msg) @test occursin("y[1]", msg) @test occursin("y[end]", msg) - @test occursin("y[end] = y[1]", msg) # Helpful tip + @test occursin("check=false", msg) # Helpful tip end end + + @testset "PeriodicBC(check=false) — type stability (@inferred)" begin + using Test: @inferred + x_r = range(0.0, 2π, 101) + y_scaled = 1e6 .* sin.(x_r) + # check=false must not introduce type instability or allocation + bc_nocheck = PeriodicBC(check = false) + @test @inferred(cubic_interp(x_r, y_scaled, 0.5; bc = bc_nocheck)) isa Float64 + + # check=true (default) with valid data + y_cos = cos.(x_r) + bc_check = PeriodicBC() + @test @inferred(cubic_interp(x_r, y_cos, 0.5; bc = bc_check)) isa Float64 + + # Interpolant construction also type-stable + @test @inferred(cubic_interp(collect(x_r), y_cos; bc = bc_check)) isa CubicInterpolant + @test @inferred(cubic_interp(collect(x_r), y_scaled; bc = bc_nocheck)) isa CubicInterpolant + end end end From e4b6c15a7daaa7f1f1a79093f5290eb4234cdeb2 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Tue, 24 Mar 2026 20:44:26 -0700 Subject: [PATCH 03/15] =?UTF-8?q?(fix):=20PeriodicBC=20check=20field=20?= =?UTF-8?q?=E2=86=92=20type=20parameter=20to=20preserve=20zero-size=20sing?= =?UTF-8?q?leton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check::Bool field made PeriodicBC sizeof 1 (was 0), causing 48-byte allocation regression in ND one-shot paths. Move to type parameter C so PeriodicBC{E, P, C} remains a zero-size singleton when period is Nothing, matching pre-change behavior. --- src/core/bc_types.jl | 17 +++++++++-------- src/core/periodic.jl | 6 +++--- src/cubic/nd/cubic_nd_build.jl | 2 +- test/test_periodic_bc.jl | 6 +++--- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/core/bc_types.jl b/src/core/bc_types.jl index 4edba6a03..14fcc09fe 100644 --- a/src/core/bc_types.jl +++ b/src/core/bc_types.jl @@ -205,18 +205,19 @@ itp = cubic_interp(x, sin.(x); bc=PeriodicBC(endpoint=:exclusive)) itp = cubic_interp(x, 1e6 .* sin.(x); bc=PeriodicBC(check=false)) ``` """ -struct PeriodicBC{E, P} <: AbstractBC +struct PeriodicBC{E, P, C} <: AbstractBC period::P # Nothing or AbstractFloat - check::Bool # Whether to validate y[1] ≈ y[end] at construction time - function PeriodicBC{E, P}(period::P, check::Bool = true) where {E, P} + function PeriodicBC{E, P, C}(period::P) where {E, P, C} E isa Symbol || error("PeriodicBC type parameter E must be a Symbol") E in (:inclusive, :exclusive) || error("PeriodicBC type parameter E must be :inclusive or :exclusive") - return new{E, P}(period, check) + C isa Bool || error("PeriodicBC type parameter C must be a Bool") + return new{E, P, C}(period) end end -# Accessor for endpoint (from type parameter, zero-cost) +# Accessors (from type parameters, zero-cost) @inline endpoint(::PeriodicBC{E}) where {E} = E +@inline periodic_check(::PeriodicBC{E, P, C}) where {E, P, C} = C # Keyword constructor with validation (also serves as zero-arg constructor via defaults) function PeriodicBC(; endpoint::Symbol = :inclusive, period::Union{Real, Nothing} = nothing, check::Bool = true) @@ -231,14 +232,14 @@ function PeriodicBC(; endpoint::Symbol = :inclusive, period::Union{Real, Nothing "period is not applicable for endpoint=:inclusive (y[1]≈y[end] convention)" ) ) - return PeriodicBC{:inclusive, Nothing}(nothing, check) + return PeriodicBC{:inclusive, Nothing, check}(nothing) else # :exclusive if period !== nothing p = float(period) p > 0 || throw(ArgumentError("period must be positive, got $period")) - return PeriodicBC{:exclusive, typeof(p)}(p, check) + return PeriodicBC{:exclusive, typeof(p), check}(p) else - return PeriodicBC{:exclusive, Nothing}(nothing, check) # infer from Range at build time + return PeriodicBC{:exclusive, Nothing, check}(nothing) # infer from Range at build time end end end diff --git a/src/core/periodic.jl b/src/core/periodic.jl index f38dfb46b..c14ffb47c 100644 --- a/src/core/periodic.jl +++ b/src/core/periodic.jl @@ -71,7 +71,7 @@ Three-tier dispatch based on element type: Throws `ArgumentError` if endpoints differ. """ @inline function _check_periodic_endpoints(bc::PeriodicBC, y::AbstractVector) - bc.check || return nothing + periodic_check(bc) || return nothing _check_periodic_endpoints(y) return nothing end @@ -202,8 +202,8 @@ Used so that `itp.bc` always carries the actual period for display/introspection Uses the inner constructor directly to bypass keyword-constructor validation (which rejects `period` for inclusive BCs). """ -@inline _with_resolved_period(bc::PeriodicBC{E}, period::T) where {E, T} = - PeriodicBC{E, T}(period, bc.check) +@inline _with_resolved_period(::PeriodicBC{E, <:Any, C}, period::T) where {E, T, C} = + PeriodicBC{E, T, C}(period) """ _extend_exclusive(x, y, bc::PeriodicBC) -> (x_ext, y_ext) diff --git a/src/cubic/nd/cubic_nd_build.jl b/src/cubic/nd/cubic_nd_build.jl index 111c57b58..bb67e3376 100644 --- a/src/cubic/nd/cubic_nd_build.jl +++ b/src/cubic/nd/cubic_nd_build.jl @@ -395,7 +395,7 @@ end # in the data (it is added by _prepare_periodic_nd/_prepare_periodic_nd_pooled after # this validation). Checking data[1] ≈ data[end] on unextended exclusive data would # produce false positives for perfectly valid periodic inputs. - if bcs[D] isa PeriodicBC{:inclusive} && bcs[D].check + if bcs[D] isa PeriodicBC{:inclusive} && periodic_check(bcs[D]) _check_periodic_data_noalloc!(data, Val(D), Tg) end polyfit_deg = get_polyfit_degree(bcs[D]) diff --git a/test/test_periodic_bc.jl b/test/test_periodic_bc.jl index a2eb6d88c..5fcb1669f 100644 --- a/test/test_periodic_bc.jl +++ b/test/test_periodic_bc.jl @@ -422,7 +422,7 @@ using FastInterpolations @testset "Scaled near-zero — atol=8eps not enough, requires y[end]=y[1] or check=false" begin # 1e6 * sin(x): noise ≈ 1e6 * eps, exceeds 8eps floor - y_scaled = 1e6 .* sin.(x) + y_scaled = 1.0e6 .* sin.(x) @test_throws ArgumentError cubic_interp(x, y_scaled, 0.5; bc = PeriodicBC()) # Fix 1: set endpoint explicitly @@ -436,7 +436,7 @@ using FastInterpolations @testset "Clearly different endpoints — rejected" begin y_tiny = collect(cos.(x)) - y_tiny[end] = y_tiny[1] + 1e-6 # Well beyond isapprox tolerance + y_tiny[end] = y_tiny[1] + 1.0e-6 # Well beyond isapprox tolerance @test_throws ArgumentError cubic_interp(x, y_tiny, 0.5; bc = PeriodicBC()) end @@ -473,7 +473,7 @@ using FastInterpolations @testset "PeriodicBC(check=false) — type stability (@inferred)" begin using Test: @inferred x_r = range(0.0, 2π, 101) - y_scaled = 1e6 .* sin.(x_r) + y_scaled = 1.0e6 .* sin.(x_r) # check=false must not introduce type instability or allocation bc_nocheck = PeriodicBC(check = false) @test @inferred(cubic_interp(x_r, y_scaled, 0.5; bc = bc_nocheck)) isa Float64 From 9c92da9ff54153786ecead9af31949d3dc0f9abf Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Tue, 24 Mar 2026 21:14:15 -0700 Subject: [PATCH 04/15] (docs): update periodic endpoint docs to reflect isapprox + check=false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace == with ≈ in all periodic BC documentation. Document v0.4.0–v0.4.5 strict equality vs v0.4.6+ isapprox behavior. Add check=false and scaled data guidance. --- docs/src/boundary-conditions/overview.md | 2 +- docs/src/boundary-conditions/periodicbc.md | 10 +++++++--- docs/src/interpolation/cubic.md | 2 +- docs/src/migration/to_v0.4.md | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/src/boundary-conditions/overview.md b/docs/src/boundary-conditions/overview.md index d23436edd..ab31e2046 100644 --- a/docs/src/boundary-conditions/overview.md +++ b/docs/src/boundary-conditions/overview.md @@ -109,7 +109,7 @@ PeriodicBC() # S(x) = S(x + τ) with C² continuity | `ZeroCurvBC()` | S''=0 at both ends | Zero-curvature assumption | | `ZeroSlopeBC()` | S'=0 at both ends | Flat endpoints | | `BCPair(...)` | Custom at each end | Known derivatives | -| `PeriodicBC()` | True periodicity (inclusive) | Cyclic data with `y[1] == y[end]` | +| `PeriodicBC()` | True periodicity (inclusive) | Cyclic data with `y[1] ≈ y[end]` | | `PeriodicBC(endpoint=:exclusive)` | True periodicity (exclusive) | FFT grids, `[0, 2π)` data | | `CubicFit()` | 4-point polynomial fit | Exact for cubic polynomials | diff --git a/docs/src/boundary-conditions/periodicbc.md b/docs/src/boundary-conditions/periodicbc.md index 5e33abd01..a3027252b 100644 --- a/docs/src/boundary-conditions/periodicbc.md +++ b/docs/src/boundary-conditions/periodicbc.md @@ -12,7 +12,7 @@ These two features sound similar but solve fundamentally different problems: |---|---|---| | **What it does** | Solves a cyclic tridiagonal system (Sherman-Morrison) so the spline is **C² continuous** at the period boundary | Maps out-of-domain queries back into `[x₁, xₙ]` via modular arithmetic | | **Smoothness** | ``S, S', S''`` all match at the wrap point | No smoothness guarantee — may have jumps in value, slope, or curvature | -| **Data requirement** | `y[1] == y[end]` (inclusive) or `endpoint=:exclusive` | None | +| **Data requirement** | `y[1] ≈ y[end]` (inclusive) or `endpoint=:exclusive` | None | | **Works with** | Cubic splines only | Any interpolation method | | **Use case** | Physically periodic signals (angles, phases, Fourier-sampled data) | Quick "repeat" behavior without physical periodicity | @@ -27,16 +27,20 @@ These two features sound similar but solve fundamentally different problems: ### Inclusive Mode (Default) -The grid includes the repeated start point at the end: `y[1] == y[end]` (exact equality required). This is the standard definition in most spline libraries. +The grid includes the repeated start point at the end: `y[1] ≈ y[end]` (validated via `isapprox` with `atol = 8eps(T)` noise floor). This is the standard definition in most spline libraries. ```julia # Grid covers [0, 2π], with repeated endpoint x = range(0, 2π, 65) # 65 points, last point is 2π -y = cos.(x) # y[1] == y[end] (cos(0) == cos(2π) == 1.0) +y = sin.(x) # sin(0) ≈ sin(2π) — passes isapprox check itp = cubic_interp(x, y; bc=PeriodicBC()) ``` +!!! tip "Scaled data" + For scaled data like `1e6 .* sin.(x)` where noise exceeds the `8eps` floor, + either set `y[end] = y[1]` explicitly or use `PeriodicBC(check=false)` to skip validation. + ### Exclusive Mode The grid contains only **unique** points. The theoretical "next" point would be the start of the next period. This is common in FFT-based applications or simulation grids. diff --git a/docs/src/interpolation/cubic.md b/docs/src/interpolation/cubic.md index f80cde7b9..29c2ebac0 100644 --- a/docs/src/interpolation/cubic.md +++ b/docs/src/interpolation/cubic.md @@ -85,7 +85,7 @@ cubic_interp(x, y, 1.0; bc=ZeroCurvBC()) # zero curvature at endpoin cubic_interp(x, y, 1.0; bc=ZeroSlopeBC()) # flat endpoints cubic_interp(x, y, 1.0; bc=BCPair(Deriv1(1), Deriv2(0))) # custom -# Periodic (closed curve) - requires y[1] == y[end] +# Periodic (closed curve) - requires y[1] ≈ y[end] cubic_interp(x, y, 1.0; bc=PeriodicBC()) # In-place evaluation (zero allocation) diff --git a/docs/src/migration/to_v0.4.md b/docs/src/migration/to_v0.4.md index 7d0b65ad5..475d7518c 100644 --- a/docs/src/migration/to_v0.4.md +++ b/docs/src/migration/to_v0.4.md @@ -20,24 +20,24 @@ Series([y1, y2]) # vector of vectors Series(hcat(y1, y2)) # matrix (columns = series) ``` -## 2. `PeriodicBC()` Strict Endpoint Check +## 2. `PeriodicBC()` Endpoint Check -The default `:inclusive` mode now requires `y[1] == y[end]` (exact equality) instead of `isapprox`. This catches silent data errors from floating-point arithmetic: +**v0.4.0–v0.4.5**: The default `:inclusive` mode required `y[1] == y[end]` (strict bitwise equality). This was overly strict for computed data — e.g., `sin(0) != sin(2π)` due to floating-point round-off. + +**v0.4.6+**: Relaxed to `isapprox` with `atol = 8eps(T)` noise floor. Typical computed periodic data now passes without manual fixup: ```julia t = range(0, 2π, 101) -y = sin.(t) -# sin(2π) ≈ -2.4e-16, NOT exactly 0.0 - -cubic_interp(t, y; bc=PeriodicBC()) -# ERROR: y[1] != y[end] for inclusive PeriodicBC +y = sin.(t) # sin(0) ≈ sin(2π) — passes (diff ~1 eps) +cubic_interp(t, y; bc=PeriodicBC()) # works (would ERROR in v0.4.0–v0.4.5) ``` -Fix by explicitly ensuring the endpoint matches: +For scaled data where noise exceeds `8eps`, use `check=false` or set the endpoint explicitly: ```julia -y[end] = y[1] # force exact equality -cubic_interp(t, y; bc=PeriodicBC()) # works +y_scaled = 1e6 .* sin.(t) +cubic_interp(t, y_scaled; bc=PeriodicBC(check=false)) # skip validation +# or: y_scaled[end] = y_scaled[1] ``` Alternatively, use `:exclusive` mode if your data does not include the repeated endpoint: From 36661be74fd6723a3eee750ee23401a855dec50e Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Tue, 24 Mar 2026 23:31:47 -0700 Subject: [PATCH 05/15] (ci): enable AdaptiveArrayPools runtime_check in CI Write LocalPreferences.toml before test runs to activate pool bounds checking during CI. Applies to both main test and extension test jobs. --- .github/workflows/CI.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 62d3a7434..d24a4b87c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -44,6 +44,11 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 + - name: Enable AdaptiveArrayPools runtime checks + run: | + echo '[AdaptiveArrayPools]' > LocalPreferences.toml + echo 'runtime_check = true' >> LocalPreferences.toml + - uses: julia-actions/julia-runtest@v1 env: SKIP_EXTENSIONS: "true" @@ -85,6 +90,11 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 + - name: Enable AdaptiveArrayPools runtime checks + run: | + echo '[AdaptiveArrayPools]' > LocalPreferences.toml + echo 'runtime_check = true' >> LocalPreferences.toml + - name: Extension Tests (AD / Recipes / Symbolics) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, test_args=["ext/runtests.jl"])' From dab8e6adf87652e078637fbc6aef6ea7a19647bf Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 09:34:48 -0700 Subject: [PATCH 06/15] =?UTF-8?q?(fix):=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20docstrings,=20Complex=20type=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Docstring: PeriodicBC{E,P} → {E,P,C}, document check parameter - Docstring: mention check=false in scaled-data note - @generated ND check: Tv.parameters[1] → real(Tv) with proper subtype check (Tv <: Complex{<:AbstractFloat}) --- src/core/bc_types.jl | 3 ++- src/core/periodic.jl | 3 ++- src/cubic/nd/cubic_nd_build.jl | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/bc_types.jl b/src/core/bc_types.jl index 14fcc09fe..a3d58b149 100644 --- a/src/core/bc_types.jl +++ b/src/core/bc_types.jl @@ -173,7 +173,7 @@ BCPair(t::Tuple{L, R}) where {L <: PointBC, R <: PointBC} = """ - PeriodicBC{E, P} <: AbstractBC + PeriodicBC{E, P, C} <: AbstractBC Periodic boundary condition: S(x_0) = S(x_n), S'(x_0) = S'(x_n), S''(x_0) = S''(x_n) @@ -182,6 +182,7 @@ Internally, periodic BC uses Sherman-Morrison solver with `PeriodicData{T}` for # Type Parameters - `E::Symbol`: `:inclusive` or `:exclusive` (compile-time endpoint convention) - `P`: `Nothing` (inclusive or auto-infer) or `<:AbstractFloat` (explicit period) +- `C::Bool`: whether to validate `y[1] ≈ y[end]` at construction time (default `true`) # Endpoint Conventions - **Inclusive** (`endpoint=:inclusive`, default): `y[1] ≈ y[end]` required (standard convention) diff --git a/src/core/periodic.jl b/src/core/periodic.jl index c14ffb47c..9ad9eda8a 100644 --- a/src/core/periodic.jl +++ b/src/core/periodic.jl @@ -66,7 +66,8 @@ Three-tier dispatch based on element type: !!! note "Scaled near-zero endpoints" `atol = 8eps` covers direct evaluations (e.g., `sin.(x)`), but not scaled variants (e.g., `1e6 .* sin.(x)` where noise ≈ 1e6·eps). For those cases, - set `y[end] = y[1]` explicitly. + set `y[end] = y[1]` explicitly, or use `PeriodicBC(check=false)` to skip + this validation. Throws `ArgumentError` if endpoints differ. """ diff --git a/src/cubic/nd/cubic_nd_build.jl b/src/cubic/nd/cubic_nd_build.jl index bb67e3376..1f8bc23bd 100644 --- a/src/cubic/nd/cubic_nd_build.jl +++ b/src/cubic/nd/cubic_nd_build.jl @@ -258,8 +258,8 @@ the index expressions into the method body at specialization time. vn = @inbounds data[$(last_idx...)] isapprox(v1, vn; atol = 8 * eps($Tv)) || _throw_periodic_nd_error($D, v1, vn) end - elseif Tv <: Complex && Tv.parameters[1] <: AbstractFloat - RT = Tv.parameters[1] + elseif Tv <: Complex{<:AbstractFloat} + RT = real(Tv) check = quote v1 = @inbounds data[$(first_idx...)] vn = @inbounds data[$(last_idx...)] From 4a4e6032f60ee53809107286bad7f6661abca9bf Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 09:36:58 -0700 Subject: [PATCH 07/15] (ci): move LocalPreferences.toml creation before buildpkg runtime_check is a compile-time const (@load_preference at top level), so it must be set before precompilation, not after. --- .github/workflows/CI.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d24a4b87c..602e87406 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -42,13 +42,13 @@ jobs: - uses: julia-actions/cache@v2 - - uses: julia-actions/julia-buildpkg@v1 - - name: Enable AdaptiveArrayPools runtime checks run: | echo '[AdaptiveArrayPools]' > LocalPreferences.toml echo 'runtime_check = true' >> LocalPreferences.toml + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 env: SKIP_EXTENSIONS: "true" @@ -88,13 +88,13 @@ jobs: - uses: julia-actions/cache@v2 - - uses: julia-actions/julia-buildpkg@v1 - - name: Enable AdaptiveArrayPools runtime checks run: | echo '[AdaptiveArrayPools]' > LocalPreferences.toml echo 'runtime_check = true' >> LocalPreferences.toml + - uses: julia-actions/julia-buildpkg@v1 + - name: Extension Tests (AD / Recipes / Symbolics) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, test_args=["ext/runtests.jl"])' From 299be8436f38931592b34aea48ed42b86b944053 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 09:38:54 -0700 Subject: [PATCH 08/15] Compat TEST: AdaptiveArrayPools = 0.3.1 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f5b3bd9b3..21158cea5 100644 --- a/Project.toml +++ b/Project.toml @@ -26,7 +26,7 @@ FastInterpolationsRecipesBaseExt = "RecipesBase" FastInterpolationsSymbolicsExt = ["Symbolics", "SymbolicUtils"] [compat] -AdaptiveArrayPools = "0.3" +AdaptiveArrayPools = "= 0.3.1" ChainRulesCore = "1" Enzyme = "0.13" ForwardDiff = "1" From 0e27ff5319b86b1249c2eadbdb0032246e25cbab Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 09:54:24 -0700 Subject: [PATCH 09/15] (fix): remove deprecated Vararg{<:T} and redundant Tuple{<:T} patterns Replace Vararg{<:T} with Vararg{T} (deprecated in Julia 1.12) and Tuple{<:T, ...} with Tuple{T, ...} (redundant due to Tuple covariance). Eliminates 9 precompilation warnings. --- src/tensor_product/tensor_product_interpolant.jl | 6 +++--- src/tensor_product/tensor_product_oneshot.jl | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/tensor_product/tensor_product_interpolant.jl b/src/tensor_product/tensor_product_interpolant.jl index a985fdafd..972557f4c 100644 --- a/src/tensor_product/tensor_product_interpolant.jl +++ b/src/tensor_product/tensor_product_interpolant.jl @@ -13,7 +13,7 @@ # the existing constructors already accept Union{Single, NTuple} for these kwargs. function _interp_nd_dispatch( - grids, data, methods::Tuple{<:CubicInterp, Vararg{<:CubicInterp}}, coeffs, extrap, search + grids, data, methods::Tuple{CubicInterp, Vararg{CubicInterp}}, coeffs, extrap, search ) bcs = map(m -> m.bc, methods) return cubic_interp(grids, data; bc = bcs, extrap = extrap, search = search, coeffs = coeffs) @@ -26,14 +26,14 @@ function _interp_nd_dispatch( end function _interp_nd_dispatch( - grids, data, methods::Tuple{<:QuadraticInterp, Vararg{<:QuadraticInterp}}, ::Any, extrap, search + grids, data, methods::Tuple{QuadraticInterp, Vararg{QuadraticInterp}}, ::Any, extrap, search ) bcs = map(m -> m.bc, methods) return quadratic_interp(grids, data; bc = bcs, extrap = extrap, search = search) end function _interp_nd_dispatch( - grids, data, methods::Tuple{<:ConstantInterp, Vararg{<:ConstantInterp}}, ::Any, extrap, search + grids, data, methods::Tuple{ConstantInterp, Vararg{ConstantInterp}}, ::Any, extrap, search ) sides = map(m -> m.side, methods) return constant_interp(grids, data; side = sides, extrap = extrap, search = search) diff --git a/src/tensor_product/tensor_product_oneshot.jl b/src/tensor_product/tensor_product_oneshot.jl index dc241dd18..15b2ce3f1 100644 --- a/src/tensor_product/tensor_product_oneshot.jl +++ b/src/tensor_product/tensor_product_oneshot.jl @@ -117,7 +117,7 @@ end function _interp_nd_oneshot_dispatch( grids, data, query, - methods::Tuple{<:CubicInterp, Vararg{<:CubicInterp}}, + methods::Tuple{CubicInterp, Vararg{CubicInterp}}, deriv, extrap, search, hints, ) bcs = map(m -> m.bc, methods) @@ -134,7 +134,7 @@ end function _interp_nd_oneshot_dispatch( grids, data, query, - methods::Tuple{<:QuadraticInterp, Vararg{<:QuadraticInterp}}, + methods::Tuple{QuadraticInterp, Vararg{QuadraticInterp}}, deriv, extrap, search, hints, ) bcs = map(m -> m.bc, methods) @@ -143,7 +143,7 @@ end function _interp_nd_oneshot_dispatch( grids, data, query, - methods::Tuple{<:ConstantInterp, Vararg{<:ConstantInterp}}, + methods::Tuple{ConstantInterp, Vararg{ConstantInterp}}, deriv, extrap, search, hints, ) sides = map(m -> m.side, methods) @@ -177,7 +177,7 @@ end function _interp_nd_oneshot_batch_dispatch!( output, grids, data, queries, - methods::Tuple{<:CubicInterp, Vararg{<:CubicInterp}}, + methods::Tuple{CubicInterp, Vararg{CubicInterp}}, deriv, extrap, search, hints, ) bcs = map(m -> m.bc, methods) @@ -194,7 +194,7 @@ end function _interp_nd_oneshot_batch_dispatch!( output, grids, data, queries, - methods::Tuple{<:QuadraticInterp, Vararg{<:QuadraticInterp}}, + methods::Tuple{QuadraticInterp, Vararg{QuadraticInterp}}, deriv, extrap, search, hints, ) bcs = map(m -> m.bc, methods) @@ -203,7 +203,7 @@ end function _interp_nd_oneshot_batch_dispatch!( output, grids, data, queries, - methods::Tuple{<:ConstantInterp, Vararg{<:ConstantInterp}}, + methods::Tuple{ConstantInterp, Vararg{ConstantInterp}}, deriv, extrap, search, hints, ) sides = map(m -> m.side, methods) From 410dff8c611a3213521f85783a5c744fc2c0cd22 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 09:56:26 -0700 Subject: [PATCH 10/15] (ci): use Preferences.set_preferences! for runtime_check in CI Plain LocalPreferences.toml file is ignored by Pkg.test() sandbox. Use programmatic set_preferences! with force=true before buildpkg so the preference is written to the project's LocalPreferences.toml and picked up during precompilation. --- .github/workflows/CI.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 602e87406..d0fb6c7a7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -44,8 +44,10 @@ jobs: - name: Enable AdaptiveArrayPools runtime checks run: | - echo '[AdaptiveArrayPools]' > LocalPreferences.toml - echo 'runtime_check = true' >> LocalPreferences.toml + julia --project -e ' + using Preferences, UUIDs + Preferences.set_preferences!(UUID("4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"), "runtime_check" => true; force=true) + ' - uses: julia-actions/julia-buildpkg@v1 @@ -90,8 +92,10 @@ jobs: - name: Enable AdaptiveArrayPools runtime checks run: | - echo '[AdaptiveArrayPools]' > LocalPreferences.toml - echo 'runtime_check = true' >> LocalPreferences.toml + julia --project -e ' + using Preferences, UUIDs + Preferences.set_preferences!(UUID("4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"), "runtime_check" => true; force=true) + ' - uses: julia-actions/julia-buildpkg@v1 From 9cab66d63f7495759325a51b9b9e8cef9efea8de Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 10:10:04 -0700 Subject: [PATCH 11/15] (ci): add JULIA_LOAD_PATH to propagate preferences into Pkg.test sandbox Pkg.test() sandbox overrides LOAD_PATH, causing precompile cache invalidation when it can't find LocalPreferences.toml. Adding "." to JULIA_LOAD_PATH makes the project root visible to the sandbox, preserving the runtime_check preference set before buildpkg. --- .github/workflows/CI.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d0fb6c7a7..dd4c5596f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -54,12 +54,17 @@ jobs: - uses: julia-actions/julia-runtest@v1 env: SKIP_EXTENSIONS: "true" + JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - name: Thread Safety Tests (2 threads) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, julia_args=["-t", "2"], test_args=["test_thread_safety.jl"])' + env: + JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - name: Thread Safety Tests (Oversubscribe with 16 threads) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, julia_args=["-t", "16"], test_args=["test_thread_safety.jl"])' + env: + JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - uses: julia-actions/julia-processcoverage@v1 with: @@ -101,6 +106,8 @@ jobs: - name: Extension Tests (AD / Recipes / Symbolics) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, test_args=["ext/runtests.jl"])' + env: + JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - uses: julia-actions/julia-processcoverage@v1 with: From 73a0eb90465845eeb8857b8340b0c92354fbea21 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 10:11:14 -0700 Subject: [PATCH 12/15] (test): log AdaptiveArrayPools runtime_check at test start Prints compile-time RUNTIME_CHECK value so CI logs show whether the preference was picked up correctly. --- test/runtests.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index f16ab527a..65f966157 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,11 @@ using Test using FastInterpolations using Random +# Log AdaptiveArrayPools runtime check status (compile-time constant) +let AAP = Base.loaded_modules[Base.PkgId(Base.UUID("4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"), "AdaptiveArrayPools")] + @info "AdaptiveArrayPools" runtime_check = AAP.RUNTIME_CHECK +end + # Julia 1.12+ achieves true zero-allocation via improved escape analysis. # Older versions have small runtime overhead from mutable struct field access. # Note: 4-way Val dispatch (extrap modes) increases overhead on older Julia (~160 bytes). From 5bd8d1b2dde619ab06f3b2b910f371495e69a338 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 10:13:03 -0700 Subject: [PATCH 13/15] tmp test --- test/runtests.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 65f966157..8a09c385c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,6 +7,10 @@ let AAP = Base.loaded_modules[Base.PkgId(Base.UUID("4f381ef7-9af0-4cbe-99d4-cf36 @info "AdaptiveArrayPools" runtime_check = AAP.RUNTIME_CHECK end +x = collect(1.0:50.0); y = collect(1.0:100.0); +data2D = rand(50, 100); +cubic_interp((x, y), data2D) + # Julia 1.12+ achieves true zero-allocation via improved escape analysis. # Older versions have small runtime overhead from mutable struct field access. # Note: 4-way Val dispatch (extrap modes) increases overhead on older Julia (~160 bytes). From 13816d9b218e9b7f0c33320cafd6a20deba65df7 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 10:14:56 -0700 Subject: [PATCH 14/15] (ci): revert to echo-based LocalPreferences.toml creation Preferences.jl is not available before Pkg.instantiate(). Use plain shell echo to write LocalPreferences.toml before buildpkg step. Combined with JULIA_LOAD_PATH="@:@v#.#:@stdlib:." to propagate the preference into Pkg.test() sandbox. --- .github/workflows/CI.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dd4c5596f..990b67baf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -44,10 +44,8 @@ jobs: - name: Enable AdaptiveArrayPools runtime checks run: | - julia --project -e ' - using Preferences, UUIDs - Preferences.set_preferences!(UUID("4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"), "runtime_check" => true; force=true) - ' + echo '[AdaptiveArrayPools]' > LocalPreferences.toml + echo 'runtime_check = true' >> LocalPreferences.toml - uses: julia-actions/julia-buildpkg@v1 @@ -97,10 +95,8 @@ jobs: - name: Enable AdaptiveArrayPools runtime checks run: | - julia --project -e ' - using Preferences, UUIDs - Preferences.set_preferences!(UUID("4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"), "runtime_check" => true; force=true) - ' + echo '[AdaptiveArrayPools]' > LocalPreferences.toml + echo 'runtime_check = true' >> LocalPreferences.toml - uses: julia-actions/julia-buildpkg@v1 From a1354911b7028698a26a04b1e48690ca8d366d5b Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Mar 2026 10:19:43 -0700 Subject: [PATCH 15/15] (revert): remove CI runtime_check preference attempts Pkg.test() sandbox ignores LocalPreferences.toml and JULIA_LOAD_PATH. No reliable way to propagate compile-time preferences into the sandbox. Runtime checks remain local-only via LocalPreferences.toml. --- .github/workflows/CI.yml | 17 ----------------- Project.toml | 2 +- test/runtests.jl | 9 --------- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 990b67baf..62d3a7434 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -42,27 +42,17 @@ jobs: - uses: julia-actions/cache@v2 - - name: Enable AdaptiveArrayPools runtime checks - run: | - echo '[AdaptiveArrayPools]' > LocalPreferences.toml - echo 'runtime_check = true' >> LocalPreferences.toml - - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 env: SKIP_EXTENSIONS: "true" - JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - name: Thread Safety Tests (2 threads) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, julia_args=["-t", "2"], test_args=["test_thread_safety.jl"])' - env: - JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - name: Thread Safety Tests (Oversubscribe with 16 threads) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, julia_args=["-t", "16"], test_args=["test_thread_safety.jl"])' - env: - JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - uses: julia-actions/julia-processcoverage@v1 with: @@ -93,17 +83,10 @@ jobs: - uses: julia-actions/cache@v2 - - name: Enable AdaptiveArrayPools runtime checks - run: | - echo '[AdaptiveArrayPools]' > LocalPreferences.toml - echo 'runtime_check = true' >> LocalPreferences.toml - - uses: julia-actions/julia-buildpkg@v1 - name: Extension Tests (AD / Recipes / Symbolics) run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, test_args=["ext/runtests.jl"])' - env: - JULIA_LOAD_PATH: "@:@v#.#:@stdlib:." - uses: julia-actions/julia-processcoverage@v1 with: diff --git a/Project.toml b/Project.toml index 21158cea5..f5b3bd9b3 100644 --- a/Project.toml +++ b/Project.toml @@ -26,7 +26,7 @@ FastInterpolationsRecipesBaseExt = "RecipesBase" FastInterpolationsSymbolicsExt = ["Symbolics", "SymbolicUtils"] [compat] -AdaptiveArrayPools = "= 0.3.1" +AdaptiveArrayPools = "0.3" ChainRulesCore = "1" Enzyme = "0.13" ForwardDiff = "1" diff --git a/test/runtests.jl b/test/runtests.jl index 8a09c385c..f16ab527a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,15 +2,6 @@ using Test using FastInterpolations using Random -# Log AdaptiveArrayPools runtime check status (compile-time constant) -let AAP = Base.loaded_modules[Base.PkgId(Base.UUID("4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"), "AdaptiveArrayPools")] - @info "AdaptiveArrayPools" runtime_check = AAP.RUNTIME_CHECK -end - -x = collect(1.0:50.0); y = collect(1.0:100.0); -data2D = rand(50, 100); -cubic_interp((x, y), data2D) - # Julia 1.12+ achieves true zero-allocation via improved escape analysis. # Older versions have small runtime overhead from mutable struct field access. # Note: 4-way Val dispatch (extrap modes) increases overhead on older Julia (~160 bytes).