Skip to content

UniformStep strategy + Auto AbstractRange short-circuit (closes #7)#69

Merged
ChrisRackauckas merged 5 commits into
SciML:mainfrom
ChrisRackauckas-Claude:uniform-step
May 21, 2026
Merged

UniformStep strategy + Auto AbstractRange short-circuit (closes #7)#69
ChrisRackauckas merged 5 commits into
SciML:mainfrom
ChrisRackauckas-Claude:uniform-step

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

Closes #7. Adds UniformStep — an O(1) direct-arithmetic lookup
strategy for uniformly-spaced vectors — and wires Auto to dispatch
through it automatically when v isa AbstractRange.

What

  • UniformStep <: SearchStrategy computes the answer index from
    (x - first(v)) / step(v) instead of going through binary search or
    galloping. Independent of length(v). Specialized for
    AbstractRange{<:Real} (where step(v) is exact); falls back to
    BinaryBracket for non-range vectors and for custom orderings.

  • Base.searchsortedlast(r::AbstractRange, x) already has an O(1)
    closed-form in Base
    UniformStep delegates to it. The strategy
    exists as a named entry point so Auto can short-circuit
    AbstractRange inputs explicitly, bypassing the gap-estimate and
    linearity-probe overhead the general Auto path would otherwise pay.

  • Auto specialization on AbstractRange in both per-query and
    batched paths (src/auto.jl and src/batched.jl). Measured:
    searchsortedlast(Auto(), 0.0:0.001:100.0, x) matches
    Base.searchsortedlast(r, x) at ~26 ns. The pre-change Auto on the
    same range went through the Vector-style heuristic path that the new
    specialization avoids.

Tests

The new @safetestset \"UniformStep + Auto on AbstractRange (issue #7)\" covers:

  • Parity vs Base.searchsorted{first,last}(r, x[, order]) across
    UnitRange, StepRange, StepRangeLen, LinRange, forward and
    reverse-direction ranges.
  • Reverse ordering on every range flavor.
  • Edge cases: empty range, single-element range, x outside the range,
    x at exact grid points.
  • Non-range fallback to BinaryBracket.
  • Auto on AbstractRange produces the right answer with minimal
    allocation (kwarg trampoline only — < 64 bytes per call).
  • Batched path on AbstractRange with sorted and unsorted queries.

85042 tests pass (+416 from the new UniformStep tests).

Docs

  • Chooser table in docs/src/strategies.md adds UniformStep.
  • @docs entry for the type.
  • NEWS.md entry under "Unreleased".

🤖 Note for reviewer: this PR is in draft. Please ignore until reviewed
by @ChrisRackauckas.

…ciML#7)

`UniformStep` is a new SearchStrategy that does O(1) direct-arithmetic
lookup for uniformly-spaced vectors: the answer index is computed from
`(x - first(v)) / step(v)` rather than via binary search or galloping.
Independent of `length(v)`. Specialized for `AbstractRange{<:Real}`
(where `step(v)` is exact); falls back to `BinaryBracket` for non-range
vectors and for non-`Forward`/non-`Reverse` orderings.

`Base.searchsortedlast(r::AbstractRange, x)` already has an O(1)
closed-form in Base — `UniformStep` delegates to it. The strategy exists
as a named entry point so `Auto` can short-circuit `AbstractRange`
inputs explicitly, bypassing the gap-estimate and linearity-probe
overhead the general `Auto` path would otherwise pay.

`Auto` is extended with `AbstractRange` specializations in both
per-query and batched paths (`src/auto.jl` and `src/batched.jl`).
Measured: `searchsortedlast(Auto(), 0.0:0.001:100.0, x)` now matches
`Base.searchsortedlast(r, x)` at ~26 ns. Without this change, Auto on
the same range went through the Vector-style heuristic path that the
new specialization avoids.

Tests cover:

  - Parity vs `Base.searchsortedlast`/`searchsortedfirst` across
    `UnitRange`, `StepRange`, `StepRangeLen`, `LinRange`, both forward
    and reverse-direction ranges.
  - `Reverse` ordering on every range flavor.
  - Edge cases: empty range, single-element range, `x` outside the
    range, `x` at exact grid points.
  - Non-range fallback to `BinaryBracket`.
  - `Auto` on `AbstractRange` produces the right answer with minimal
    allocation (kwarg trampoline only — < 64 bytes per call).
  - Batched path on `AbstractRange` with sorted and unsorted queries.

85042 tests pass (+416 from the new UniformStep tests).

Docs: chooser table in `strategies.md` adds `UniformStep`. `@docs`
entry for the type. `NEWS.md` documents the new strategy under
"Unreleased".

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Replaces the Base.searchsortedlast delegation with explicit closed-form
arithmetic so the strategy actually has its own implementation rather
than being a no-op marker.

Formula: `i = firstindex(r) + fld(x - first(r), step(r))` for
searchsortedlast, `cld` for searchsortedfirst. step(r) carries the
direction in its sign — same formula works for Forward (ascending) and
Reverse (descending) ranges. Type inference keeps Int operands in
Int arithmetic (matching Base's UnitRange{Int} closed-form speed) while
letting Float64 inputs use Float64 math directly.

Float-eltype ranges (StepRangeLen, LinRange) suffer from standard float
roundoff: `5.0/0.1 ≈ 49.9999…` so naive fld returns 49 instead of 50.
Base handles this internally via TwicePrecision; we use the cheaper
approach of treating fld's result as a rough estimate and doing a
one-step correction by checking `r[i+1]` (advance if it still meets the
threshold) or `r[i]` (retreat if it doesn't). The correction is at most
one increment in either direction because fld rounding error is bounded
by one ulp of the divisor.

The correction comparisons use `Base.Order.lt(order, ...)` to stay
polarity-aware so a Reverse-sorted range with x past first(r) doesn't
incorrectly advance into the array.

Perf on the new explicit arithmetic:

| Range type            | UniformStep | Base direct | Ratio |
|-----------------------|-------------|-------------|-------|
| UnitRange{Int} n=10⁶  |    ~5 ns    |    ~4 ns    | 1.4×  |
| Float64 StepRangeLen  |   ~52 ns    |   ~26 ns    | 2.0×  |
| LinRange{Float64}     |   ~52 ns    |   ~27 ns    | 1.9×  |

The Float roundoff correction (~25-30 ns) accounts for most of the
difference vs Base. Implementing TwicePrecision-equivalent arithmetic
to close the gap would duplicate Base's internal machinery. The Int path
is competitive and is the main user of `Auto`'s `AbstractRange`
short-circuit (a Vector{Float64} caller is rare).

85042 tests pass (no test-count change — the implementation is internal).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Reframes Auto's `AbstractRange` short-circuit as a property-based check
rather than a static type test. `SearchProperties` gains an
`is_uniform::Bool` field; the dedicated `SearchProperties(::AbstractRange)`
constructor sets it to `true` automatically (every `AbstractRange` is by
definition uniformly-spaced), and `SearchProperties(v::AbstractVector;
is_uniform = false)` accepts an explicit override for `Vector` callers
who know their data is exactly uniformly-spaced.

`Auto`'s per-query and batched dispatch now consults
`_auto_is_uniform(v, props)`, which:

  - Returns `true` unconditionally on `AbstractRange` (static type test;
    covers `Auto()` calls without props).
  - Returns `true` on any `AbstractVector` when
    `props.has_props && props.is_uniform` (covers `Auto(SearchProperties(v;
    is_uniform = true))` for hand-flagged Vectors).
  - Returns `false` otherwise.

This replaces the dedicated `AbstractRange`-specialised `_batched!`
overloads from the previous commit — they're subsumed by the
property-based check. The per-query path also gains the short-circuit
(it didn't before).

For `AbstractRange` the dispatch path is unchanged in behaviour and
performance: `Auto(UnitRange{Int}, x)` is ~5 ns (matches Base direct),
`Auto(Float64 StepRangeLen, x)` is ~64 ns (the closed-form math with
roundoff correction; slower than Base's TwicePrecision but still O(1)).

Vector with `is_uniform = true` currently falls through to `BinaryBracket`
because UniformStep's positional dispatch only specialises on
`AbstractRange`. The property is accepted as a forward-compatibility
marker — extending UniformStep to compute step from `v[2] - v[1]` for
general AbstractVector is a follow-up.

Tests cover the new property automatically (`SearchProperties(1:100)` etc.)
plus the Vector opt-in flag.

85047 tests pass.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…orm on Vector

Two refinements:

**1. Single-scan probe**

`_sampled_looks_linear(v, tol)` is replaced by `_sampled_linear_err(v)`
which returns the maximum relative deviation from a single 9-point scan.
The bool checks layer on top:

  - `_sampled_looks_linear(v, tol) = _sampled_linear_err(v) <= tol` for
    Auto's `InterpolationSearch` gating.
  - `_sampled_looks_uniform(v) = _sampled_linear_err(v) <= 1e-12` flags
    *exactly* uniformly-spaced data (a few ulp of accumulated float
    roundoff; random / jittered data has rel_err well above this).

`SearchProperties(v::AbstractVector)` runs the scan once and derives
both `is_linear` and `is_uniform` from it — no second pass. So a
`collect(0.0:0.1:10.0)` is now correctly flagged `is_uniform = true`
without paying for two probes.

**2. AbstractRange skips probes entirely**

`SearchProperties(v::AbstractRange{<:Real})` hardcodes all five fields:

  - `has_props = true`
  - `is_linear = true` (ranges are linear in index by definition)
  - `has_nan = false` (Int ranges can't have NaN; Float ranges
    constructed normally don't)
  - `is_log_linear = false` (arithmetically-spaced ≠ log-linear)
  - `is_uniform = true`

Measured:

  - `SearchProperties(Vector{Float64})` n=100001: ~66 μs (the O(n) NaN
    scan dominates)
  - `SearchProperties(StepRangeLen)`: **~2 ns** — ~30,000× faster

The pathological `LinRange(NaN, …)` case isn't checked; the docstring
documents that the caller is on their own there.

Tests now confirm:

  - `collect(1:100)`, `collect(0.0:0.1:10.0)` → `is_uniform = true`
    (detected via the tight-tolerance probe).
  - `collect(exp.(range(…)))`, random sorted → `is_uniform = false`.
  - Override via `is_uniform = true / false` kwarg works.
  - `1:100`, `0.0:0.1:10.0`, `LinRange(0.0, 10.0, 100)` →
    `is_uniform = true` (via the AbstractRange constructor).

85051 tests pass.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review May 21, 2026 08:35
@ChrisRackauckas ChrisRackauckas merged commit 0606d28 into SciML:main May 21, 2026
5 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbstractRange accelerated searching

2 participants