From 53294b66d446f4148eb6a51f399a4ea813e2b774 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 26 Jul 2026 19:19:12 -0400 Subject: [PATCH 1/6] Require strict SciMLTesting 2.4 QA Co-Authored-By: Chris Rackauckas --- Project.toml | 10 +- docs/make.jl | 3 +- docs/src/developer_api.md | 20 ++++ src/ComplementaritySolve.jl | 16 +++- src/algorithms/generic.jl | 2 +- src/algorithms/lcs/naive_lcs.jl | 2 +- src/algorithms/mcp/pathsolver.jl | 5 +- src/algorithms/solve.jl | 91 ++++++++++++++++++- src/problems/complementarity_problems.jl | 24 ++++- src/problems/complementarity_systems.jl | 8 +- src/sensitivity/mcp.jl | 6 +- src/utils.jl | 2 +- test/Project.toml | 6 ++ .../cpu/control_learning/cartpole.jl | 1 + .../partial_state_feedback.jl | 1 + .../control_learning/soft_joint_acrobot.jl | 1 + test/core/cpu/lcp.jl | 1 + test/core/cpu/mcp.jl | 1 + test/core/cuda/lcp.jl | 1 + test/core/cuda/mcp.jl | 1 + test/interfaces.jl | 91 +++++++++++++++++++ test/qa.jl | 26 +----- test/runtests.jl | 3 + 23 files changed, 277 insertions(+), 45 deletions(-) create mode 100644 docs/src/developer_api.md create mode 100644 test/interfaces.jl diff --git a/Project.toml b/Project.toml index e86a473..b94cc6b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,13 +1,13 @@ name = "ComplementaritySolve" uuid = "b40a91a3-bdaf-4e1c-b965-8c278a33a8d3" authors = ["Avik Pal "] -version = "1.0.0" +version = "2.0.0" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471" -FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" @@ -18,6 +18,7 @@ PATHSolver = "f5f7c340-0bb3-5c69-969a-41884d311d1b" Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" @@ -26,9 +27,10 @@ Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" ChainRulesCore = "1" CommonSolve = "0.2.6" ConcreteStructs = "0.2" -FillArrays = "1.13" +DifferentiationInterface = "0.6, 0.7" ForwardDiff = "1" GPUArraysCore = "0.2" +LinearAlgebra = "1.10" LinearSolve = "3.61, 4.2, 5.0" NNlib = "0.9.34" NonlinearSolve = "4.12" @@ -36,6 +38,8 @@ PATHSolver = "1.4" Polyester = "0.7.18" PrecompileTools = "1.2.1" SciMLBase = "2.147, 3.1" +SciMLPublic = "1" SimpleNonlinearSolve = "2.7" +SparseArrays = "1.10" Zygote = "0.7.5" julia = "1.10" diff --git a/docs/make.jl b/docs/make.jl index f548e02..45bbc9b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -3,7 +3,7 @@ using Documenter makedocs(; modules = [ComplementaritySolve], - checkdocs = :exports, + checkdocs = :public, sitename = "ComplementaritySolve.jl", format = Documenter.HTML(; prettyurls = get(ENV, "CI", "false") == "true", @@ -12,6 +12,7 @@ makedocs(; ), pages = [ "Home" => "index.md", + "Developer API" => "developer_api.md", ], ) diff --git a/docs/src/developer_api.md b/docs/src/developer_api.md new file mode 100644 index 0000000..ec3611d --- /dev/null +++ b/docs/src/developer_api.md @@ -0,0 +1,20 @@ +# Developer API + +The qualified APIs on this page are intended for packages implementing +ComplementaritySolve-compatible problem containers, algorithms, or sensitivity rules. +Application code should use the exported problem constructors and algorithms instead. + +```@docs +ComplementaritySolve.AbstractComplementarityProblem +ComplementaritySolve.AbstractLinearComplementarityProblem +ComplementaritySolve.AbstractNonlinearComplementarityProblem +ComplementaritySolve.AbstractComplementaritySystem +ComplementaritySolve.AbstractComplementarityAlgorithm +ComplementaritySolve.AbstractComplementaritySystemAlgorithm +ComplementaritySolve.AbstractComplementaritySensitivityAlgorithm +ComplementaritySolve.AbstractComplementaritySolution +ComplementaritySolve.AbstractLinearComplementaritySolution +ComplementaritySolve.isbatched +ComplementaritySolve.__solve +ComplementaritySolve.__solve_adjoint +``` diff --git a/src/ComplementaritySolve.jl b/src/ComplementaritySolve.jl index 6b37172..3cea30b 100644 --- a/src/ComplementaritySolve.jl +++ b/src/ComplementaritySolve.jl @@ -10,8 +10,11 @@ using GPUArraysCore: GPUArraysCore using SciMLBase: SciMLBase, FunctionOperator, LinearProblem, NonlinearFunction, NonlinearProblem, ODEFunction, ODEProblem, ReturnCode, SteadyStateProblem, isinplace +using SciMLPublic: @public using CommonSolve: CommonSolve using ConcreteStructs: ConcreteStructs, @concrete +using DifferentiationInterface: DifferentiationInterface, AutoForwardDiff, AutoZygote, + jacobian ## Stdlibs using LinearAlgebra: LinearAlgebra, Diagonal, I, diagind, mul!, norm, pinv, \, / using SparseArrays: SparseArrays @@ -31,7 +34,6 @@ using Polyester: Polyester, @batch import CommonSolve: init, solve, solve! import ChainRulesCore as CRC -import FillArrays: AbstractFill const ∂0 = ZeroTangent() const ∂∅ = NoTangent() @@ -54,7 +56,7 @@ intended to be passed as the second argument to `solve(prob, alg; kwargs...)` fo # Interface -- Implement an internal `__solve(prob, alg, args...; kwargs...)` method for each +- Implement `ComplementaritySolve.__solve(prob, alg, args...; kwargs...)` for each supported problem family. - Return an `AbstractComplementaritySolution` subtype whose `prob` and `alg` fields reference the original problem and algorithm. @@ -73,7 +75,7 @@ Developer interface for algorithms that solve complementarity systems. # Interface -- Implement `solve(prob::AbstractComplementaritySystem, alg; kwargs...)`. +- Implement `ComplementaritySolve.__solve(prob, alg; kwargs...)`. - Return the solution object produced by the wrapped ODE or steady-state solve. - Forward relevant solver keywords to the continuous dynamics solve and the embedded complementarity solve. @@ -127,7 +129,13 @@ export PATHSolverAlgorithm export NaiveLCSAlgorithm export LinearComplementarityAdjoint, MixedComplementarityAdjoint export LinearComplementaritySolution, MixedComplementaritySolution -export solve + +@public AbstractComplementarityAlgorithm, AbstractComplementaritySystemAlgorithm +@public AbstractComplementaritySensitivityAlgorithm +@public AbstractComplementarityProblem, AbstractLinearComplementarityProblem +@public AbstractNonlinearComplementarityProblem, AbstractComplementaritySystem +@public AbstractComplementaritySolution, AbstractLinearComplementaritySolution +@public isbatched, __solve, __solve_adjoint include("precompilation.jl") diff --git a/src/algorithms/generic.jl b/src/algorithms/generic.jl index 32b45a9..364cc1b 100644 --- a/src/algorithms/generic.jl +++ b/src/algorithms/generic.jl @@ -19,7 +19,7 @@ nonlinear residual and delegating to a NonlinearSolve.jl-compatible solver. using ComplementaritySolve prob = LinearComplementarityProblem([2.0 -1.0; -1.0 2.0], [-1.0, -1.0]) -sol = solve(prob, NonlinearReformulation(:smooth)) +sol = ComplementaritySolve.solve(prob, NonlinearReformulation(:smooth)) ``` """ @concrete struct NonlinearReformulation{method} <: AbstractComplementarityAlgorithm diff --git a/src/algorithms/lcs/naive_lcs.jl b/src/algorithms/lcs/naive_lcs.jl index 168bf9b..a1b4397 100644 --- a/src/algorithms/lcs/naive_lcs.jl +++ b/src/algorithms/lcs/naive_lcs.jl @@ -26,7 +26,7 @@ right-hand-side evaluation. lcp_solver end -function solve( +function __solve( prob::LinearComplementaritySystem{sstate}, alg::NaiveLCSAlgorithm; ode_kwargs = (;), lcp_kwargs = (;), kwargs... ) where {sstate} diff --git a/src/algorithms/mcp/pathsolver.jl b/src/algorithms/mcp/pathsolver.jl index f542542..f1d332d 100644 --- a/src/algorithms/mcp/pathsolver.jl +++ b/src/algorithms/mcp/pathsolver.jl @@ -46,9 +46,10 @@ function __solve( function J!(n, nnz, z, col, len, row, data) if !iip - J = (n ≤ 100 ? ForwardDiff.jacobian : Zygote.jacobian)(fₚ, z) + backend = n ≤ 100 ? AutoForwardDiff() : AutoZygote() + J = jacobian(fₚ, backend, z) else - J = ForwardDiff.jacobian(fₚ, similar(z, n), z) + J = jacobian(fₚ, similar(z, n), AutoForwardDiff(), z) end i = 1 for c in 1:n diff --git a/src/algorithms/solve.jl b/src/algorithms/solve.jl index b24f106..b6992e2 100644 --- a/src/algorithms/solve.jl +++ b/src/algorithms/solve.jl @@ -21,6 +21,36 @@ function solve( return __solve(prob, sensealg, solver, u0, M, q, args_...; kwargs...) end +""" + solve(prob::AbstractComplementaritySystem, alg::AbstractComplementaritySystemAlgorithm; kwargs...) + +Solve a complementarity system with a compatible system algorithm. + +# Arguments + +- `prob`: A complementarity system implementing the + `AbstractComplementaritySystem` field contract. +- `alg`: A compatible `AbstractComplementaritySystemAlgorithm` implementation. + +# Keyword Arguments + +- `kwargs...`: Solver-specific keyword arguments forwarded unchanged to the + developer `__solve` implementation. + +# Interface + +Packages extending this interface must implement +`ComplementaritySolve.__solve(prob, alg; kwargs...)`. The generic `solve` function +does not inspect concrete system fields, so the extension owns validation and the +returned continuous-solver solution type. +""" +function solve( + prob::AbstractComplementaritySystem, alg::AbstractComplementaritySystemAlgorithm; + kwargs... + ) + return __solve(prob, alg; kwargs...) +end + function __solver_and_args(prob, args...) return length(args) == 0 ? (__default_solver(prob), ()) : (first(args), args[2:end]) end @@ -40,8 +70,67 @@ end ## and works well with inplace/out of place and also works OOTB with GPUs __default_solver(::Union{LCP, MCP}) = NonlinearReformulation(:smooth, DEFAULT_NLSOLVER) -# Algorithms should dispatch on __solve +""" + __solve(prob, alg, args...; kwargs...) + +Developer extension hook implementing a complementarity solve. + +# Arguments + +- `prob`: A subtype of `AbstractComplementarityProblem` or + `AbstractComplementaritySystem`. +- `alg`: A compatible developer algorithm subtype. +- `args...`: Normalized solve data forwarded by `solve`; problem algorithms receive + the effective initial state and problem data, while system algorithms receive no + additional positional data. + +# Keyword Arguments + +- `kwargs...`: Algorithm-specific solve options forwarded from `solve`. + +# Returns + +- A solution satisfying the documented solution contract for the problem family, or + the underlying continuous-solver solution for complementarity systems. + +# Interface + +This qualified, non-exported name is the stable extension point for solver packages. +Implementations must not mutate stored problem data when processing per-call +overrides, and must preserve the original `prob` and `alg` in complementarity solution +objects. Applications should call `solve`, not `__solve`, directly. +""" function __solve end + +""" + __solve_adjoint(prob, sensealg, sol, Δsol, args...; kwargs...) + +Developer extension hook for the reverse rule of a complementarity solve. + +# Arguments + +- `prob`: The original complementarity problem. +- `sensealg`: An `AbstractComplementaritySensitivityAlgorithm` compatible with + `prob` and `sol`. +- `sol`: The primal complementarity solution. +- `Δsol`: The incoming tangent for `sol`. +- `args...`: The normalized primal solve data passed to `__solve`. + +# Keyword Arguments + +- `kwargs...`: The solve keyword arguments supplied to the primal solve. + +# Returns + +- Tangents in the order of the differentiable primal solve data. + +# Interface + +Implementations must return `NoTangent` or `ZeroTangent` for nondifferentiable data +and shape-compatible tangents for differentiable data. This hook is called by the +`ChainRulesCore.rrule` for `__solve`; applications should select a concrete sensitivity +algorithm through `solve(...; sensealg = ...)` instead of invoking it directly. +""" function __solve_adjoint end function __solve( diff --git a/src/problems/complementarity_problems.jl b/src/problems/complementarity_problems.jl index bdac0f3..b26c758 100644 --- a/src/problems/complementarity_problems.jl +++ b/src/problems/complementarity_problems.jl @@ -61,6 +61,28 @@ abstract type AbstractNonlinearComplementarityProblem{iip} <: AbstractComplementarityProblem{iip} end SciMLBase.isinplace(::AbstractComplementarityProblem{iip}) where {iip} = iip + +""" + isbatched(prob::AbstractLinearComplementarityProblem) -> Bool + +Return whether `prob` stores independent linear complementarity problems in a batch. + +# Arguments + +- `prob`: A linear complementarity problem subtype. + +# Returns + +- `true` when `M` has a batch dimension and the corresponding `q` and `u0` values + represent independent column-wise problems; otherwise `false`. + +# Interface + +The return value is determined by the second type parameter of +`AbstractLinearComplementarityProblem{iip, batched}`. Subtypes must use `true` only +when their documented storage satisfies the batched matrix/vector layout expected by +algorithms in this package. +""" isbatched(::AbstractLinearComplementarityProblem{I, B}) where {I, B} = B """ @@ -97,7 +119,7 @@ using ComplementaritySolve M = [2.0 -1.0; -1.0 2.0] q = [-1.0, -1.0] prob = LinearComplementarityProblem(M, q) -sol = solve(prob, PGS()) +sol = ComplementaritySolve.solve(prob, PGS()) ``` """ @concrete struct LinearComplementarityProblem{iip, batched} <: diff --git a/src/problems/complementarity_systems.jl b/src/problems/complementarity_systems.jl index 29bed02..2d7d5ec 100644 --- a/src/problems/complementarity_systems.jl +++ b/src/problems/complementarity_systems.jl @@ -4,8 +4,9 @@ Developer interface for complementarity system containers. Complementarity systems couple continuous dynamics with complementarity variables. -Generic system algorithms are expected to call `solve(prob, alg; kwargs...)` and use -the concrete problem fields documented by the system type. +External system algorithms implement the qualified developer hook +`ComplementaritySolve.__solve(prob, alg; kwargs...)`; users invoke the generic +`solve(prob, alg; kwargs...)` entry point. # Interface @@ -14,6 +15,9 @@ the concrete problem fields documented by the system type. - `prob.tspan` must contain the integration interval. - `prob.p` must contain parameters passed to the controller. - `prob.controller` must be callable as `controller(x, λ, p, t)`. +- A subtype and an `AbstractComplementaritySystemAlgorithm` subtype must have a + matching `__solve` method. That method must return the solution object produced by + the underlying continuous solver. """ abstract type AbstractComplementaritySystem{iip} end diff --git a/src/sensitivity/mcp.jl b/src/sensitivity/mcp.jl index 920d478..4c0377c 100644 --- a/src/sensitivity/mcp.jl +++ b/src/sensitivity/mcp.jl @@ -56,12 +56,12 @@ end A₂ = ∂ϕ₊∂v₊ * ∂ϕ₋∂u₋ + ∂ϕ₋∂v₋ if isinplace(prob) # Using ForwardDiff for now. We can potentially use Enzyme.jl here - J = ForwardDiff.jacobian((y, u) -> f(y, u, p), fᵤ, u) + J = jacobian((y, u) -> f(y, u, p), fᵤ, AutoForwardDiff(), u) A = J' * A₁ .+ A₂ else if length(u) ≤ 50 # Construct the Full Matrix - A = only(Zygote.jacobian(Base.Fix2(f, p), u))' * A₁ .+ A₂ + A = jacobian(Base.Fix2(f, p), AutoZygote(), u)' * A₁ .+ A₂ else # Use Matrix Free Methods ## NOTE: If we use SparseDiffTools here we will have to mess around with a wrapper @@ -79,7 +79,7 @@ end if isinplace(prob) # Using ForwardDiff for now. We can potentially use Enzyme.jl here - J = ForwardDiff.jacobian((y, p) -> f(y, u, p), fᵤ, p) + J = jacobian((y, p) -> f(y, u, p), fᵤ, AutoForwardDiff(), p) ∂p = -reshape((A₁ * λ)' * J, size(p)) else _, pb_f = Zygote.pullback(Base.Fix1(f, u), p) diff --git a/src/utils.jl b/src/utils.jl index 8aeb9b4..b38e3bb 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -35,7 +35,7 @@ __notangent(::ZeroTangent) = true __notangent(::NoTangent) = true __notangent(::Any) = false -__unfillarray(x::AbstractFill) = collect(x) +__unfillarray(x::AbstractArray) = ismutable(x) ? x : collect(x) __unfillarray(x) = x function batched_matvec(A::AA3, x::AM) diff --git a/test/Project.toml b/test/Project.toml index dcfc676..1c65226 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,7 +1,9 @@ [deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" @@ -14,6 +16,7 @@ OptimizationOptimisers = "42dfb2eb-d2b4-4451-abcd-913932933ac1" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" PATHSolver = "f5f7c340-0bb3-5c69-969a-41884d311d1b" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" @@ -25,9 +28,11 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [compat] +Aqua = "0.8" BenchmarkTools = "1" CUDA = "4, 5, 6.1" ChainRulesCore = "1" +CommonSolve = "0.2" ComponentArrays = "0.15" DiffEqBase = "6, 7.5" FiniteDifferences = "0.12" @@ -39,6 +44,7 @@ OptimizationOptimisers = "0.2, 0.3" OrdinaryDiffEq = "6, 7.0" PATHSolver = "1" SafeTestsets = "0.1" +SciMLBase = "2.147, 3.1" SciMLSensitivity = "7" SciMLTesting = "2.4" SimpleNonlinearSolve = "0.1, 1, 2" diff --git a/test/applications/cpu/control_learning/cartpole.jl b/test/applications/cpu/control_learning/cartpole.jl index 5cbb142..a4039e6 100644 --- a/test/applications/cpu/control_learning/cartpole.jl +++ b/test/applications/cpu/control_learning/cartpole.jl @@ -2,6 +2,7 @@ using Zygote, LinearAlgebra, SimpleNonlinearSolve, OrdinaryDiffEq, Optimization, OptimizationOptimisers, SciMLSensitivity, SteadyStateDiffEq, Test, ComponentArrays, StableRNGs using ComplementaritySolve +using CommonSolve: solve const g = 9.81; const mp = 0.1; diff --git a/test/applications/cpu/control_learning/partial_state_feedback.jl b/test/applications/cpu/control_learning/partial_state_feedback.jl index da660c5..1f75dec 100644 --- a/test/applications/cpu/control_learning/partial_state_feedback.jl +++ b/test/applications/cpu/control_learning/partial_state_feedback.jl @@ -2,6 +2,7 @@ using Zygote, LinearAlgebra, SimpleNonlinearSolve, OrdinaryDiffEq, Optimization, OptimizationOptimisers, SciMLSensitivity, Test, ComponentArrays, SparseArrays, StableRNGs using ComplementaritySolve +using CommonSolve: solve # parameters const m1 = 1.0; diff --git a/test/applications/cpu/control_learning/soft_joint_acrobot.jl b/test/applications/cpu/control_learning/soft_joint_acrobot.jl index 7813370..1a56cca 100644 --- a/test/applications/cpu/control_learning/soft_joint_acrobot.jl +++ b/test/applications/cpu/control_learning/soft_joint_acrobot.jl @@ -2,6 +2,7 @@ using ChainRulesCore, ComplementaritySolve, ComponentArrays, DiffEqBase, Forward LinearAlgebra, Optimization, OptimizationOptimisers, OrdinaryDiffEq, SciMLSensitivity, SimpleNonlinearSolve, SparseArrays, StableRNGs, Statistics, SteadyStateDiffEq, Test using Zygote +using CommonSolve: solve const m₁ = 0.5f0 const m₂ = 1.0f0 diff --git a/test/core/cpu/lcp.jl b/test/core/cpu/lcp.jl index 83e812c..b0fb966 100644 --- a/test/core/cpu/lcp.jl +++ b/test/core/cpu/lcp.jl @@ -1,4 +1,5 @@ using ComplementaritySolve, ComponentArrays, FiniteDifferences, ForwardDiff +using CommonSolve: solve using NonlinearSolve: Broyden using SimpleNonlinearSolve, StableRNGs, Test, Zygote diff --git a/test/core/cpu/mcp.jl b/test/core/cpu/mcp.jl index 56ff819..8c6c34f 100644 --- a/test/core/cpu/mcp.jl +++ b/test/core/cpu/mcp.jl @@ -1,4 +1,5 @@ using BenchmarkTools, ComplementaritySolve, ComponentArrays, FiniteDifferences +using CommonSolve: solve using ForwardDiff, NonlinearSolve, SimpleNonlinearSolve, StableRNGs, Test, Zygote rng = StableRNG(0) diff --git a/test/core/cuda/lcp.jl b/test/core/cuda/lcp.jl index 51bf4e7..095293b 100644 --- a/test/core/cuda/lcp.jl +++ b/test/core/cuda/lcp.jl @@ -1,4 +1,5 @@ using ComplementaritySolve, ComponentArrays, FiniteDifferences, ForwardDiff +using CommonSolve: solve using SimpleNonlinearSolve, StableRNGs, Test, Zygote using CUDA diff --git a/test/core/cuda/mcp.jl b/test/core/cuda/mcp.jl index 8163827..de8099d 100644 --- a/test/core/cuda/mcp.jl +++ b/test/core/cuda/mcp.jl @@ -1,4 +1,5 @@ using BenchmarkTools, ComplementaritySolve, ComponentArrays, FiniteDifferences +using CommonSolve: solve using ForwardDiff, NonlinearSolve, SimpleNonlinearSolve, StableRNGs, Test, Zygote using CUDA diff --git a/test/interfaces.jl b/test/interfaces.jl new file mode 100644 index 0000000..4980464 --- /dev/null +++ b/test/interfaces.jl @@ -0,0 +1,91 @@ +using ChainRulesCore, ComplementaritySolve, SciMLBase, Test + +struct ExternalLinearProblem{M, Q, U} <: + ComplementaritySolve.AbstractLinearComplementarityProblem{true, false} + M::M + q::Q + u0::U +end + +struct ExternalNonlinearProblem{F, U, P} <: + ComplementaritySolve.AbstractNonlinearComplementarityProblem{false} + f::F + u0::U + p::P +end + +struct ExternalSystem <: ComplementaritySolve.AbstractComplementaritySystem{false} + x0::Vector{Float64} + λ0::Vector{Float64} + tspan::Tuple{Float64, Float64} + p::Nothing + controller::Function +end + +struct ExternalProblemAlgorithm <: ComplementaritySolve.AbstractComplementarityAlgorithm end +struct ExternalSystemAlgorithm <: ComplementaritySolve.AbstractComplementaritySystemAlgorithm end +struct ExternalAdjoint <: ComplementaritySolve.AbstractComplementaritySensitivityAlgorithm end + +struct ExternalSolution <: ComplementaritySolve.AbstractLinearComplementaritySolution + u::Vector{Float64} + residual::Float64 + prob + alg + retcode::SciMLBase.ReturnCode.T +end + +function ComplementaritySolve.__solve( + prob::ExternalLinearProblem, alg::ExternalProblemAlgorithm, u0, M, q; kwargs... + ) + return ExternalSolution(u0, sum(abs2, M * u0 + q), prob, alg, SciMLBase.ReturnCode.Success) +end + +function ComplementaritySolve.__solve( + prob::ExternalNonlinearProblem, alg::ExternalProblemAlgorithm, u0, p; kwargs... + ) + return ExternalSolution(u0, sum(abs2, prob.f(u0, p)), prob, alg, SciMLBase.ReturnCode.Success) +end + +function ComplementaritySolve.__solve( + prob::ExternalSystem, alg::ExternalSystemAlgorithm; kwargs... + ) + return (; prob, alg, kwargs) +end + +function ComplementaritySolve.__solve_adjoint( + ::ExternalLinearProblem, ::ExternalAdjoint, ::ExternalSolution, ::Nothing, args...; kwargs... + ) + return ntuple(_ -> ChainRulesCore.NoTangent(), length(args)) +end + +@testset "Generic developer interfaces" begin + linear_prob = ExternalLinearProblem([2.0;;], [-1.0], [0.0]) + nonlinear_prob = ExternalNonlinearProblem((u, p) -> u .- p, [0.0], [1.0]) + problem_alg = ExternalProblemAlgorithm() + + @test SciMLBase.isinplace(linear_prob) + @test !ComplementaritySolve.isbatched(linear_prob) + @test !SciMLBase.isinplace(nonlinear_prob) + + linear_sol = ComplementaritySolve.solve( + linear_prob, problem_alg; sensealg = ExternalAdjoint(), u0 = [0.5] + ) + nonlinear_sol = ComplementaritySolve.solve( + nonlinear_prob, problem_alg; sensealg = ExternalAdjoint(), p = [0.5] + ) + @test linear_sol.u == [0.5] + @test nonlinear_sol.residual == 0.25 + @test occursin("retcode", sprint(show, MIME"text/plain"(), linear_sol)) + + system_prob = ExternalSystem([0.0], [0.0], (0.0, 1.0), nothing, (x, λ, p, t) -> x) + system_sol = ComplementaritySolve.solve(system_prob, ExternalSystemAlgorithm(); saveat = 0.1) + @test system_sol.prob === system_prob + @test system_sol.kwargs[:saveat] == 0.1 + + tangents = ComplementaritySolve.__solve_adjoint( + linear_prob, ExternalAdjoint(), linear_sol, nothing, linear_sol.u, linear_prob.M, linear_prob.q + ) + @test tangents == ( + ChainRulesCore.NoTangent(), ChainRulesCore.NoTangent(), ChainRulesCore.NoTangent(), + ) +end diff --git a/test/qa.jl b/test/qa.jl index 4405095..2aeccaa 100644 --- a/test/qa.jl +++ b/test/qa.jl @@ -1,27 +1,3 @@ using SciMLTesting, ComplementaritySolve, Test -run_qa( - ComplementaritySolve; - api_docs_kwargs = (; rendered_ignore = (:solve,)), - reexports_allow = (:solve,), - aqua_kwargs = (; - ambiguities = false, # Too many ambiguities from downstream - persistent_tasks = false, # PATHSolver precompile workload triggers persistent task detection - project_extras = false, - deps_compat = false, - ), - ei_kwargs = (; - all_qualified_accesses_are_public = (; - ignore = ( - # PATHSolver C-API surface: no exports / no `public` declarations. - :MCP_MajorIterationLimit, :MCP_MinorIterationLimit, - :MCP_NoProgress, :MCP_Solved, :MCP_TimeLimit, :solve_mcp, - :jacobian, # ForwardDiff documented-but-not-`public` API - ), - ), - all_explicit_imports_are_public = (; - # FillArrays.AbstractFill is documented but neither exported nor `public`. - ignore = (:AbstractFill,), - ), - ), -) +run_qa(ComplementaritySolve) diff --git a/test/runtests.jl b/test/runtests.jl index 554c270..a36f7ff 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,6 +19,9 @@ end @testset "Complementarity Solve" begin @testif TESTING_GROUP "Core" begin @testif BACKEND_GROUP "CPU" begin + @safetestset "Generic developer interfaces" begin + include("interfaces.jl") + end @safetestset "Linear Complementarity Problems" begin include("core/cpu/lcp.jl") end From 7c01a1910551bebfbd7cd4722e0a92fd4e2badab Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 1 Aug 2026 17:12:08 -0400 Subject: [PATCH 2/6] Ignore PATHSolver's non-public C-API names in QA PATHSolver declares no `export` and no `public` names, so every qualified access into it fails `all_qualified_accesses_are_public`. There is no public alternative for reaching the PATH C API, so restore an ignore list scoped to the six names actually used, with a note to drop it once PATHSolver publishes them upstream. All other QA suppressions stay removed. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LJbtE2oppQxPFNGSJbhm22 --- test/qa.jl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/qa.jl b/test/qa.jl index 2aeccaa..5762d5e 100644 --- a/test/qa.jl +++ b/test/qa.jl @@ -1,3 +1,18 @@ using SciMLTesting, ComplementaritySolve, Test -run_qa(ComplementaritySolve) +# PATHSolver declares no `export` and no `public` names at all, so every access into it +# trips `all_qualified_accesses_are_public`. Its README nevertheless directs users to +# `PATHSolver.solve_mcp` and the `MCP_Termination` enum values as the supported entry +# point, and there is no public alternative to reach the PATH C API. Drop this ignore +# once PATHSolver declares these names public upstream. +run_qa( + ComplementaritySolve; + ei_kwargs = (; + all_qualified_accesses_are_public = (; + ignore = ( + :MCP_MajorIterationLimit, :MCP_MinorIterationLimit, + :MCP_NoProgress, :MCP_Solved, :MCP_TimeLimit, :solve_mcp, + ), + ), + ), +) From 498f94c20120ef2d5ee1699fdaea8789daebb3bc Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 1 Aug 2026 18:53:07 -0400 Subject: [PATCH 3/6] Take FunctionOperator from SciMLOperators, not SciMLBase SciMLBase 3.40.0 dropped `@reexport using SciMLOperators`, so `using SciMLBase: FunctionOperator` fails to load on SciMLBase >= 3.40 while the declared compat still allows it. The test environments happen to pin SciMLBase to 3.39.1, but Aqua's persistent-tasks check resolves Project.toml on its own and picks 3.41.0, so enabling that check surfaced the break as a precompile failure. Depend on SciMLOperators directly and import the name from the package that owns it. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LJbtE2oppQxPFNGSJbhm22 --- Project.toml | 2 ++ src/ComplementaritySolve.jl | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index b94cc6b..d09a7f3 100644 --- a/Project.toml +++ b/Project.toml @@ -18,6 +18,7 @@ PATHSolver = "f5f7c340-0bb3-5c69-969a-41884d311d1b" Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" @@ -38,6 +39,7 @@ PATHSolver = "1.4" Polyester = "0.7.18" PrecompileTools = "1.2.1" SciMLBase = "2.147, 3.1" +SciMLOperators = "1.3" SciMLPublic = "1" SimpleNonlinearSolve = "2.7" SparseArrays = "1.10" diff --git a/src/ComplementaritySolve.jl b/src/ComplementaritySolve.jl index 3cea30b..00671fb 100644 --- a/src/ComplementaritySolve.jl +++ b/src/ComplementaritySolve.jl @@ -7,7 +7,7 @@ module ComplementaritySolve ## Core / QOL Dependencies using ChainRulesCore: ChainRulesCore, NoTangent, ZeroTangent using GPUArraysCore: GPUArraysCore -using SciMLBase: SciMLBase, FunctionOperator, LinearProblem, NonlinearFunction, +using SciMLBase: SciMLBase, LinearProblem, NonlinearFunction, NonlinearProblem, ODEFunction, ODEProblem, ReturnCode, SteadyStateProblem, isinplace using SciMLPublic: @public @@ -20,7 +20,7 @@ using LinearAlgebra: LinearAlgebra, Diagonal, I, diagind, mul!, norm, pinv, \, / using SparseArrays: SparseArrays ## SciML Dependencies using LinearSolve: LinearSolve -## SciMLOperators is used transitively via SciMLBase (FunctionOperator) +using SciMLOperators: FunctionOperator using SimpleNonlinearSolve: SimpleNonlinearSolve, SimpleNewtonRaphson using NonlinearSolve: NonlinearSolve ## AD Packages (for sensitivities & PATHSolver; move to extensions) From 7fe3c065100932fd2c39dad69cfde8dc98f509c9 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 2 Aug 2026 07:18:13 -0400 Subject: [PATCH 4/6] TEMP DIAGNOSTIC: surface Aqua persistent-tasks subprocess error Do not review; this commit will be reverted. Aqua silences the probe subprocess, so the CI failure has no diagnostics. Reproduce the probe with stderr passed through and report exitcode/termsignal. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5 (1M context) --- test/qa.jl | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/test/qa.jl b/test/qa.jl index 5762d5e..ccb1703 100644 --- a/test/qa.jl +++ b/test/qa.jl @@ -1,10 +1,51 @@ using SciMLTesting, ComplementaritySolve, Test -# PATHSolver declares no `export` and no `public` names at all, so every access into it -# trips `all_qualified_accesses_are_public`. Its README nevertheless directs users to -# `PATHSolver.solve_mcp` and the `MCP_Termination` enum values as the supported entry -# point, and there is no public alternative to reach the PATH C API. Drop this ignore -# once PATHSolver declares these names public upstream. +# TEMPORARY DIAGNOSTIC — revert before review. +# Aqua's persistent-tasks probe runs `Pkg.precompile(; io = devnull)`, so when its +# wrapper subprocess dies the cause is discarded and every failure mode looks +# identical. Reproduce the probe here with stderr passed through, and report the +# subprocess exit code and termination signal, to find out what actually happens +# on CI. Not reproducible locally: 0/25 cold recompiles fail, and a full cold +# precompile capped at 4 CPUs / 16 GB succeeds in 214s. +using Pkg +let + pkgpath = pkgdir(ComplementaritySolve) + wrapperdir = tempname() + wrappername, _ = only(Pkg.generate(wrapperdir)) + prev_project = Base.active_project() + isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(false) + try + Pkg.activate(wrapperdir) + Pkg.develop(Pkg.PackageSpec(path = pkgpath)) + statusfile = joinpath(wrapperdir, "done.log") + open(joinpath(wrapperdir, "src", wrappername * ".jl"), "w") do io + println( + io, """ + module $wrappername + using ComplementaritySolve + open("$(escape_string(statusfile))", "w") do io + println(io, "done"); flush(io) + end + end + """ + ) + end + @info "DIAGNOSTIC: starting wrapper precompile (stderr passed through)" + cmd = `$(Base.julia_cmd()) --project=$wrapperdir -e 'push!(LOAD_PATH, "@stdlib"); using Pkg; Pkg.precompile()'` + t0 = time() + proc = run(cmd, stdin, stdout, stderr; wait = false) + while !isfile(statusfile) && process_running(proc) + sleep(0.5) + end + ok = isfile(statusfile) + wait(proc) + @info "DIAGNOSTIC: wrapper precompile finished" done_log_written = ok exitcode = proc.exitcode termsignal = proc.termsignal elapsed_s = round(time() - t0, digits = 1) + finally + isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(true) + Pkg.activate(prev_project) + end +end + run_qa( ComplementaritySolve; ei_kwargs = (; From 9f244f7c63247f0e6b8fbec6c05f8f41a2d50546 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 2 Aug 2026 07:38:42 -0400 Subject: [PATCH 5/6] TEMP DIAGNOSTIC: add Pkg to test deps so the probe can run Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5 (1M context) --- test/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Project.toml b/test/Project.toml index 1c65226..31cccdf 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -15,6 +15,7 @@ Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationOptimisers = "42dfb2eb-d2b4-4451-abcd-913932933ac1" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" PATHSolver = "f5f7c340-0bb3-5c69-969a-41884d311d1b" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" From eaf4a3eccb0db2cd4414768b4557a900eee41b87 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 2 Aug 2026 08:03:25 -0400 Subject: [PATCH 6/6] Revert temporary persistent-tasks diagnostics The diagnostic did its job: the probe's wrapper precompile exits 0 with no signal, having failed on Julia's "missing from the cache" race for SciMLBaseDifferentiationInterfaceExt. Not a crash, not resource exhaustion. Restores test/qa.jl and drops the temporary Pkg test dependency. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5 (1M context) --- test/Project.toml | 1 - test/qa.jl | 51 +++++------------------------------------------ 2 files changed, 5 insertions(+), 47 deletions(-) diff --git a/test/Project.toml b/test/Project.toml index 31cccdf..1c65226 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -15,7 +15,6 @@ Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationOptimisers = "42dfb2eb-d2b4-4451-abcd-913932933ac1" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" PATHSolver = "f5f7c340-0bb3-5c69-969a-41884d311d1b" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" diff --git a/test/qa.jl b/test/qa.jl index ccb1703..5762d5e 100644 --- a/test/qa.jl +++ b/test/qa.jl @@ -1,51 +1,10 @@ using SciMLTesting, ComplementaritySolve, Test -# TEMPORARY DIAGNOSTIC — revert before review. -# Aqua's persistent-tasks probe runs `Pkg.precompile(; io = devnull)`, so when its -# wrapper subprocess dies the cause is discarded and every failure mode looks -# identical. Reproduce the probe here with stderr passed through, and report the -# subprocess exit code and termination signal, to find out what actually happens -# on CI. Not reproducible locally: 0/25 cold recompiles fail, and a full cold -# precompile capped at 4 CPUs / 16 GB succeeds in 214s. -using Pkg -let - pkgpath = pkgdir(ComplementaritySolve) - wrapperdir = tempname() - wrappername, _ = only(Pkg.generate(wrapperdir)) - prev_project = Base.active_project() - isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(false) - try - Pkg.activate(wrapperdir) - Pkg.develop(Pkg.PackageSpec(path = pkgpath)) - statusfile = joinpath(wrapperdir, "done.log") - open(joinpath(wrapperdir, "src", wrappername * ".jl"), "w") do io - println( - io, """ - module $wrappername - using ComplementaritySolve - open("$(escape_string(statusfile))", "w") do io - println(io, "done"); flush(io) - end - end - """ - ) - end - @info "DIAGNOSTIC: starting wrapper precompile (stderr passed through)" - cmd = `$(Base.julia_cmd()) --project=$wrapperdir -e 'push!(LOAD_PATH, "@stdlib"); using Pkg; Pkg.precompile()'` - t0 = time() - proc = run(cmd, stdin, stdout, stderr; wait = false) - while !isfile(statusfile) && process_running(proc) - sleep(0.5) - end - ok = isfile(statusfile) - wait(proc) - @info "DIAGNOSTIC: wrapper precompile finished" done_log_written = ok exitcode = proc.exitcode termsignal = proc.termsignal elapsed_s = round(time() - t0, digits = 1) - finally - isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(true) - Pkg.activate(prev_project) - end -end - +# PATHSolver declares no `export` and no `public` names at all, so every access into it +# trips `all_qualified_accesses_are_public`. Its README nevertheless directs users to +# `PATHSolver.solve_mcp` and the `MCP_Termination` enum values as the supported entry +# point, and there is no public alternative to reach the PATH C API. Drop this ignore +# once PATHSolver declares these names public upstream. run_qa( ComplementaritySolve; ei_kwargs = (;