From e572180e3d4c25e55be687a3bb81c9de5ad63e48 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 27 May 2026 13:37:47 -0400 Subject: [PATCH 1/3] docs: fix three CI failures (linkcheck, cross_references, example_block) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Documentation workflow on master was failing with three independent errors, all addressed here: 1. **linkcheck** — `https://iopscience.iop.org/article/10.1088/1757-899X/1276/1/012010/` returns a 302 to a PerimeterX bot validator from GitHub Actions runners. Added the URL to the existing `linkcheck_ignore` list in `docs/make.jl`. 2. **cross_references** — `docs/src/solvers/bracketing_solvers.md` referenced `[`modAB`](@ref)` but the exported binding (per commits `e856faf` / `1da0421`) is `ModAB` (uppercase). Fixed the casing. 3. **example_block** — the `@example ill_conditioned_nlprob` block at `docs/src/tutorials/large_systems.md:322-337` tripped `NoFunctionWrapperFoundError` (`No matching function wrapper was found!`) when running `solve(prob_brusselator_2d_approx_di, NewtonRaphson())` against a `NonlinearFunction` whose `sparsity` was `DifferentiationInterface.DenseSparsityDetector(AutoForwardDiff(); atol=1e-4)`. Root cause: `maybe_wrap_nonlinear_f` wraps IIP problem functions with a `FunctionWrappersWrapper` whose signatures are keyed off `ForwardDiff.Dual{Tag{NonlinearSolveTag, Float64}, ...}`. `DenseSparsityDetector{AutoForwardDiff}` runs the wrapped function with ForwardDiff duals whose tag is generated by DI (`Tag{DifferentiationInterface.FixTail{NonlinearFunction{...}}, Float64}`), not `NonlinearSolveTag`. Those duals are *isbits*, so FWW's `AllowNonIsBits` fallback does not kick in, and the dispatch trips `NoFunctionWrapperFoundError`. (Tracer-based detectors happen to work because Tracer types are non-isbits and hit the `AllowNonIsBits` fallback.) Fix: extend the same "skip wrapping in incompatible contexts" pattern introduced by #940 (Enzyme reverse-mode) to the sparsity-detector path. In `maybe_wrap_nonlinear_f`, when `prob.f.sparsity` is a non-`NoSparsityDetector` `AbstractSparsityDetector`, return the raw function unwrapped. The autospecialize gain is also less significant on the sparse path (per-color Jacobian columns are small), so this is the right tradeoff. Test added in `lib/NonlinearSolveBase/test/runtests.jl` asserting that `maybe_wrap_nonlinear_f` wraps under `NoSparsityDetector` (the default) and skips when an `AbstractSparsityDetector` is supplied. Verified locally (Julia 1.12.6): - `Pkg.test("NonlinearSolveBase")`: 40/40 pass (+3 new test cases). - Standalone repro of the docs example_block: both the TracerSparsityDetector and DenseSparsityDetector+AutoForwardDiff variants now `solve` successfully (`retcode = Success`). Without the fix the DenseSparsityDetector variant trips `NoFunctionWrapperFoundError`. - Full docs build with `julia +1.12 --project=docs -e 'include("docs/make.jl")'` (with `linkcheck = false` locally to skip the slow linkcheck pass) runs end-to-end through `Populate: populating indices.` / `RenderDocument: rendering document.` / `HTMLWriter: rendering HTML pages.` with no `Cannot resolve @ref`, no `failed to run @example block`, and no `makedocs encountered errors`. The `linkcheck = false` override is local-only and is not in the committed diff. Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Chris Rackauckas --- docs/make.jl | 1 + docs/src/solvers/bracketing_solvers.md | 2 +- lib/NonlinearSolveBase/src/autospecialize.jl | 13 ++++++ lib/NonlinearSolveBase/test/runtests.jl | 42 ++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index c36f9eade1..a20a11a4d1 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -52,6 +52,7 @@ makedocs(; "https://github.com/devernay/cminpack/blob/d1f5f5a273862ca1bbcf58394e4ac060d9e22c76/hybrj.c", "https://github.com/devernay/cminpack/blob/d1f5f5a273862ca1bbcf58394e4ac060d9e22c76/lmder.c", "https://net-informations.com/faq/net/stack-heap.htm", # Unreliable external site + "https://iopscience.iop.org/article/10.1088/1757-899X/1276/1/012010/", # IOP redirects to PerimeterX bot validator from CI ], checkdocs = :exports, warnonly = [:missing_docs], diff --git a/docs/src/solvers/bracketing_solvers.md b/docs/src/solvers/bracketing_solvers.md index 42fc5f5570..b41d52fec7 100644 --- a/docs/src/solvers/bracketing_solvers.md +++ b/docs/src/solvers/bracketing_solvers.md @@ -8,7 +8,7 @@ Solves for ``f(t) = 0`` in the problem defined by `prob` using the algorithm `al algorithm is given, a default algorithm will be chosen. ## Recommended Methods -[`modAB`](@ref) (Modified Anderson-Bjork) is the recommended method for the scalar interval +[`ModAB`](@ref) (Modified Anderson-Bjork) is the recommended method for the scalar interval root-finding problems. It combines Bisection with Anderson-Bjork steps to achieve superlinear convergence 1.7–1.8, providing optimal convergence rate for poorly behaved functions. According to our benchmarks, it outperforms the other methods in most cases. diff --git a/lib/NonlinearSolveBase/src/autospecialize.jl b/lib/NonlinearSolveBase/src/autospecialize.jl index 2e0de11eb5..ab416f5334 100644 --- a/lib/NonlinearSolveBase/src/autospecialize.jl +++ b/lib/NonlinearSolveBase/src/autospecialize.jl @@ -157,6 +157,19 @@ function maybe_wrap_nonlinear_f(prob::AbstractNonlinearProblem) # FullSpecialize opts out of wrapping, keeping the exact function type. SciMLBase.specialization(prob.f) === SciMLBase.AutoSpecialize || return prob.f.f + # Skip wrapping when the user supplied a sparsity detector. Detectors call + # the user function with foreign eltypes — either non-isbits types (Tracer- + # based detectors, handled by FWW's `AllowNonIsBits` fallback) or *isbits* + # types whose tag does not match the wrapper signatures (`DenseSparsityDetector` + # backed by `AutoForwardDiff` uses a `DI.FixTail`-tagged Dual). The latter + # bypasses `AllowNonIsBits` and trips `NoFunctionWrapperFoundError`. The + # autospecialize gain is also less significant on the sparse path (per-color + # Jacobian columns are small), so unwrapping is the right tradeoff. + sp = prob.f.sparsity + if sp isa ADTypes.AbstractSparsityDetector && !(sp isa NoSparsityDetector) + return prob.f.f + end + orig = prob.f.f inputs = (u0, u0, p) return AutoSpecializeCallable(wrapfun_iip(orig, inputs), orig) diff --git a/lib/NonlinearSolveBase/test/runtests.jl b/lib/NonlinearSolveBase/test/runtests.jl index da7afe6ec4..4334c06890 100644 --- a/lib/NonlinearSolveBase/test/runtests.jl +++ b/lib/NonlinearSolveBase/test/runtests.jl @@ -1,7 +1,13 @@ using InteractiveUtils, Test +import ADTypes @info sprint(InteractiveUtils.versioninfo) +# Used by the "maybe_wrap_nonlinear_f skips when a sparsity detector is supplied" +# testset below. Top-level so the struct definition is not nested inside +# `@testset`, which is unreliable across Julia versions. +struct _TestSparsityDetector <: ADTypes.AbstractSparsityDetector end + # Changing any code here triggers all the other tests to be run. So we intentionally # keep the tests here minimal. @testset "NonlinearSolveBase.jl" begin @@ -131,6 +137,42 @@ using InteractiveUtils, Test ) end + @testset "maybe_wrap_nonlinear_f skips when a sparsity detector is supplied" begin + # Regression for the docs-build failure on Brusselator with + # `sparsity = DifferentiationInterface.DenseSparsityDetector(AutoForwardDiff(); ...)`. + # `DenseSparsityDetector{AutoForwardDiff}` runs the user function with + # ForwardDiff duals whose tag is generated by DI (`FixTail{...}`), not + # `NonlinearSolveTag`. The wrapper's signatures are keyed off + # `NonlinearSolveTag` and the foreign-tagged Dual is *isbits*, so + # `AllowNonIsBits` does not fall back and the wrapper trips + # `NoFunctionWrapperFoundError`. The fix is to leave the function + # unwrapped whenever the user provides an `AbstractSparsityDetector`, + # since sparsity-detector pre-passes intrinsically call the function + # with eltypes the wrapper cannot cover. + using NonlinearSolveBase, SciMLBase, ADTypes + + resid!(du, u, p) = (du .= u; nothing) + + # `NoSparsityDetector` (the default) must NOT inhibit wrapping. + f_none = NonlinearFunction{true, SciMLBase.AutoSpecialize}( + resid!; sparsity = ADTypes.NoSparsityDetector() + ) + prob_none = NonlinearProblem(f_none, [1.0, 2.0], [0.5, 0.25]) + @test NonlinearSolveBase.is_fw_wrapped( + NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_none) + ) + + # A user-provided `AbstractSparsityDetector` must inhibit wrapping. + f_sp = NonlinearFunction{true, SciMLBase.AutoSpecialize}( + resid!; sparsity = _TestSparsityDetector() + ) + prob_sp = NonlinearProblem(f_sp, [1.0, 2.0], [0.5, 0.25]) + @test NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_sp) === f_sp.f + @test !NonlinearSolveBase.is_fw_wrapped( + NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_sp) + ) + end + @testset "EnzymeExt _accum_tangent! caches accumulation (#935)" begin include("enzyme_accum_tangent.jl") end From fc732eda194c6316758dcab7f0e285ac2d4bdb9e Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 27 May 2026 16:05:45 -0400 Subject: [PATCH 2/3] maybe_wrap_nonlinear_f: narrow skip to DenseSparsityDetector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior version skipped FunctionWrappers wrapping for *any* `AbstractSparsityDetector` other than `NoSparsityDetector`. This is overbroad: Tracer/Symbolics-style detectors emit non-isbits eltypes (`Tracer`, `Num`, …) which `FunctionWrappersWrappers`' `AllowNonIsBits` fallback already handles correctly — wrapping works for them. Only `DI.DenseSparsityDetector` is the actual failure mode, because it runs the user function with isbits ForwardDiff duals carrying DI's own `FixTail`-tagged Tag, for which no wrapper signature is pre-built and which bypasses `AllowNonIsBits`. Narrow the predicate to `prob.f.sparsity isa DI.DenseSparsityDetector` so Tracer/Symbolics paths keep the AutoSpecialize precompile benefit. The test now exercises three cases: default (wraps), `DenseSparsityDetector` (does not wrap — the regression), and an `AbstractSparsityDetector` proxy for Tracer/Symbolics (wraps). Locally verified that `NLS.solve(prob_brusselator_2d_approx_di, NewtonRaphson())` from docs/src/tutorials/large_systems.md returns `ReturnCode.Success` and that the new testset passes 4/4 on Julia 1.12.6. Co-Authored-By: Chris Rackauckas --- lib/NonlinearSolveBase/src/autospecialize.jl | 20 +++----- lib/NonlinearSolveBase/test/runtests.jl | 52 +++++++++++--------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/lib/NonlinearSolveBase/src/autospecialize.jl b/lib/NonlinearSolveBase/src/autospecialize.jl index ab416f5334..a9c1ced094 100644 --- a/lib/NonlinearSolveBase/src/autospecialize.jl +++ b/lib/NonlinearSolveBase/src/autospecialize.jl @@ -157,18 +157,14 @@ function maybe_wrap_nonlinear_f(prob::AbstractNonlinearProblem) # FullSpecialize opts out of wrapping, keeping the exact function type. SciMLBase.specialization(prob.f) === SciMLBase.AutoSpecialize || return prob.f.f - # Skip wrapping when the user supplied a sparsity detector. Detectors call - # the user function with foreign eltypes — either non-isbits types (Tracer- - # based detectors, handled by FWW's `AllowNonIsBits` fallback) or *isbits* - # types whose tag does not match the wrapper signatures (`DenseSparsityDetector` - # backed by `AutoForwardDiff` uses a `DI.FixTail`-tagged Dual). The latter - # bypasses `AllowNonIsBits` and trips `NoFunctionWrapperFoundError`. The - # autospecialize gain is also less significant on the sparse path (per-color - # Jacobian columns are small), so unwrapping is the right tradeoff. - sp = prob.f.sparsity - if sp isa ADTypes.AbstractSparsityDetector && !(sp isa NoSparsityDetector) - return prob.f.f - end + # Skip wrapping for `DI.DenseSparsityDetector`, which runs the user function + # with ForwardDiff duals carrying DI's own `FixTail`-tagged Tag. Those duals + # are isbits, so `FunctionWrappersWrappers`' `AllowNonIsBits` fallback does + # not fire and the call trips `NoFunctionWrapperFoundError` because no + # wrapper signature was pre-built for the foreign tag. Other detectors + # (e.g. `TracerSparsityDetector`, Symbolics-based detectors) emit non-isbits + # eltypes and are handled by the `AllowNonIsBits` fallback — keep wrapping. + prob.f.sparsity isa DI.DenseSparsityDetector && return prob.f.f orig = prob.f.f inputs = (u0, u0, p) diff --git a/lib/NonlinearSolveBase/test/runtests.jl b/lib/NonlinearSolveBase/test/runtests.jl index 4334c06890..49231e1735 100644 --- a/lib/NonlinearSolveBase/test/runtests.jl +++ b/lib/NonlinearSolveBase/test/runtests.jl @@ -3,10 +3,9 @@ import ADTypes @info sprint(InteractiveUtils.versioninfo) -# Used by the "maybe_wrap_nonlinear_f skips when a sparsity detector is supplied" -# testset below. Top-level so the struct definition is not nested inside -# `@testset`, which is unreliable across Julia versions. -struct _TestSparsityDetector <: ADTypes.AbstractSparsityDetector end +# Proxy for Tracer/Symbolics-style detectors. Defined at top level so the +# `struct` definition isn't nested inside `@testset`. +struct _NonDenseSparsityDetector <: ADTypes.AbstractSparsityDetector end # Changing any code here triggers all the other tests to be run. So we intentionally # keep the tests here minimal. @@ -137,39 +136,44 @@ struct _TestSparsityDetector <: ADTypes.AbstractSparsityDetector end ) end - @testset "maybe_wrap_nonlinear_f skips when a sparsity detector is supplied" begin + @testset "maybe_wrap_nonlinear_f and sparsity detectors" begin # Regression for the docs-build failure on Brusselator with # `sparsity = DifferentiationInterface.DenseSparsityDetector(AutoForwardDiff(); ...)`. # `DenseSparsityDetector{AutoForwardDiff}` runs the user function with - # ForwardDiff duals whose tag is generated by DI (`FixTail{...}`), not - # `NonlinearSolveTag`. The wrapper's signatures are keyed off - # `NonlinearSolveTag` and the foreign-tagged Dual is *isbits*, so - # `AllowNonIsBits` does not fall back and the wrapper trips - # `NoFunctionWrapperFoundError`. The fix is to leave the function - # unwrapped whenever the user provides an `AbstractSparsityDetector`, - # since sparsity-detector pre-passes intrinsically call the function - # with eltypes the wrapper cannot cover. - using NonlinearSolveBase, SciMLBase, ADTypes + # ForwardDiff duals tagged by DI (`FixTail{...}`), not `NonlinearSolveTag`. + # Those duals are isbits, so FunctionWrappersWrappers' `AllowNonIsBits` + # fallback does not fire and the wrapper trips `NoFunctionWrapperFoundError`. + # We skip wrapping *only* for `DenseSparsityDetector`. Tracer-style + # detectors emit non-isbits eltypes and are handled by `AllowNonIsBits`, + # so they must still wrap. + using NonlinearSolveBase, SciMLBase, ADTypes, DifferentiationInterface resid!(du, u, p) = (du .= u; nothing) - # `NoSparsityDetector` (the default) must NOT inhibit wrapping. - f_none = NonlinearFunction{true, SciMLBase.AutoSpecialize}( - resid!; sparsity = ADTypes.NoSparsityDetector() - ) + # Default `NoSparsityDetector` → wraps. + f_none = NonlinearFunction{true, SciMLBase.AutoSpecialize}(resid!) prob_none = NonlinearProblem(f_none, [1.0, 2.0], [0.5, 0.25]) @test NonlinearSolveBase.is_fw_wrapped( NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_none) ) - # A user-provided `AbstractSparsityDetector` must inhibit wrapping. - f_sp = NonlinearFunction{true, SciMLBase.AutoSpecialize}( - resid!; sparsity = _TestSparsityDetector() + # `DenseSparsityDetector` → must NOT wrap (the failure mode). + f_dense = NonlinearFunction{true, SciMLBase.AutoSpecialize}( + resid!; sparsity = DenseSparsityDetector(AutoForwardDiff(); atol = 1.0e-6) ) - prob_sp = NonlinearProblem(f_sp, [1.0, 2.0], [0.5, 0.25]) - @test NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_sp) === f_sp.f + prob_dense = NonlinearProblem(f_dense, [1.0, 2.0], [0.5, 0.25]) + @test NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_dense) === f_dense.f @test !NonlinearSolveBase.is_fw_wrapped( - NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_sp) + NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_dense) + ) + + # Other `AbstractSparsityDetector`s (proxy for Tracer/Symbolics) → wrap. + f_other = NonlinearFunction{true, SciMLBase.AutoSpecialize}( + resid!; sparsity = _NonDenseSparsityDetector() + ) + prob_other = NonlinearProblem(f_other, [1.0, 2.0], [0.5, 0.25]) + @test NonlinearSolveBase.is_fw_wrapped( + NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_other) ) end From 518c36eae3d0379e98f35a79bfa12ef5d84818cd Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 28 May 2026 13:13:16 -0400 Subject: [PATCH 3/3] Revert maybe_wrap_nonlinear_f change; scope PR to docs-only fixes The FunctionWrappers/ForwardDiff `NoFunctionWrapperFoundError` triggered by `DenseSparsityDetector(AutoForwardDiff())` is a separate, deeper issue that shouldn't be addressed in a docs-CI PR. Restore autospecialize.jl and its test to master; this PR now only carries the linkcheck and cross-reference fixes. Co-Authored-By: Chris Rackauckas --- lib/NonlinearSolveBase/src/autospecialize.jl | 9 ---- lib/NonlinearSolveBase/test/runtests.jl | 46 -------------------- 2 files changed, 55 deletions(-) diff --git a/lib/NonlinearSolveBase/src/autospecialize.jl b/lib/NonlinearSolveBase/src/autospecialize.jl index a9c1ced094..2e0de11eb5 100644 --- a/lib/NonlinearSolveBase/src/autospecialize.jl +++ b/lib/NonlinearSolveBase/src/autospecialize.jl @@ -157,15 +157,6 @@ function maybe_wrap_nonlinear_f(prob::AbstractNonlinearProblem) # FullSpecialize opts out of wrapping, keeping the exact function type. SciMLBase.specialization(prob.f) === SciMLBase.AutoSpecialize || return prob.f.f - # Skip wrapping for `DI.DenseSparsityDetector`, which runs the user function - # with ForwardDiff duals carrying DI's own `FixTail`-tagged Tag. Those duals - # are isbits, so `FunctionWrappersWrappers`' `AllowNonIsBits` fallback does - # not fire and the call trips `NoFunctionWrapperFoundError` because no - # wrapper signature was pre-built for the foreign tag. Other detectors - # (e.g. `TracerSparsityDetector`, Symbolics-based detectors) emit non-isbits - # eltypes and are handled by the `AllowNonIsBits` fallback — keep wrapping. - prob.f.sparsity isa DI.DenseSparsityDetector && return prob.f.f - orig = prob.f.f inputs = (u0, u0, p) return AutoSpecializeCallable(wrapfun_iip(orig, inputs), orig) diff --git a/lib/NonlinearSolveBase/test/runtests.jl b/lib/NonlinearSolveBase/test/runtests.jl index 49231e1735..da7afe6ec4 100644 --- a/lib/NonlinearSolveBase/test/runtests.jl +++ b/lib/NonlinearSolveBase/test/runtests.jl @@ -1,12 +1,7 @@ using InteractiveUtils, Test -import ADTypes @info sprint(InteractiveUtils.versioninfo) -# Proxy for Tracer/Symbolics-style detectors. Defined at top level so the -# `struct` definition isn't nested inside `@testset`. -struct _NonDenseSparsityDetector <: ADTypes.AbstractSparsityDetector end - # Changing any code here triggers all the other tests to be run. So we intentionally # keep the tests here minimal. @testset "NonlinearSolveBase.jl" begin @@ -136,47 +131,6 @@ struct _NonDenseSparsityDetector <: ADTypes.AbstractSparsityDetector end ) end - @testset "maybe_wrap_nonlinear_f and sparsity detectors" begin - # Regression for the docs-build failure on Brusselator with - # `sparsity = DifferentiationInterface.DenseSparsityDetector(AutoForwardDiff(); ...)`. - # `DenseSparsityDetector{AutoForwardDiff}` runs the user function with - # ForwardDiff duals tagged by DI (`FixTail{...}`), not `NonlinearSolveTag`. - # Those duals are isbits, so FunctionWrappersWrappers' `AllowNonIsBits` - # fallback does not fire and the wrapper trips `NoFunctionWrapperFoundError`. - # We skip wrapping *only* for `DenseSparsityDetector`. Tracer-style - # detectors emit non-isbits eltypes and are handled by `AllowNonIsBits`, - # so they must still wrap. - using NonlinearSolveBase, SciMLBase, ADTypes, DifferentiationInterface - - resid!(du, u, p) = (du .= u; nothing) - - # Default `NoSparsityDetector` → wraps. - f_none = NonlinearFunction{true, SciMLBase.AutoSpecialize}(resid!) - prob_none = NonlinearProblem(f_none, [1.0, 2.0], [0.5, 0.25]) - @test NonlinearSolveBase.is_fw_wrapped( - NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_none) - ) - - # `DenseSparsityDetector` → must NOT wrap (the failure mode). - f_dense = NonlinearFunction{true, SciMLBase.AutoSpecialize}( - resid!; sparsity = DenseSparsityDetector(AutoForwardDiff(); atol = 1.0e-6) - ) - prob_dense = NonlinearProblem(f_dense, [1.0, 2.0], [0.5, 0.25]) - @test NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_dense) === f_dense.f - @test !NonlinearSolveBase.is_fw_wrapped( - NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_dense) - ) - - # Other `AbstractSparsityDetector`s (proxy for Tracer/Symbolics) → wrap. - f_other = NonlinearFunction{true, SciMLBase.AutoSpecialize}( - resid!; sparsity = _NonDenseSparsityDetector() - ) - prob_other = NonlinearProblem(f_other, [1.0, 2.0], [0.5, 0.25]) - @test NonlinearSolveBase.is_fw_wrapped( - NonlinearSolveBase.maybe_wrap_nonlinear_f(prob_other) - ) - end - @testset "EnzymeExt _accum_tangent! caches accumulation (#935)" begin include("enzyme_accum_tangent.jl") end