From e341c26f77d026b8b8e9e051a5cebeb4777ab719 Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Feb 2026 10:58:08 -0800 Subject: [PATCH 1/4] Remove HintedBinary, route Binary+hint to LinearBinary{2} BREAKING: Delete HintedBinary struct, _search_hinted_binary!, and all HintedBinary dispatch overloads. Binary()+hint and AutoSearch()+hint now auto-upgrade to LinearBinary{2} instead. Add LinearBinary(linear_window=0) as the direct replacement for HintedBinary behavior (hint check only, no walk, binary fallback). --- src/FastInterpolations.jl | 2 +- src/constant/constant_oneshot.jl | 2 +- src/core/search.jl | 89 ++++++------------------------ src/core/show.jl | 1 - src/cubic/cubic_types.jl | 2 +- src/linear/linear_oneshot.jl | 4 +- src/quadratic/quadratic_oneshot.jl | 2 +- 7 files changed, 23 insertions(+), 79 deletions(-) diff --git a/src/FastInterpolations.jl b/src/FastInterpolations.jl index 2f1f9201..f07e133f 100644 --- a/src/FastInterpolations.jl +++ b/src/FastInterpolations.jl @@ -42,7 +42,7 @@ export precompute_transpose! # Pre-allocate point-contiguous layout for scalar export set_cubic_cache_size!, get_cubic_cache_size, clear_cubic_cache! # Search policy types (user-facing algorithm selection) -export AbstractSearchPolicy, Binary, HintedBinary, Linear, LinearBinary, AutoSearch +export AbstractSearchPolicy, Binary, Linear, LinearBinary, AutoSearch # Boundary condition types export AbstractBC, PointBC, Deriv1, Deriv2, Deriv3, BCPair diff --git a/src/constant/constant_oneshot.jl b/src/constant/constant_oneshot.jl index b7fcd46b..5e89bf67 100644 --- a/src/constant/constant_oneshot.jl +++ b/src/constant/constant_oneshot.jl @@ -169,7 +169,7 @@ Constant (step/piecewise constant) interpolation at a single point. - `deriv::DerivOp`: Derivative order (`EvalValue()`, `DerivOp(1)`, or `DerivOp(2)`). Derivatives are always 0. - `search::AbstractSearchPolicy`: Search algorithm for interval finding - `Binary()` (default): O(log n) binary search, stateless - - `HintedBinary()`: O(1) if hint valid, O(log n) fallback + - `LinearBinary(linear_window=0)`: O(1) if hint valid, O(log n) fallback - `LinearBinary(linear_window=8)`: Linear search within window, then binary fallback # Returns diff --git a/src/core/search.jl b/src/core/search.jl index 299ef6bc..84189ec0 100644 --- a/src/core/search.jl +++ b/src/core/search.jl @@ -23,7 +23,7 @@ Abstract supertype for user-facing search policy selection. Concrete subtypes encode the search algorithm choice. -See also: [`Binary`](@ref), [`HintedBinary`](@ref), [`LinearBinary`](@ref) +See also: [`Binary`](@ref), [`LinearBinary`](@ref) """ abstract type AbstractSearchPolicy end @@ -35,7 +35,7 @@ Stateless and thread-safe. This is the default search policy. # Hint Behavior When a `hint` argument is provided with `Binary()`, the search automatically -upgrades to `HintedBinary` behavior to utilize the hint. Without a hint, +upgrades to `LinearBinary{2}` to utilize the hint for locality. Without a hint, pure binary search is used. # Example @@ -43,29 +43,15 @@ pure binary search is used. val = linear_interp(x, y, 0.5; search=Binary()) # explicit binary search val = linear_interp(x, y, 0.5) # default: AutoSearch() -# With hint: auto-upgrades to HintedBinary behavior +# With hint: auto-upgrades to LinearBinary{2} hint = Ref(1) -val = itp(0.5; search=Binary(), hint=hint) # uses hinted binary internally +val = itp(0.5; search=Binary(), hint=hint) # uses LinearBinary{2} internally ``` -See also: [`HintedBinary`](@ref), [`LinearBinary`](@ref) +See also: [`LinearBinary`](@ref) """ struct Binary <: AbstractSearchPolicy end -""" - HintedBinary <: AbstractSearchPolicy - -Hinted binary search: O(1) if query hits cached interval, O(log n) fallback. -Useful when queries repeatedly access the same interval region. - -# Example -```julia -itp = linear_interp(x, y) -vals = itp(query_points; search=HintedBinary()) -``` -""" -struct HintedBinary <: AbstractSearchPolicy end - """ Linear <: AbstractSearchPolicy @@ -103,7 +89,7 @@ for t in t_values # strictly increasing end ``` -See also: [`LinearBinary`](@ref), [`Binary`](@ref), [`HintedBinary`](@ref) +See also: [`LinearBinary`](@ref), [`Binary`](@ref) """ struct Linear <: AbstractSearchPolicy end @@ -126,7 +112,8 @@ fall in adjacent or nearby intervals. # Construction Use the factory function (recommended) to construct with a curated set of values: ```julia -LinearBinary() # default MAX=2 +LinearBinary() # default MAX=2 +LinearBinary(linear_window=0) # hint check only, no walk LinearBinary(linear_window=4) # custom MAX=4 ``` @@ -141,7 +128,7 @@ sorted_queries = sort(rand(1000)) vals = linear_interp(x, y, sorted_queries; search=LinearBinary(linear_window=8)) ``` -See also: [`Binary`](@ref), [`HintedBinary`](@ref) +See also: [`Binary`](@ref) """ struct LinearBinary{MAX} <: AbstractSearchPolicy end @@ -161,7 +148,7 @@ covering the practical range of use cases. # Arguments - `linear_window::Integer=2`: Size of the linear search window before binary fallback. - **Allowed values**: `1, 2, 4, 8, 16, 32, 64, 128` (powers of 2 from 2⁰ to 2⁷) + **Allowed values**: `0, 1, 2, 4, 8, 16, 32, 64, 128` # Throws - `ArgumentError`: If `linear_window` is not one of the allowed values. @@ -175,11 +162,13 @@ policy = LinearBinary(linear_window=3) # ERROR: ArgumentError ``` # Choosing `linear_window` +- **Zero (0)**: Hint check only, no walk — minimal random-query overhead. - **Small values (1–2)**: Minimal overhead, best for default usage and mixed query patterns - **Medium values (4–16)**: Good balance for known-sorted query patterns - **Large values (32–128)**: For highly localized queries or very large datasets """ function LinearBinary(linear_window::Integer) + linear_window == 0 && return LinearBinary{0}() linear_window == 1 && return LinearBinary{1}() linear_window == 2 && return LinearBinary{2}() linear_window == 4 && return LinearBinary{4}() @@ -188,7 +177,7 @@ function LinearBinary(linear_window::Integer) linear_window == 32 && return LinearBinary{32}() linear_window == 64 && return LinearBinary{64}() linear_window == 128 && return LinearBinary{128}() - throw(ArgumentError("`linear_window` must be one of (1, 2, 4, 8, 16, 32, 64, 128), got $linear_window")) + throw(ArgumentError("`linear_window` must be one of (0, 1, 2, 4, 8, 16, 32, 64, 128), got $linear_window")) end LinearBinary(; linear_window::Integer=2) = LinearBinary(linear_window) @@ -294,7 +283,7 @@ Internal searcher type combining search policy with hint state. Type parameters enable compile-time dispatch with zero runtime overhead. # Type Parameters -- `P`: Search policy type (`Binary`, `HintedBinary`, `LinearBinary{N}`) +- `P`: Search policy type (`Binary`, `LinearBinary{N}`) - `H`: Hint type (`NoHint`, `RefHint`) # Fields @@ -302,7 +291,7 @@ Type parameters enable compile-time dispatch with zero runtime overhead. # Note Users should not construct Searcher directly. Use the policy types (`Binary()`, -`HintedBinary()`, `LinearBinary()`) with the `search` keyword argument instead. +`LinearBinary()`) with the `search` keyword argument instead. """ struct Searcher{P<:AbstractSearchPolicy,H<:AbstractHint} hint::H @@ -331,7 +320,6 @@ Convert user-facing policy to internal Searcher with fresh hint state. Creates a new RefHint for stateful policies, ensuring thread safety. """ @inline _to_searcher(::Binary) = Searcher{Binary,NoHint}(NoHint()) -@inline _to_searcher(::HintedBinary) = Searcher{HintedBinary,RefHint}(RefHint()) @inline _to_searcher(::Linear) = Searcher{Linear,RefHint}(RefHint()) @inline _to_searcher(::LinearBinary{MAX}) where {MAX} = Searcher{LinearBinary{MAX},RefHint}(RefHint()) @@ -343,9 +331,7 @@ Creates a new RefHint for stateful policies, ensuring thread safety. # When hint=Ref{Int}, stateful policies use the external Ref for persistence. @inline _to_searcher(::Binary, ::Nothing) = Searcher{Binary,NoHint}(NoHint()) -@inline _to_searcher(::Binary, hint::Base.RefValue{Int}) = Searcher{HintedBinary,RefHint}(RefHint(hint)) # auto-upgrade to hinted -@inline _to_searcher(::HintedBinary, ::Nothing) = Searcher{HintedBinary,RefHint}(RefHint()) -@inline _to_searcher(::HintedBinary, hint::Base.RefValue{Int}) = Searcher{HintedBinary,RefHint}(RefHint(hint)) +@inline _to_searcher(::Binary, hint::Base.RefValue{Int}) = Searcher{LinearBinary{2},RefHint}(RefHint(hint)) # auto-upgrade to LinearBinary{2} @inline _to_searcher(::Linear, ::Nothing) = Searcher{Linear,RefHint}(RefHint()) @inline _to_searcher(::Linear, hint::Base.RefValue{Int}) = Searcher{Linear,RefHint}(RefHint(hint)) @inline _to_searcher(::LinearBinary{MAX}, ::Nothing) where {MAX} = Searcher{LinearBinary{MAX},RefHint}(RefHint()) @@ -355,7 +341,7 @@ Creates a new RefHint for stateful policies, ensuring thread safety. # code path misses resolution, fall back to Binary (safe stateless default). @inline _to_searcher(::AutoSearch) = Searcher{Binary,NoHint}(NoHint()) @inline _to_searcher(::AutoSearch, ::Nothing) = Searcher{Binary,NoHint}(NoHint()) -@inline _to_searcher(::AutoSearch, hint::Base.RefValue{Int}) = Searcher{HintedBinary,RefHint}(RefHint(hint)) +@inline _to_searcher(::AutoSearch, hint::Base.RefValue{Int}) = Searcher{LinearBinary{2},RefHint}(RefHint(hint)) # ---------------------------------------- # Searcher passthrough (advanced usage) @@ -533,25 +519,6 @@ end # Core hinted implementations (type-matched) # ---------------------------------------- -""" - _search_hinted_binary!(x, xq, hint_ref) -> (idx, xL, xR) - -Cached binary search: O(1) if hint valid, O(log n) fallback. -Updates `hint_ref` with the found interval index. -""" -@inline function _search_hinted_binary!( - x::AbstractVector{T}, xq::T, hint_ref::Base.RefValue{Int} -) where {T<:Real} - ix = hint_ref[] - n = length(x) - @inbounds if 1 <= ix <= n - 1 && x[ix] <= xq < x[ix + 1] - return ix, x[ix], x[ix + 1] - end - idx, xL, xR = _search_binary(x, xq) - hint_ref[] = idx - return idx, xL, xR -end - """ _search_linear!(x, xq, hint_ref) -> (idx, xL, xR) @@ -691,11 +658,6 @@ end return _search_direct!(x, spacing, _to_grid_type(xq, Tg), hint_ref) end -"""Generic wrapper for hinted binary search.""" -@inline function _search_hinted_binary!(x::AbstractVector{Tg}, xq::Tq, hint_ref::Base.RefValue{Int}) where {Tg<:Real, Tq<:Real} - return _search_hinted_binary!(x, _to_grid_type(xq, Tg), hint_ref) -end - """Generic wrapper for linear search.""" @inline function _search_linear!(x::AbstractVector{Tg}, xq::Tq, hint_ref::Base.RefValue{Int}) where {Tg<:Real, Tq<:Real} return _search_linear!(x, _to_grid_type(xq, Tg), hint_ref) @@ -733,16 +695,6 @@ end @inline search_interval(::Searcher{Binary,NoHint}, x::AbstractRange{Tg}, spacing::ScalarSpacing{Tg}, xq::Real) where {Tg} = _search_direct(x, spacing, xq) -# --- HintedBinary + RefHint --- - -@inline function search_interval(p::Searcher{HintedBinary,RefHint}, x::AbstractVector, xq::Real) - return _search_hinted_binary!(x, xq, p.hint.idx) -end - -# Range: O(1) direct + hint update -@inline search_interval(p::Searcher{HintedBinary,RefHint}, x::AbstractRange, xq::Real) = - _search_direct!(x, xq, p.hint.idx) - # --- Linear + RefHint --- @inline function search_interval(p::Searcher{Linear,RefHint}, x::AbstractVector, xq::Real) @@ -766,13 +718,6 @@ end # For uniform grids (AbstractRange + ScalarSpacing): always O(1) direct # For non-uniform grids (AbstractVector + VectorSpacing): delegate to standard search -# HintedBinary + spacing -@inline search_interval(p::Searcher{HintedBinary,RefHint}, x::AbstractVector, ::AbstractGridSpacing, xq::Real) = - _search_hinted_binary!(x, xq, p.hint.idx) - -@inline search_interval(p::Searcher{HintedBinary,RefHint}, x::AbstractRange, spacing::ScalarSpacing, xq::Real) = - _search_direct!(x, spacing, xq, p.hint.idx) - # Linear + spacing @inline search_interval(p::Searcher{Linear,RefHint}, x::AbstractVector, ::AbstractGridSpacing, xq::Real) = _search_linear!(x, xq, p.hint.idx) diff --git a/src/core/show.jl b/src/core/show.jl index 0d74cec6..be1d8fe6 100644 --- a/src/core/show.jl +++ b/src/core/show.jl @@ -119,7 +119,6 @@ _format_side(side::AbstractSide) = string(typeof(side)) """Format search policy name.""" _format_search(::Binary) = "Binary" -_format_search(::HintedBinary) = "HintedBinary" _format_search(::Linear) = "Linear" _format_search(::LinearBinary{MAX}) where {MAX} = "LinearBinary{$MAX}" _format_search(::AutoSearch) = "AutoSearch (scalar→Binary, vector→LinearBinary)" diff --git a/src/cubic/cubic_types.jl b/src/cubic/cubic_types.jl index 1a984c9a..7481c033 100644 --- a/src/cubic/cubic_types.jl +++ b/src/cubic/cubic_types.jl @@ -93,7 +93,7 @@ Returned by `cubic_interp(x, y)` (2-argument form). - `Tv`: Value type for y-values (can be Tg, Complex{Tg}, or other Number) - `C`: CubicSplineCache type (preserves grid type info for O(1) vs O(log n) lookup) - `E`: Extrapolation mode type (compile-time specialized) -- `P`: Search policy type (AutoSearch, Binary, HintedBinary, LinearBinary, etc.) +- `P`: Search policy type (AutoSearch, Binary, LinearBinary, etc.) - `BC`: Boundary condition type (BCPair or PeriodicBC) # Fields diff --git a/src/linear/linear_oneshot.jl b/src/linear/linear_oneshot.jl index d837af17..a16e302b 100644 --- a/src/linear/linear_oneshot.jl +++ b/src/linear/linear_oneshot.jl @@ -28,7 +28,7 @@ Zero-allocation linear interpolation with automatic dispatch: - `deriv::DerivOp`: Derivative order (`EvalValue()` default, `DerivOp(1)` first derivative, `DerivOp(2)` second derivative) - `search::AbstractSearchPolicy`: Search algorithm for interval finding - `Binary()` (default): O(log n) binary search, stateless - - `HintedBinary()`: O(1) if hint valid, O(log n) fallback + - `LinearBinary(linear_window=0)`: O(1) if hint valid, O(log n) fallback - `LinearBinary(linear_window=8)`: Linear search within window, then binary fallback # Example @@ -184,7 +184,7 @@ Zero-allocation scalar linear interpolation with automatic dispatch: - `deriv::DerivOp`: Derivative order (`EvalValue()` default, `DerivOp(1)` first derivative) - `search::AbstractSearchPolicy`: Search algorithm for interval finding - `Binary()` (default): O(log n) binary search, stateless - - `HintedBinary()`: O(1) if hint valid, O(log n) fallback + - `LinearBinary(linear_window=0)`: O(1) if hint valid, O(log n) fallback - `LinearBinary(linear_window=8)`: Linear search within window, then binary fallback # Returns diff --git a/src/quadratic/quadratic_oneshot.jl b/src/quadratic/quadratic_oneshot.jl index 19579928..b27d0e2c 100644 --- a/src/quadratic/quadratic_oneshot.jl +++ b/src/quadratic/quadratic_oneshot.jl @@ -172,7 +172,7 @@ C1 piecewise quadratic spline interpolation at a single point. - `deriv::DerivOp`: Derivative order -- use `EvalValue()` (default), `DerivOp(1)`, or `DerivOp(2)` - `search::AbstractSearchPolicy`: Search algorithm for interval finding - `Binary()` (default): O(log n) binary search, stateless - - `HintedBinary()`: O(1) if hint valid, O(log n) fallback + - `LinearBinary(linear_window=0)`: O(1) if hint valid, O(log n) fallback - `LinearBinary(linear_window=8)`: Linear search within window, then binary fallback # Returns From 97ed76f98782deaf9c7d160213328d275e6e3b1f Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Feb 2026 10:58:18 -0800 Subject: [PATCH 2/4] Update tests: replace HintedBinary with LinearBinary{0} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all HintedBinary references across 8 test files: - Searcher{HintedBinary,RefHint} → Searcher{LinearBinary{0},RefHint} - search=HintedBinary() → search=LinearBinary(linear_window=0) - Auto-upgrade assertions: now expect LinearBinary{2} not HintedBinary - Delete _search_hinted_binary! direct coverage test (function removed) - Fix LinearBinary constructor test: 0 is now a valid linear_window --- test/test_coeffs.jl | 2 +- test/test_derivatives.jl | 2 +- test/test_nd_hint.jl | 2 +- test/test_nd_utils_shared.jl | 2 +- test/test_search.jl | 106 ++++++++----------------- test/test_search_anchor_integration.jl | 2 +- test/test_show.jl | 6 +- test/test_thread_safety.jl | 4 +- 8 files changed, 42 insertions(+), 84 deletions(-) diff --git a/test/test_coeffs.jl b/test/test_coeffs.jl index 06c342fe..cb9e393c 100644 --- a/test/test_coeffs.jl +++ b/test/test_coeffs.jl @@ -118,7 +118,7 @@ using FastInterpolations @testset "search keyword" begin cell1 = coeffs(itp, 1.5; search=Binary()) hint = Ref(1) - cell2 = coeffs(itp, 1.5; search=HintedBinary(), hint=hint) + cell2 = coeffs(itp, 1.5; search=LinearBinary(linear_window=0), hint=hint) @test cell1.p == cell2.p @test cell1.xL == cell2.xL end diff --git a/test/test_derivatives.jl b/test/test_derivatives.jl index 43e9eba3..28ccbeea 100644 --- a/test/test_derivatives.jl +++ b/test/test_derivatives.jl @@ -2561,7 +2561,7 @@ end # DerivativeView SeriesInterpolant search/hint keywords d1 = deriv1(itp) # DerivativeView with kwargs should match direct itp call with kwargs - for search_policy in [Binary(), Linear(), LinearBinary(), HintedBinary()] + for search_policy in [Binary(), Linear(), LinearBinary(), LinearBinary(linear_window=0)] hint = Ref(1) hint2 = Ref(1) diff --git a/test/test_nd_hint.jl b/test/test_nd_hint.jl index 16ebdf6e..5bb290e3 100644 --- a/test/test_nd_hint.jl +++ b/test/test_nd_hint.jl @@ -125,7 +125,7 @@ using FastInterpolations result = itp((qx, qy); hint=hints) @test result ≈ itp((qx, qy)) - # Verify exact interval, proving HintedBinary was used + # Verify exact interval, proving hint-based search was used @test hints[1][] == expected_interval(x, qx) @test hints[2][] == expected_interval(y, qy) end diff --git a/test/test_nd_utils_shared.jl b/test/test_nd_utils_shared.jl index bc5d0c43..9608c010 100644 --- a/test/test_nd_utils_shared.jl +++ b/test/test_nd_utils_shared.jl @@ -51,7 +51,7 @@ import FastInterpolations: _resolve_extrap_nd, _resolve_search_nd, _resolve_bcs_ @testset "reject wrong-length tuple" begin @test_throws ArgumentError _resolve_search_nd((Binary(), LinearBinary()), Val(3)) - @test_throws ArgumentError _resolve_search_nd((Binary(), LinearBinary(), Linear(), HintedBinary()), Val(3)) + @test_throws ArgumentError _resolve_search_nd((Binary(), LinearBinary(), Linear(), LinearBinary(linear_window=0)), Val(3)) end end diff --git a/test/test_search.jl b/test/test_search.jl index fb3b4589..afc0c566 100644 --- a/test/test_search.jl +++ b/test/test_search.jl @@ -1,7 +1,7 @@ using Test using FastInterpolations using FastInterpolations: search_interval, _search_binary, _search_direct, _search_interval, - Searcher, Binary, HintedBinary, Linear, LinearBinary, AutoSearch, + Searcher, Binary, Linear, LinearBinary, AutoSearch, NoHint, RefHint, DEFAULT_SEARCHER, ScalarSpacing, _create_spacing, _to_searcher, _resolve_search @@ -14,7 +14,6 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "Search Policy Types" begin @testset "Type Hierarchy" begin @test Binary <: FastInterpolations.AbstractSearchPolicy - @test HintedBinary <: FastInterpolations.AbstractSearchPolicy @test Linear <: FastInterpolations.AbstractSearchPolicy @test LinearBinary{8} <: FastInterpolations.AbstractSearchPolicy @@ -34,7 +33,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @test hint2.idx[] == 50 # Full searcher construction - searcher = Searcher{HintedBinary,RefHint}(RefHint(Ref(10))) + searcher = Searcher{LinearBinary{0},RefHint}(RefHint(Ref(10))) @test searcher.hint.idx[] == 10 end end @@ -96,12 +95,12 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear # Hinted Binary Search Tests # ======================================== - @testset "Hinted Binary with RefHint" begin + @testset "LinearBinary{0} with RefHint" begin x = collect(range(0.0, 1.0, 101)) @testset "Hint Update" begin hint = Ref(1) - policy = Searcher{HintedBinary,RefHint}(RefHint(hint)) + policy = Searcher{LinearBinary{0},RefHint}(RefHint(hint)) # Query far from hint - should update idx, _, _ = search_interval(policy, x, 0.75) @@ -111,7 +110,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "Hint Hit (O(1) path)" begin hint = Ref(50) - policy = Searcher{HintedBinary,RefHint}(RefHint(hint)) + policy = Searcher{LinearBinary{0},RefHint}(RefHint(hint)) # Query within hint interval - should hit O(1) path # Interval 50 covers [0.49, 0.50) @@ -122,7 +121,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "Monotonic Query Sequence" begin hint = Ref(1) - policy = Searcher{HintedBinary,RefHint}(RefHint(hint)) + policy = Searcher{LinearBinary{0},RefHint}(RefHint(hint)) # Monotonically increasing queries for xi in range(0.1, 0.9, 10) @@ -134,7 +133,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "Range Updates Hint" begin x_range = range(0.0, 1.0, 101) hint = Ref(50) - policy = Searcher{HintedBinary,RefHint}(RefHint(hint)) + policy = Searcher{LinearBinary{0},RefHint}(RefHint(hint)) # Range path: hint checked first, then O(1) fallback + hint update idx, _, _ = search_interval(policy, x_range, 0.1) @@ -418,7 +417,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "Hinted Search on Non-Uniform" begin hint = Ref(1) - policy_hint = Searcher{HintedBinary,RefHint}(RefHint(hint)) + policy_hint = Searcher{LinearBinary{0},RefHint}(RefHint(hint)) # Sequential queries queries = [0.005, 0.015, 0.05, 0.2, 0.5, 0.8, 0.95] @@ -467,39 +466,6 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear # ======================================== # These tests specifically target uncovered code paths - @testset "Coverage: _search_hinted_binary! Direct" begin - # Import internal function for direct testing - _search_hinted_binary! = FastInterpolations._search_hinted_binary! - - x = collect(range(0.0, 1.0, 101)) - - @testset "O(1) Hint Hit Path" begin - hint_ref = Ref(50) - # Query exactly in interval 50: [0.49, 0.50) - idx, xL, xR = _search_hinted_binary!(x, 0.495, hint_ref) - @test idx == 50 - @test xL ≈ 0.49 atol=1e-12 - @test xR ≈ 0.50 atol=1e-12 - @test hint_ref[] == 50 # Unchanged (direct hit) - end - - @testset "Hint Miss - Fallback to Binary" begin - hint_ref = Ref(10) - # Query far from hint - idx, xL, xR = _search_hinted_binary!(x, 0.75, hint_ref) - @test idx == 76 - @test hint_ref[] == 76 # Updated after binary search - end - - @testset "Hint at Boundary" begin - hint_ref = Ref(1) - # Query in first interval - idx, _, _ = _search_hinted_binary!(x, 0.005, hint_ref) - @test idx == 1 - @test hint_ref[] == 1 - end - end - @testset "Coverage: LinearBinaryAlg Direct Hit" begin # Test the early return path when hint already points to correct interval x = collect(range(0.0, 1.0, 101)) @@ -602,10 +568,10 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear end end - @testset "Coverage: HintedBinaryAlg with Range" begin + @testset "Coverage: LinearBinary{0} with Range" begin x_range = range(0.0, 1.0, 101) hint = Ref(80) - policy = Searcher{HintedBinary,RefHint}(RefHint(hint)) + policy = Searcher{LinearBinary{0},RefHint}(RefHint(hint)) @testset "Range Updates Hint" begin idx, xL, xR = search_interval(policy, x_range, 0.15) @@ -698,7 +664,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "LinearBinary Constructor" begin @testset "Valid linear_window Values" begin # All allowed linear_window values - # Note: linear_window=1 maps to LinearBinary{2} (see factory function) + @test LinearBinary(linear_window=0) isa LinearBinary{0} @test LinearBinary(linear_window=1) isa LinearBinary{1} @test LinearBinary(linear_window=2) isa LinearBinary{2} @test LinearBinary(linear_window=4) isa LinearBinary{4} @@ -717,7 +683,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear end @testset "Invalid linear_window Throws ArgumentError" begin - invalid_steps = (0, 3, 5, 6, 7, 9, 10, 15, 17, 100, 256) + invalid_steps = (3, 5, 6, 7, 9, 10, 15, 17, 100, 256) for ms in invalid_steps @test_throws ArgumentError LinearBinary(linear_window=ms) @test_throws ArgumentError LinearBinary(ms) @@ -759,7 +725,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear itp(out_vec1, xq_vec) # Default search (Binary) itp(out_vec2, xq_vec; search=Binary()) # Binary - itp(out_vec3, xq_vec; search=HintedBinary()) # HintedBinary + itp(out_vec3, xq_vec; search=LinearBinary(linear_window=0)) # LinearBinary{0} itp(out_vec4, xq_vec; search=Linear()) # Linear (pure) itp(out_vec5, xq_vec; search=LinearBinary()) # LinearBinary{8} itp(out_vec6, xq_vec; search=LinearBinary{2}()) # LinearBinary{2} @@ -786,9 +752,9 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear itp_lb = linear_interp(x, y; search=LinearBinary()) @test itp_lb.search_policy isa LinearBinary{2} - # Create with HintedBinary as default - itp_hb = linear_interp(x, y; search=HintedBinary()) - @test itp_hb.search_policy isa HintedBinary + # Create with LinearBinary{0} as default + itp_hb = linear_interp(x, y; search=LinearBinary(linear_window=0)) + @test itp_hb.search_policy isa LinearBinary{0} # Create with default (AutoSearch) itp_auto = linear_interp(x, y) @@ -811,17 +777,17 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @test hint[] >= 540 && hint[] <= 570 end - @testset "Override with Binary + hint auto-upgrades to HintedBinary" begin + @testset "Override with Binary + hint auto-upgrades to LinearBinary{2}" begin # Create with LinearBinary default, but override with Binary at call time itp = linear_interp(x, y; search=LinearBinary()) hint = Ref(100) - # Override with Binary + hint → auto-upgrades to HintedBinary behavior + # Override with Binary + hint → auto-upgrades to LinearBinary{2} for xi in range(0.5, 0.6, 10) yi = itp(xi; search=Binary(), hint=hint) end - # hint should be updated (auto-upgraded to HintedBinary) + # hint should be updated (auto-upgraded to LinearBinary{2}) @test hint[] >= 500 && hint[] <= 610 end @@ -841,12 +807,12 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear end @testset "Quadratic interpolant baked-in policy" begin - itp = quadratic_interp(x, y; search=HintedBinary()) - @test itp.search_policy isa HintedBinary + itp = quadratic_interp(x, y; search=LinearBinary(linear_window=0)) + @test itp.search_policy isa LinearBinary{0} hint = Ref(300) yi = itp(0.35; hint=hint) - # HintedBinary updates hint + # LinearBinary{0} updates hint @test hint[] >= 340 && hint[] <= 360 end @@ -876,8 +842,8 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear end @testset "CubicSeriesInterpolant stored policy" begin - sitp = cubic_interp(x, [y1, y2]; search=HintedBinary()) - @test sitp.search_policy isa HintedBinary + sitp = cubic_interp(x, [y1, y2]; search=LinearBinary(linear_window=0)) + @test sitp.search_policy isa LinearBinary{0} hint = Ref(600) yi = sitp(0.65; hint=hint) @@ -902,10 +868,10 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear sitp = linear_interp(x, [y1, y2]; search=LinearBinary()) hint = Ref(250) - # Override with Binary + hint → auto-upgrades to HintedBinary + # Override with Binary + hint → auto-upgrades to LinearBinary{2} yi = sitp(0.75; search=Binary(), hint=hint) - # hint should be updated (auto-upgraded to HintedBinary) + # hint should be updated (auto-upgraded to LinearBinary{2}) @test hint[] >= 740 && hint[] <= 760 end end @@ -919,10 +885,6 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear s1 = _to_searcher(Binary()) @test s1.hint isa NoHint - s2 = _to_searcher(HintedBinary()) - @test s2.hint isa RefHint - @test s2.hint.idx[] == 1 - s3 = _to_searcher(Linear()) @test s3.hint isa RefHint @@ -939,9 +901,6 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear s1 = _to_searcher(LinearBinary(), nothing) @test s1.hint.idx[] == 1 - s2 = _to_searcher(HintedBinary(), nothing) - @test s2.hint.idx[] == 1 - s3 = _to_searcher(Linear(), nothing) @test s3.hint.idx[] == 1 end @@ -958,8 +917,8 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @test s2.hint.idx[] == 75 end - @testset "Binary with hint auto-upgrades to HintedBinary" begin - # Binary policy with hint auto-upgrades to HintedBinary behavior + @testset "Binary with hint auto-upgrades to LinearBinary{2}" begin + # Binary policy with hint auto-upgrades to LinearBinary{2} ext_ref = Ref(100) s1 = _to_searcher(Binary(), ext_ref) @test s1.hint isa RefHint @@ -1012,7 +971,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear xi = 0.2 for _ in 1:50 xi += 2e-3 - yi = itp(xi; search=HintedBinary(), hint=hint) + yi = itp(xi; search=LinearBinary(linear_window=0), hint=hint) end # hint should track xi position (0.3 → index ~301) @@ -1048,7 +1007,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @testset "quadratic_interp oneshot" begin hint = Ref(50) for xi in range(0.5, 0.9, 10) - yi = quadratic_interp(x, y, xi; search=HintedBinary(), hint=hint) + yi = quadratic_interp(x, y, xi; search=LinearBinary(linear_window=0), hint=hint) @test yi ≈ xi^2 atol=1e-4 end @test hint[] > 50 @@ -1088,7 +1047,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear sitp = cubic_interp(x, [y1, y2]) hint = Ref(50) - yi = sitp(0.55; search=HintedBinary(), hint=hint) + yi = sitp(0.55; search=LinearBinary(linear_window=0), hint=hint) @test hint[] >= 54 && hint[] <= 57 end end @@ -1151,7 +1110,6 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear @test _resolve_search(Binary(), [0.1]) === Binary() @test _resolve_search(LinearBinary(), 0.5) isa LinearBinary @test _resolve_search(LinearBinary(), [0.1]) isa LinearBinary - @test _resolve_search(HintedBinary(), 0.5) === HintedBinary() @test _resolve_search(Linear(), [0.1]) === Linear() # Tuple of policies: each resolved independently @@ -1265,7 +1223,7 @@ using FastInterpolations: search_interval, _search_binary, _search_direct, _sear # y = x^2, so itp(0.5) ≈ 0.25 @test itp(0.5) ≈ 0.25 atol=1e-6 - # Scalar call with hint: Binary+hint auto-upgrades to HintedBinary → hint updates + # Scalar call with hint: Binary+hint auto-upgrades to LinearBinary{2} → hint updates hint_scalar = Ref(1) itp(0.5; hint=hint_scalar) @test hint_scalar[] >= 490 && hint_scalar[] <= 510 # hint moved to ~500 diff --git a/test/test_search_anchor_integration.jl b/test/test_search_anchor_integration.jl index 2418f063..5fe5fbc3 100644 --- a/test/test_search_anchor_integration.jl +++ b/test/test_search_anchor_integration.jl @@ -2,7 +2,7 @@ using Test using FastInterpolations using FastInterpolations: _anchor_query, _fill_anchors!, _LinearAnchoredQuery, _ConstantAnchoredQuery, _QuadraticAnchoredQuery, _CubicAnchoredQuery, - Searcher, HintedBinary, LinearBinary, RefHint + Searcher, LinearBinary, RefHint @testset "Search Policy Anchor Integration" begin diff --git a/test/test_show.jl b/test/test_show.jl index 89f3c664..2bca17ab 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -200,7 +200,7 @@ # Test different search policies itp_binary = linear_interp(x_vec, y; search=Binary()) - itp_hinted = linear_interp(x_vec, y; search=HintedBinary()) + itp_hinted = linear_interp(x_vec, y; search=LinearBinary(linear_window=0)) itp_linear_binary = linear_interp(x_vec, y; search=LinearBinary()) itp_linear_binary_16 = linear_interp(x_vec, y; search=LinearBinary(linear_window=16)) @@ -208,7 +208,7 @@ @test occursin("Binary", verbose_binary) verbose_hinted = sprint(show, MIME("text/plain"), itp_hinted) - @test occursin("HintedBinary", verbose_hinted) + @test occursin("LinearBinary{0}", verbose_hinted) verbose_lb = sprint(show, MIME("text/plain"), itp_linear_binary) @test occursin("LinearBinary{2}", verbose_lb) @@ -472,7 +472,7 @@ @test FI._format_search(Linear()) == "Linear" @test FI._format_search(Binary()) == "Binary" - @test FI._format_search(HintedBinary()) == "HintedBinary" + @test FI._format_search(LinearBinary(linear_window=0)) == "LinearBinary{0}" @test FI._format_search(LinearBinary()) == "LinearBinary{2}" @test FI._format_search(LinearBinary(linear_window=4)) == "LinearBinary{4}" @test FI._format_search(AutoSearch()) == "AutoSearch (scalar→Binary, vector→LinearBinary)" diff --git a/test/test_thread_safety.jl b/test/test_thread_safety.jl index 7d93e573..306a0ba9 100644 --- a/test/test_thread_safety.jl +++ b/test/test_thread_safety.jl @@ -648,7 +648,7 @@ end @test all(isfinite, results) end - @testset "HintedBinary concurrent access" begin + @testset "LinearBinary{0} concurrent access" begin x = collect(range(0.0, 1.0, 1001)) y = sin.(2π .* x) itp = linear_interp(x, y) @@ -658,7 +658,7 @@ end @threads for i in eachindex(results) try - results[i] = itp(rand(); search=HintedBinary()) + results[i] = itp(rand(); search=LinearBinary(linear_window=0)) catch atomic_add!(errors, 1) end From 20c425fe8d7ce9014701829af7556fc5c14f470e Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Feb 2026 10:58:26 -0800 Subject: [PATCH 3/4] Update docs: remove HintedBinary, document LinearBinary{0} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete HintedBinary section from policies.md - Remove from decision matrix and @docs block - Update auto-upgrade docs: Binary+hint → LinearBinary{2} - Add linear_window=0 to LinearBinary tuning guide --- docs/src/api/types.md | 1 - docs/src/guides/search/hints.md | 10 +++++----- docs/src/guides/search/overview.md | 3 +-- docs/src/guides/search/policies.md | 32 ++++++------------------------ 4 files changed, 12 insertions(+), 34 deletions(-) diff --git a/docs/src/api/types.md b/docs/src/api/types.md index b0f6e014..ab3cc07a 100644 --- a/docs/src/api/types.md +++ b/docs/src/api/types.md @@ -108,7 +108,6 @@ Search policies control how the interpolant finds the correct interval for a que AbstractSearchPolicy AutoSearch Binary -HintedBinary Linear LinearBinary ``` diff --git a/docs/src/guides/search/hints.md b/docs/src/guides/search/hints.md index 591845da..ae0ba049 100644 --- a/docs/src/guides/search/hints.md +++ b/docs/src/guides/search/hints.md @@ -4,7 +4,7 @@ Hints allow you to persist search state across calls, enabling O(1) lookup for s ## Basic Usage -For stateful policies (`HintedBinary`, `Linear`, `LinearBinary`), provide an external `Ref{Int}` to persist the hint: +For stateful policies (`Linear`, `LinearBinary`), provide an external `Ref{Int}` to persist the hint: ```julia itp = linear_interp(x, y) @@ -30,18 +30,18 @@ External hints are particularly useful for: ## Auto-Upgrade Behavior -When you provide a `hint` argument with `Binary()`, the search automatically upgrades to `HintedBinary` behavior: +When you provide a `hint` argument with `Binary()`, the search automatically upgrades to `LinearBinary{2}`: ```julia hint = Ref(1) -val = itp(0.5; search=Binary(), hint=hint) # auto-upgrades to HintedBinary +val = itp(0.5; search=Binary(), hint=hint) # auto-upgrades to LinearBinary{2} ``` -This also works with the default `AutoSearch()` — scalar queries resolve to `Binary()`, and if a hint is provided, they auto-upgrade to `HintedBinary`: +This also works with the default `AutoSearch()` — scalar queries resolve to `Binary()`, and if a hint is provided, they auto-upgrade to `LinearBinary{2}`: ```julia hint = Ref(1) -val = itp(0.5; hint=hint) # AutoSearch → Binary() → auto-upgrades to HintedBinary +val = itp(0.5; hint=hint) # AutoSearch → Binary() → auto-upgrades to LinearBinary{2} ``` Without a hint, binary search is used (no hint tracking). diff --git a/docs/src/guides/search/overview.md b/docs/src/guides/search/overview.md index 75b8a5b5..fa74ad61 100644 --- a/docs/src/guides/search/overview.md +++ b/docs/src/guides/search/overview.md @@ -35,7 +35,6 @@ nothing # hide |:-------|:---------|:-----------|:--------------| | [`AutoSearch()`](@ref search_policies) | **General use (default)** — adapts per query type | Delegates to Binary/LinearBinary | ✓ Stateless | | [`Binary()`](@ref search_policies) | Random access (explicit) | O(log n) | ✓ Stateless | -| [`HintedBinary()`](@ref search_policies) | Repeated queries in same region | O(1) hit, O(log n) miss | ✓ With hint | | [`LinearBinary()`](@ref search_policies) | **Monotonic queries (explicit)** | O(1) local, O(log n) fallback | ✓ With hint | | [`Linear()`](@ref search_policies) | Close + monotonic queries (expert) | O(1) amortized | ✓ With hint | @@ -50,7 +49,7 @@ nothing # hide - **General use / unknown pattern** → `AutoSearch()` ✅ **default** — adapts scalar→`Binary()`, vector→`LinearBinary()` - **Known random access** → `Binary()` (explicit; skips AutoSearch dispatch) -- **Queries cluster in same region** → `HintedBinary()` +- **Queries cluster in same region** → `LinearBinary(linear_window=0)` (hint check only) - **Known monotonic queries (sorted, streaming, ODE)** → `LinearBinary()` (explicit) - **Strictly monotonic, performance-critical** → `Linear()` (benchmark first! see below) diff --git a/docs/src/guides/search/policies.md b/docs/src/guides/search/policies.md index ed2c96e0..fee138d8 100644 --- a/docs/src/guides/search/policies.md +++ b/docs/src/guides/search/policies.md @@ -57,28 +57,6 @@ val = itp(0.5; search=Binary()) # explicit Binary --- -## HintedBinary - -Caches the last-found interval. If the next query falls in the same interval, lookup is O(1). - -**Complexity**: O(1) cache hit, O(log n) cache miss - -**When to use**: -- Queries that cluster in the same region -- Monte Carlo sampling within a subregion -- Iterative refinement around a point - -```julia -itp = linear_interp(x, y; search=HintedBinary()) -for xi in query_points - val = itp(xi) # O(1) when consecutive queries hit same interval -end -``` - -**How it works**: Before binary search, checks if the query falls in the cached interval. If yes, returns immediately. If no, performs full binary search and updates the cache. - ---- - ## Linear Maximum-speed linear search for **strictly monotonic, closely-spaced queries**. Scans the grid sequentially one interval at a time from the hint until the target is found—no binary fallback, no window limit. @@ -142,18 +120,20 @@ vals = itp(sorted_queries) # O(1) amortized for sorted input You can tune the linear search window size before falling back to binary search: ```julia -LinearBinary() # default: linear_window=2 -LinearBinary(linear_window=4) # moderate bound for known-sorted sequences -LinearBinary(linear_window=16) # larger bound for sparser-spaced sorted queries +LinearBinary() # default: linear_window=2 +LinearBinary(linear_window=0) # hint check only, no walk (minimal random overhead) +LinearBinary(linear_window=4) # moderate bound for known-sorted sequences +LinearBinary(linear_window=16) # larger bound for sparser-spaced sorted queries ``` **Guidelines**: +- **Zero (0)**: Hint check only, no walk — minimal random-query overhead. Good when queries cluster in the same interval. - **Small `linear_window` (1–2)**: Minimal overhead; best for mixed or unknown patterns. The default `LinearBinary()` uses `2`. - **Medium `linear_window` (4–16)**: Good balance when queries are known-sorted - **Large `linear_window` (32–128)**: For highly localized queries or very large datasets !!! note "Type Parameter Restriction" - `linear_window` is restricted to powers of 2 (1, 2, 4, 8, 16, 32, 64) to prevent type parameter explosion. Each unique value creates a specialized method, so limiting choices keeps compile times reasonable. + `linear_window` is restricted to `0` plus powers of 2 (1, 2, 4, 8, 16, 32, 64, 128) to prevent type parameter explosion. Each unique value creates a specialized method, so limiting choices keeps compile times reasonable. --- From 9ccfb5d9e340f2e34fb7f24fca83ef3bda74be3f Mon Sep 17 00:00:00 2001 From: Min-Gu Yoo Date: Wed, 25 Feb 2026 11:28:24 -0800 Subject: [PATCH 4/4] Update Binary search behavior to use LinearBinary() as default with hints --- src/core/search.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/search.jl b/src/core/search.jl index 84189ec0..96eda6c9 100644 --- a/src/core/search.jl +++ b/src/core/search.jl @@ -35,7 +35,7 @@ Stateless and thread-safe. This is the default search policy. # Hint Behavior When a `hint` argument is provided with `Binary()`, the search automatically -upgrades to `LinearBinary{2}` to utilize the hint for locality. Without a hint, +upgrades to `LinearBinary()` (default window) to utilize the hint for locality. Without a hint, pure binary search is used. # Example @@ -43,9 +43,9 @@ pure binary search is used. val = linear_interp(x, y, 0.5; search=Binary()) # explicit binary search val = linear_interp(x, y, 0.5) # default: AutoSearch() -# With hint: auto-upgrades to LinearBinary{2} +# With hint: auto-upgrades to LinearBinary() (default window) hint = Ref(1) -val = itp(0.5; search=Binary(), hint=hint) # uses LinearBinary{2} internally +val = itp(0.5; search=Binary(), hint=hint) # uses LinearBinary() internally ``` See also: [`LinearBinary`](@ref) @@ -331,7 +331,7 @@ Creates a new RefHint for stateful policies, ensuring thread safety. # When hint=Ref{Int}, stateful policies use the external Ref for persistence. @inline _to_searcher(::Binary, ::Nothing) = Searcher{Binary,NoHint}(NoHint()) -@inline _to_searcher(::Binary, hint::Base.RefValue{Int}) = Searcher{LinearBinary{2},RefHint}(RefHint(hint)) # auto-upgrade to LinearBinary{2} +@inline _to_searcher(::Binary, hint::Base.RefValue{Int}) = _to_searcher(LinearBinary(), hint) # auto-upgrade to default LinearBinary @inline _to_searcher(::Linear, ::Nothing) = Searcher{Linear,RefHint}(RefHint()) @inline _to_searcher(::Linear, hint::Base.RefValue{Int}) = Searcher{Linear,RefHint}(RefHint(hint)) @inline _to_searcher(::LinearBinary{MAX}, ::Nothing) where {MAX} = Searcher{LinearBinary{MAX},RefHint}(RefHint()) @@ -341,7 +341,7 @@ Creates a new RefHint for stateful policies, ensuring thread safety. # code path misses resolution, fall back to Binary (safe stateless default). @inline _to_searcher(::AutoSearch) = Searcher{Binary,NoHint}(NoHint()) @inline _to_searcher(::AutoSearch, ::Nothing) = Searcher{Binary,NoHint}(NoHint()) -@inline _to_searcher(::AutoSearch, hint::Base.RefValue{Int}) = Searcher{LinearBinary{2},RefHint}(RefHint(hint)) +@inline _to_searcher(::AutoSearch, hint::Base.RefValue{Int}) = _to_searcher(LinearBinary(), hint) # auto-upgrade to default LinearBinary # ---------------------------------------- # Searcher passthrough (advanced usage)