Skip to content

Commit 1351a3a

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Make SearchProperties probes robust to non-primitive Real eltypes (AD) (#100)
* Make SearchProperties probes robust to non-primitive Real eltypes (AD) `SearchProperties(v::AbstractVector{<:Real})` runs sampled linearity / uniformity / log-linearity probes that coerced intermediate values with `Float64(...)`. `ForwardDiff.Dual <: Real`, but `Float64(::Dual)` is deliberately undefined, so constructing `SearchProperties` on a Dual-valued knot vector — as happens when an interpolation is reconstructed inside `ForwardDiff` and the knots are promoted to Duals — threw a `MethodError`. The `Float64(...)` coercions are redundant for the primitive eltypes: for `Int`/`Float32`/`Float64` the probe expressions are already `Float64`. Drop them and compute in native arithmetic, so the probes work for any ordered `Real` (the classification is taken from the primal and partials are kept intact), while the primitive hot path is byte-for-byte unchanged. Adds a ForwardDiff regression testset differentiating through both values and Dual-valued knots, and wires ForwardDiff into the test targets. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvBXVKGujjeaCB3iLwsDeG * Apply suggestion from @ChrisRackauckas --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ec34cf7 commit 1351a3a

3 files changed

Lines changed: 56 additions & 7 deletions

File tree

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ authors = ["Chris Elrod <elrodc@gmail.com> and contributors"]
77
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
88

99
[compat]
10+
ForwardDiff = "0.10, 1"
1011
PrecompileTools = "1.0.1"
1112
SafeTestsets = "0.1"
1213
SciMLTesting = "2.2"
@@ -15,10 +16,11 @@ Test = "1.10"
1516
julia = "1.10"
1617

1718
[extras]
19+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
1820
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
1921
SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283"
2022
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
2123
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2224

2325
[targets]
24-
test = ["SafeTestsets", "SciMLTesting", "StableRNGs", "Test"]
26+
test = ["ForwardDiff", "SafeTestsets", "SciMLTesting", "StableRNGs", "Test"]

src/search_properties.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const _AUTO_LINEAR_REL_TOLERANCE = 1.0e-3
4747
for k in 1:9
4848
kk = 1 + (k * nm1) ÷ 10
4949
expected = v1 + (kk - 1) / nm1 * span
50-
rel_err = Float64(abs(v[kk] - expected) / abs_span)
50+
rel_err = abs(v[kk] - expected) / abs_span
5151
rel_err > max_err && (max_err = rel_err)
5252
end
5353
return max_err
@@ -83,12 +83,12 @@ function _validated_uniform(v::AbstractVector{<:Real})
8383
n < 2 && return false
8484
@inbounds begin
8585
v1, vn = v[1], v[n]
86-
span = Float64(vn - v1)
86+
span = vn - v1
8787
(iszero(span) || !isfinite(span)) && return false
8888
nm1 = n - 1
8989
for i in 2:nm1
9090
expected = v1 + (i - 1) / nm1 * span
91-
abs(Float64(v[i] - expected)) > _UNIFORM_REL_TOLERANCE * abs(span) &&
91+
abs(v[i] - expected) > _UNIFORM_REL_TOLERANCE * abs(span) &&
9292
return false
9393
end
9494
end
@@ -110,8 +110,8 @@ end
110110
@inbounds begin
111111
v1, vn = v[1], v[n]
112112
(v1 <= 0 || vn <= 0 || !isfinite(v1) || !isfinite(vn)) && return false
113-
log_v1 = log(Float64(v1))
114-
log_vn = log(Float64(vn))
113+
log_v1 = log(v1)
114+
log_vn = log(vn)
115115
span = log_vn - log_v1
116116
(iszero(span) || !isfinite(span)) && return false
117117
abs_span = abs(span)
@@ -121,7 +121,7 @@ end
121121
vk = v[kk]
122122
(vk <= 0 || !isfinite(vk)) && return false
123123
expected = log_v1 + (kk - 1) / nm1 * span
124-
rel_err = abs(log(Float64(vk)) - expected) / abs_span
124+
rel_err = abs(log(vk) - expected) / abs_span
125125
rel_err > tol && return false
126126
end
127127
end

test/core_tests.jl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,53 @@ end
645645
@test p_nonuniform.inv_step == 0.0
646646
end
647647

648+
@safetestset "SearchProperties with ForwardDiff.Dual knots" begin
649+
using FindFirstFunctions
650+
using FindFirstFunctions: SearchProperties, Auto, searchsorted_last, searchsorted_first
651+
using ForwardDiff
652+
653+
# `ForwardDiff.Dual <: Real`, so a Dual-valued knot vector — as produced
654+
# when an interpolation is reconstructed inside `ForwardDiff` and the
655+
# knots are promoted to Duals — dispatches to the `<:Real` probe. The
656+
# probes must not force a `Float64(::Dual)` conversion (which errors);
657+
# they must classify from the primal values and keep partials intact.
658+
ForwardDiff.derivative(2.0) do p
659+
t = collect(range(0.0, 1.0; length = 15)) .* p # linear Dual knots
660+
props = SearchProperties(t)
661+
@test props.has_props
662+
@test props.is_linear # linear in index
663+
@test props.is_uniform # evenly spaced
664+
# first_val / inv_step carry the derivative w.r.t. p.
665+
@test ForwardDiff.value(props.first_val) == 0.0
666+
# sum(t) = 7.5p, so d/dp = 7.5; exercise a value that depends on the
667+
# branch chosen from the probe result.
668+
props.is_linear ? sum(t) : 2 * sum(t)
669+
end == 7.5
670+
671+
# Non-uniform Dual knots must probe without error and report not-uniform.
672+
ForwardDiff.derivative(3.0) do p
673+
t = (Float64[0, 1, 2, 4, 8, 16, 20, 21, 22, 40, 41, 100]) .* p
674+
props = SearchProperties(t)
675+
@test props.has_props
676+
@test !props.is_uniform
677+
sum(t)
678+
end
679+
680+
# Full search path on uniform Dual knots exercises the closed-form
681+
# UniformStep kernel (`Auto` resolves to `KIND_UNIFORM_STEP`). It must
682+
# return the same index as `Base` on the primal values.
683+
ForwardDiff.derivative(2.0) do p
684+
t = collect(range(0.0, 10.0; length = 40)) .* p
685+
tv = map(ForwardDiff.value, t)
686+
auto = Auto(t)
687+
for q in (t[1], t[7], t[end], 5.0 * p, -1.0 * p, 11.0 * p)
688+
@test searchsorted_last(auto, t, q) == searchsortedlast(tv, ForwardDiff.value(q))
689+
@test searchsorted_first(auto, t, q) == searchsortedfirst(tv, ForwardDiff.value(q))
690+
end
691+
sum(t)
692+
end
693+
end
694+
648695
@safetestset "Auto{T} parametric + props-aware UniformStep kernel" begin
649696
using FindFirstFunctions
650697
using FindFirstFunctions:

0 commit comments

Comments
 (0)