From c6c78ec5e8cb13cf301b2c679651db34af0d71d6 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 10:58:47 -0500 Subject: [PATCH 01/18] Add CondaPkg.toml to pin Python < 3.14 for NonlinearSolveSciPy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes CI failures caused by Python 3.14.0's buggy stack overflow detection. The error "Fatal Python error: _Py_CheckRecursiveCall: Unrecoverable stack overflow" with negative memory values indicates a calculation bug in Python 3.14's new stack detection mechanism when interacting with Julia's task system. This issue affects: - Julia 1.9, 1.12 - macOS and Linux - Python 3.14.0 Pinning Python to >= 3.9, < 3.14 resolves the issue. References: - PythonCall.jl issue: https://github.com/JuliaPy/PythonCall.jl/issues/694 - Python upstream issues: python/cpython#139653, python/cpython#137573 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveSciPy/CondaPkg.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 lib/NonlinearSolveSciPy/CondaPkg.toml diff --git a/lib/NonlinearSolveSciPy/CondaPkg.toml b/lib/NonlinearSolveSciPy/CondaPkg.toml new file mode 100644 index 000000000..e58cfe09c --- /dev/null +++ b/lib/NonlinearSolveSciPy/CondaPkg.toml @@ -0,0 +1,8 @@ +# Pin Python to < 3.14 due to stack overflow detection bug in Python 3.14 +# See: https://github.com/JuliaPy/PythonCall.jl/issues/694 +# Python 3.14 introduced a buggy stack overflow detection mechanism that causes +# false positives with negative memory values when interacting with Julia's task system. + +[deps] +python = ">=3.9,<3.14" +scipy = "" From c9c2272eca828e02a52ce4f552c353b9bdba877a Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 15:32:07 -0500 Subject: [PATCH 02/18] Filter out Julia-specific kwargs before forwarding to scipy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes test failures where Julia-specific keyword arguments (alias, verbose) were being forwarded to scipy.optimize functions that don't recognize them. The error was: ``` Python: TypeError: least_squares() got an unexpected keyword argument 'alias' Python: TypeError: root() got an unexpected keyword argument 'alias' ``` Now filters out :alias and :verbose kwargs in all three __solve methods: - SciPyLeastSquares - SciPyRoot - SciPyRootScalar 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/NonlinearSolveSciPy.jl | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 1d327e0fd..ec335a8b1 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -133,12 +133,17 @@ function SciMLBase.__solve( bounds = nothing end + # Filter out Julia-specific kwargs that scipy doesn't understand + scipy_kwargs = filter(kwargs) do (k, v) + k ∉ (:alias, :verbose) + end + res = scipy_optimize[].least_squares(py_f, collect(prob.u0); method = alg.method, loss = alg.loss, max_nfev = maxiters, bounds = bounds === nothing ? PY_NONE[] : bounds, - kwargs...) + scipy_kwargs...) u_vec = Vector{Float64}(res.x) resid = Vector{Float64}(res.fun) @@ -170,11 +175,16 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol + # Filter out Julia-specific kwargs that scipy doesn't understand + scipy_kwargs = filter(kwargs) do (k, v) + k ∉ (:alias, :verbose) + end + res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, tol = tol, options = Dict("maxiter" => maxiters), - kwargs...) + scipy_kwargs...) u_vec = Vector{Float64}(res.x) f!(resid, u_vec) @@ -208,12 +218,17 @@ function SciMLBase.__solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan + # Filter out Julia-specific kwargs that scipy doesn't understand + scipy_kwargs = filter(kwargs) do (k, v) + k ∉ (:alias, :verbose) + end + res = scipy_optimize[].root_scalar(py_f; method = alg.method, bracket = (a, b), maxiter = maxiters, xtol = abstol, - kwargs...) + scipy_kwargs...) u_root = res.root resid = f(u_root, p) From fe2f2a076e5ae0334da8cf04e2fce4fa8c0a3833 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 15:40:39 -0500 Subject: [PATCH 03/18] Fix PythonCall type conversions and kwargs splatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes two critical issues in Python interop: 1. **Type conversion errors**: Replace direct Vector{Float64}() and Float64() constructors with pyconvert() for proper PythonCall type conversion. - Import pyconvert from PythonCall - Update _make_py_residual to use pyconvert(Vector{Float64}, x_py) - Update _make_py_scalar to use pyconvert(Float64, x_py) - Update all result conversions (res.x, res.fun) to use pyconvert Fixes error: ``` MethodError: no method matching Vector{Float64}(::PythonCall.Py) ``` 2. **Kwargs splatting errors**: Change from filter() iterator to pairs generator for proper kwargs splatting to Python functions. - Replace scipy_kwargs = filter(...) with inline pairs generator - Use (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))... Fixes error: ``` TypeError: object of type 'NoneType' has no len() ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/NonlinearSolveSciPy.jl | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index ec335a8b1..df7f8a842 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -3,7 +3,7 @@ module NonlinearSolveSciPy using ConcreteStructs: @concrete using Reexport: @reexport -using PythonCall: pyimport, pyfunc, Py +using PythonCall: pyimport, pyfunc, pyconvert, Py const scipy_optimize = Ref{Union{Py, Nothing}}(nothing) const PY_NONE = Ref{Union{Py, Nothing}}(nothing) @@ -99,7 +99,7 @@ Internal: wrap a Julia residual function into a Python callable """ function _make_py_residual(f::F, p) where F return pyfunc(x_py -> begin - x = Vector{Float64}(x_py) + x = pyconvert(Vector{Float64}, x_py) r = f(x, p) return r end) @@ -110,7 +110,7 @@ Internal: wrap a Julia scalar function into a Python callable """ function _make_py_scalar(f::F, p) where F return pyfunc(x_py -> begin - x = Float64(x_py) + x = pyconvert(Float64, x_py) return f(x, p) end) end @@ -134,19 +134,15 @@ function SciMLBase.__solve( end # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = filter(kwargs) do (k, v) - k ∉ (:alias, :verbose) - end - res = scipy_optimize[].least_squares(py_f, collect(prob.u0); method = alg.method, loss = alg.loss, max_nfev = maxiters, bounds = bounds === nothing ? PY_NONE[] : bounds, - scipy_kwargs...) + (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))...) - u_vec = Vector{Float64}(res.x) - resid = Vector{Float64}(res.fun) + u_vec = pyconvert(Vector{Float64}, res.x) + resid = pyconvert(Vector{Float64}, res.fun) u = prob.u0 isa Number ? u_vec[1] : reshape(u_vec, size(prob.u0)) @@ -168,7 +164,7 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; f!, u0, resid = construct_extension_function_wrapper(prob; alias_u0) py_f = pyfunc(x_py -> begin - x = Vector{Float64}(x_py) + x = pyconvert(Vector{Float64}, x_py) f!(resid, x) return resid end) @@ -176,17 +172,13 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = filter(kwargs) do (k, v) - k ∉ (:alias, :verbose) - end - res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, tol = tol, options = Dict("maxiter" => maxiters), - scipy_kwargs...) + (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))...) - u_vec = Vector{Float64}(res.x) + u_vec = pyconvert(Vector{Float64}, res.x) f!(resid, u_vec) u_out = prob.u0 isa Number ? u_vec[1] : reshape(u_vec, size(prob.u0)) @@ -219,16 +211,12 @@ function SciMLBase.__solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = filter(kwargs) do (k, v) - k ∉ (:alias, :verbose) - end - res = scipy_optimize[].root_scalar(py_f; method = alg.method, bracket = (a, b), maxiter = maxiters, xtol = abstol, - scipy_kwargs...) + (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))...) u_root = res.root resid = f(u_root, p) From b0eb8ff3b57134697428c7e6acf62280686ee5df Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:04:24 -0500 Subject: [PATCH 04/18] Fix kwargs splatting and PythonCall boolean conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes two critical issues discovered in CI: 1. **Kwargs splatting error**: Convert generator to Tuple before splatting - Change from inline generator: `(k => v for ...)...` - To collected Tuple: `scipy_kwargs = Tuple(k => v for ...); scipy_kwargs...` - Fixes: `TypeError: object of type 'NoneType' has no len()` 2. **Boolean context errors**: Use pyconvert for Python boolean fields - `res.success` → `pyconvert(Bool, res.success)` - `res.converged` → `pyconvert(Bool, res.converged)` - Fixes: `TypeError: non-boolean (PythonCall.Py) used in boolean context` Applied to all three solver methods: - SciPyLeastSquares (line 137, 151) - SciPyRoot (line 177, 190) - SciPyRootScalar (line 218, 230) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/NonlinearSolveSciPy.jl | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index df7f8a842..d52f71fff 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -134,19 +134,21 @@ function SciMLBase.__solve( end # Filter out Julia-specific kwargs that scipy doesn't understand + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + res = scipy_optimize[].least_squares(py_f, collect(prob.u0); method = alg.method, loss = alg.loss, max_nfev = maxiters, bounds = bounds === nothing ? PY_NONE[] : bounds, - (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))...) + scipy_kwargs...) u_vec = pyconvert(Vector{Float64}, res.x) resid = pyconvert(Vector{Float64}, res.fun) u = prob.u0 isa Number ? u_vec[1] : reshape(u_vec, size(prob.u0)) - ret = res.success ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure + ret = pyconvert(Bool, res.success) ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure njev = try Int(res.njev) catch @@ -172,18 +174,20 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol # Filter out Julia-specific kwargs that scipy doesn't understand + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, tol = tol, options = Dict("maxiter" => maxiters), - (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))...) + scipy_kwargs...) u_vec = pyconvert(Vector{Float64}, res.x) f!(resid, u_vec) u_out = prob.u0 isa Number ? u_vec[1] : reshape(u_vec, size(prob.u0)) - ret = res.success ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure + ret = pyconvert(Bool, res.success) ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure nfev = try Int(res.nfev) catch @@ -211,17 +215,19 @@ function SciMLBase.__solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan # Filter out Julia-specific kwargs that scipy doesn't understand + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + res = scipy_optimize[].root_scalar(py_f; method = alg.method, bracket = (a, b), maxiter = maxiters, xtol = abstol, - (k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose))...) + scipy_kwargs...) u_root = res.root resid = f(u_root, p) - ret = res.converged ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure + ret = pyconvert(Bool, res.converged) ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure nfev = try Int(res.function_calls) catch From 8cfb46fa80aec7d1834013773cd92b26fd9859ed Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:08:49 -0500 Subject: [PATCH 05/18] Fix bounds parameter passing to scipy.optimize.least_squares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes error: `TypeError: object of type 'NoneType' has no len()` Problem: We were passing `bounds=None` to scipy when bounds weren't specified, but scipy.optimize.least_squares tries to call len() on the bounds parameter, which fails on None. Solution: Conditionally omit the bounds kwarg entirely when not specified, rather than passing Python's None. This allows scipy to use its default bounds of (-inf, inf). Split the scipy call into two branches: - Without bounds: When prob doesn't have lb/ub constraints - With bounds: When prob has lb/ub constraints 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/NonlinearSolveSciPy.jl | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index d52f71fff..076b9696f 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -136,12 +136,21 @@ function SciMLBase.__solve( # Filter out Julia-specific kwargs that scipy doesn't understand scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) - res = scipy_optimize[].least_squares(py_f, collect(prob.u0); - method = alg.method, - loss = alg.loss, - max_nfev = maxiters, - bounds = bounds === nothing ? PY_NONE[] : bounds, - scipy_kwargs...) + # Call scipy with conditional bounds argument + if bounds === nothing + res = scipy_optimize[].least_squares(py_f, collect(prob.u0); + method = alg.method, + loss = alg.loss, + max_nfev = maxiters, + scipy_kwargs...) + else + res = scipy_optimize[].least_squares(py_f, collect(prob.u0); + method = alg.method, + loss = alg.loss, + max_nfev = maxiters, + bounds = bounds, + scipy_kwargs...) + end u_vec = pyconvert(Vector{Float64}, res.x) resid = pyconvert(Vector{Float64}, res.fun) From b8511055d628a8046a6879e7b173a029defc931f Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:23:39 -0500 Subject: [PATCH 06/18] Fix IntervalNonlinearProblem compatibility in solve function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntervalNonlinearProblem doesn't have a u0 field (it uses tspan for the bracket). Added conditional check using hasfield() before accessing prob.u0 in both solve() and init() functions to handle problem types that don't have initial conditions. Fixes error: "type IntervalNonlinearProblem has no field u0" when solving with SciPyRootScalar. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveBase/src/solve.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/NonlinearSolveBase/src/solve.jl b/lib/NonlinearSolveBase/src/solve.jl index 36e8a602f..b8df52a00 100644 --- a/lib/NonlinearSolveBase/src/solve.jl +++ b/lib/NonlinearSolveBase/src/solve.jl @@ -80,7 +80,7 @@ function solve(prob::AbstractNonlinearProblem, args...; sensealg = nothing, NonlinearAliasSpecifier(alias_u0 = false) end - u0 = u0 !== nothing ? u0 : prob.u0 + u0 = u0 !== nothing ? u0 : (hasfield(typeof(prob), :u0) ? prob.u0 : nothing) p = p !== nothing ? p : prob.p if wrap isa Val{true} @@ -208,7 +208,7 @@ function init( verbose = NonlinearVerbosity(verbose) end - u0 = u0 !== nothing ? u0 : prob.u0 + u0 = u0 !== nothing ? u0 : (hasfield(typeof(prob), :u0) ? prob.u0 : nothing) p = p !== nothing ? p : prob.p init_up(prob, sensealg, u0, p, args...; alias = alias_spec, verbose, kwargs...) From 73f17209418917a9b69f92bcaa838e12c027a97f Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:29:59 -0500 Subject: [PATCH 07/18] Use solve instead of __solve for IntervalNonlinearProblem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntervalNonlinearProblem doesn't have a u0 field, so it should bypass the NonlinearSolveBase solve machinery by directly overloading CommonSolve.solve instead of SciMLBase.__solve. This avoids the need for conditional checks in the base solve function. - Changed SciPyRootScalar to overload CommonSolve.solve for IntervalNonlinearProblem - Added using CommonSolve to NonlinearSolveSciPy - Reverted the hasfield() conditional checks in NonlinearSolveBase since they're no longer needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveBase/src/solve.jl | 4 ++-- lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/NonlinearSolveBase/src/solve.jl b/lib/NonlinearSolveBase/src/solve.jl index b8df52a00..36e8a602f 100644 --- a/lib/NonlinearSolveBase/src/solve.jl +++ b/lib/NonlinearSolveBase/src/solve.jl @@ -80,7 +80,7 @@ function solve(prob::AbstractNonlinearProblem, args...; sensealg = nothing, NonlinearAliasSpecifier(alias_u0 = false) end - u0 = u0 !== nothing ? u0 : (hasfield(typeof(prob), :u0) ? prob.u0 : nothing) + u0 = u0 !== nothing ? u0 : prob.u0 p = p !== nothing ? p : prob.p if wrap isa Val{true} @@ -208,7 +208,7 @@ function init( verbose = NonlinearVerbosity(verbose) end - u0 = u0 !== nothing ? u0 : (hasfield(typeof(prob), :u0) ? prob.u0 : nothing) + u0 = u0 !== nothing ? u0 : prob.u0 p = p !== nothing ? p : prob.p init_up(prob, sensealg, u0, p, args...; alias = alias_spec, verbose, kwargs...) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 076b9696f..77d20d175 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -19,6 +19,7 @@ function __init__() end end +using CommonSolve using SciMLBase using NonlinearSolveBase: AbstractNonlinearSolveAlgorithm, construct_extension_function_wrapper @@ -215,7 +216,7 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; original = res, stats = stats) end -function SciMLBase.__solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyRootScalar; +function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyRootScalar, args...; abstol = nothing, maxiters = 10_000, kwargs...) f = prob.f p = prob.p From 6c17a16d25f829f2a8b6e50cf6e915f77f875067 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:33:01 -0500 Subject: [PATCH 08/18] Add CommonSolve dependency to NonlinearSolveSciPy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added CommonSolve to dependencies since we use CommonSolve.solve for IntervalNonlinearProblem. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveSciPy/Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/NonlinearSolveSciPy/Project.toml b/lib/NonlinearSolveSciPy/Project.toml index ad5b05a50..1fcc7f2af 100644 --- a/lib/NonlinearSolveSciPy/Project.toml +++ b/lib/NonlinearSolveSciPy/Project.toml @@ -4,6 +4,7 @@ authors = ["SciML"] version = "1.2.0" [deps] +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471" NonlinearSolveBase = "be0214bd-f91f-a760-ac4e-3421ce2b2da0" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" @@ -15,6 +16,7 @@ SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" path = "../NonlinearSolveBase" [compat] +CommonSolve = "0.2" ConcreteStructs = "0.2.3" Hwloc = "3" InteractiveUtils = "<0.0.1, 1" From a277d5e3288176d24f5a3d07fc46a88532dbb80c Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:40:25 -0500 Subject: [PATCH 09/18] Fix remaining PythonCall type conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert all Python statistics fields (nfev, njev, nit, function_calls, iterations) to Julia Int using pyconvert instead of direct Int() constructor. Also convert u_root from Python Float to Julia Float64 in SciPyRootScalar. Fixes: - MethodError: Cannot convert PythonCall.Py to Int64 in NLStats - MethodError: no method matching ndims(::PythonCall.Py) in build_solution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/NonlinearSolveSciPy.jl | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 77d20d175..03bc8958d 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -159,12 +159,17 @@ function SciMLBase.__solve( u = prob.u0 isa Number ? u_vec[1] : reshape(u_vec, size(prob.u0)) ret = pyconvert(Bool, res.success) ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure + nfev = try + pyconvert(Int, res.nfev) + catch + 0 + end njev = try - Int(res.njev) + pyconvert(Int, res.njev) catch 0 end - stats = SciMLBase.NLStats(res.nfev, njev, 0, 0, res.nfev) + stats = SciMLBase.NLStats(nfev, njev, 0, 0, nfev) return SciMLBase.build_solution(prob, alg, u, resid; retcode = ret, original = res, stats = stats) @@ -199,15 +204,13 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; ret = pyconvert(Bool, res.success) ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure nfev = try - Int(res.nfev) + pyconvert(Int, res.nfev) catch - ; 0 end niter = try - Int(res.nit) + pyconvert(Int, res.nit) catch - ; 0 end stats = SciMLBase.NLStats(nfev, 0, 0, 0, niter) @@ -234,20 +237,18 @@ function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR xtol = abstol, scipy_kwargs...) - u_root = res.root + u_root = pyconvert(Float64, res.root) resid = f(u_root, p) ret = pyconvert(Bool, res.converged) ? SciMLBase.ReturnCode.Success : SciMLBase.ReturnCode.Failure nfev = try - Int(res.function_calls) + pyconvert(Int, res.function_calls) catch - ; 0 end niter = try - Int(res.iterations) + pyconvert(Int, res.iterations) catch - ; 0 end stats = SciMLBase.NLStats(nfev, 0, 0, 0, niter) From c99e4ab071c407373ff8cae32bb47b43fe96ab8d Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:43:59 -0500 Subject: [PATCH 10/18] Filter lb and ub from kwargs passed to scipy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added :lb and :ub to the kwargs filter list to prevent them from being forwarded to NonlinearSolveBase which doesn't recognize them as valid solve kwargs. These bounds are extracted directly from the problem using hasproperty and passed to scipy separately. Fixes: Unrecognized keyword arguments error for [:lb, :ub] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 03bc8958d..2d61e310f 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -135,7 +135,7 @@ function SciMLBase.__solve( end # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) # Call scipy with conditional bounds argument if bounds === nothing @@ -189,7 +189,7 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, @@ -228,7 +228,7 @@ function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) res = scipy_optimize[].root_scalar(py_f; method = alg.method, From 7563889afe5973cdb38013c932e76958d7e12ea4 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:49:35 -0500 Subject: [PATCH 11/18] Revert "Filter lb and ub from kwargs passed to scipy" This reverts commit c99e4ab071c407373ff8cae32bb47b43fe96ab8d. --- lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 2d61e310f..03bc8958d 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -135,7 +135,7 @@ function SciMLBase.__solve( end # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) # Call scipy with conditional bounds argument if bounds === nothing @@ -189,7 +189,7 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, @@ -228,7 +228,7 @@ function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) res = scipy_optimize[].root_scalar(py_f; method = alg.method, From 8cb8c423af974b15cf7b859cd2731b94452a3ecb Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:51:48 -0500 Subject: [PATCH 12/18] Add NonlinearKeywordArgError to accept lb and ub in NonlinearSolveBase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created a custom keyword argument handler NonlinearKeywordArgError that extends the standard SciMLBase keywords to include bounds (lb, ub) for NonlinearLeastSquaresProblem. - Added NonlinearKeywordArgError struct and checkkwargs method in NonlinearSolveBase.jl - Added specific solve_call and init_call methods for NonlinearLeastSquaresProblem that use NonlinearKeywordArgError as the default kwargshandle - This allows bounds to be passed as problem kwargs without triggering "Unrecognized keyword arguments" errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/NonlinearSolveBase.jl | 29 ++++++++++ lib/NonlinearSolveBase/src/solve.jl | 55 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl b/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl index 43d7e8439..dbc68b986 100644 --- a/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl +++ b/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl @@ -42,6 +42,35 @@ using Printf: @printf const DI = DifferentiationInterface const SII = SymbolicIndexingInterface +# Custom keyword argument handler that extends the standard SciMLBase keywords +# to include bounds (lb, ub) for NonlinearLeastSquaresProblem +struct NonlinearKeywordArgError end + +function SciMLBase.checkkwargs(::Type{NonlinearKeywordArgError}; kwargs...) + keywords = keys(kwargs) + allowed_keywords = (:dense, :saveat, :save_idxs, :save_discretes, :tstops, :tspan, + :d_discontinuities, :save_everystep, :save_on, :save_start, :save_end, + :initialize_save, :adaptive, :abstol, :reltol, :dt, :dtmax, :dtmin, + :force_dtmin, :internalnorm, :controller, :gamma, :beta1, :beta2, + :qmax, :qmin, :qsteady_min, :qsteady_max, :qoldinit, :failfactor, + :calck, :alias_u0, :maxiters, :maxtime, :callback, :isoutofdomain, + :unstable_check, :verbose, :merge_callbacks, :progress, :progress_steps, + :progress_name, :progress_message, :progress_id, :timeseries_errors, + :dense_errors, :weak_timeseries_errors, :weak_dense_errors, :wrap, + :calculate_error, :initializealg, :alg, :save_noise, :delta, :seed, + :alg_hints, :kwargshandle, :trajectories, :batch_size, :sensealg, + :advance_to_tstop, :stop_at_next_tstop, :u0, :p, :default_set, + :second_time, :prob_choice, :alias_jump, :alias_noise, :batch, + :nlsolve_kwargs, :odesolve_kwargs, :linsolve_kwargs, :ensemblealg, + :show_trace, :trace_level, :store_trace, :termination_condition, + :alias, :fit_parameters, :lb, :ub) # Added lb and ub + for kw in keywords + if kw ∉ allowed_keywords + throw(SciMLBase.KeywordArgumentError(kw)) + end + end +end + include("public.jl") include("utils.jl") include("verbosity.jl") diff --git a/lib/NonlinearSolveBase/src/solve.jl b/lib/NonlinearSolveBase/src/solve.jl index 36e8a602f..8afaf0c5d 100644 --- a/lib/NonlinearSolveBase/src/solve.jl +++ b/lib/NonlinearSolveBase/src/solve.jl @@ -171,6 +171,42 @@ function solve_call(prob::SteadyStateProblem, kwargs...) end +function solve_call(prob::NonlinearLeastSquaresProblem, args...; + merge_callbacks = true, kwargshandle = nothing, kwargs...) + # Use NonlinearKeywordArgError which accepts lb and ub + kwargshandle = kwargshandle === nothing ? NonlinearKeywordArgError : kwargshandle + kwargshandle = has_kwargs(prob) && haskey(prob.kwargs, :kwargshandle) ? + prob.kwargs[:kwargshandle] : kwargshandle + + if has_kwargs(prob) + kwargs = isempty(prob.kwargs) ? kwargs : merge(values(prob.kwargs), kwargs) + end + + checkkwargs(kwargshandle; kwargs...) + if isdefined(prob, :u0) + if prob.u0 isa Array + if !isconcretetype(RecursiveArrayTools.recursive_unitless_eltype(prob.u0)) + throw(NonConcreteEltypeError(RecursiveArrayTools.recursive_unitless_eltype(prob.u0))) + end + + if !(eltype(prob.u0) <: Number) && !(eltype(prob.u0) <: Enum) + throw(NonNumberEltypeError(eltype(prob.u0))) + end + end + + if prob.u0 === nothing + return build_null_solution(prob, args...; kwargs...) + end + end + + if hasfield(typeof(prob), :f) && hasfield(typeof(prob.f), :f) && + prob.f.f isa EvalFunc + Base.invokelatest(__solve, prob, args...; kwargs...) + else + __solve(prob, args...; kwargs...) + end +end + function init( prob::AbstractNonlinearProblem, args...; sensealg = nothing, u0 = nothing, p = nothing, verbose = NonlinearVerbosity(), kwargs...) @@ -258,6 +294,25 @@ function init_call(_prob, args...; merge_callbacks=true, kwargshandle=nothing, end end +function init_call(prob::NonlinearLeastSquaresProblem, args...; + merge_callbacks = true, kwargshandle = nothing, kwargs...) + # Use NonlinearKeywordArgError which accepts lb and ub + kwargshandle = kwargshandle === nothing ? NonlinearKeywordArgError : kwargshandle + kwargshandle = has_kwargs(prob) && haskey(prob.kwargs, :kwargshandle) ? + prob.kwargs[:kwargshandle] : kwargshandle + if has_kwargs(prob) + kwargs = isempty(prob.kwargs) ? kwargs : merge(values(prob.kwargs), kwargs) + end + + checkkwargs(kwargshandle; kwargs...) + if hasfield(typeof(prob), :f) && hasfield(typeof(prob.f), :f) && + prob.f.f isa EvalFunc + Base.invokelatest(__init, prob, args...; kwargs...) + else + __init(prob, args...; kwargs...) + end +end + function SciMLBase.__solve( prob::AbstractNonlinearProblem, alg::AbstractNonlinearSolveAlgorithm, args...; kwargs...) cache = SciMLBase.__init(prob, alg, args...; kwargs...) From 37db027d90f7c0ab019ccf19431eccbed570034a Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:55:18 -0500 Subject: [PATCH 13/18] Revert "Add NonlinearKeywordArgError to accept lb and ub in NonlinearSolveBase" This reverts commit 8cb8c423af974b15cf7b859cd2731b94452a3ecb. --- .../src/NonlinearSolveBase.jl | 29 ---------- lib/NonlinearSolveBase/src/solve.jl | 55 ------------------- 2 files changed, 84 deletions(-) diff --git a/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl b/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl index dbc68b986..43d7e8439 100644 --- a/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl +++ b/lib/NonlinearSolveBase/src/NonlinearSolveBase.jl @@ -42,35 +42,6 @@ using Printf: @printf const DI = DifferentiationInterface const SII = SymbolicIndexingInterface -# Custom keyword argument handler that extends the standard SciMLBase keywords -# to include bounds (lb, ub) for NonlinearLeastSquaresProblem -struct NonlinearKeywordArgError end - -function SciMLBase.checkkwargs(::Type{NonlinearKeywordArgError}; kwargs...) - keywords = keys(kwargs) - allowed_keywords = (:dense, :saveat, :save_idxs, :save_discretes, :tstops, :tspan, - :d_discontinuities, :save_everystep, :save_on, :save_start, :save_end, - :initialize_save, :adaptive, :abstol, :reltol, :dt, :dtmax, :dtmin, - :force_dtmin, :internalnorm, :controller, :gamma, :beta1, :beta2, - :qmax, :qmin, :qsteady_min, :qsteady_max, :qoldinit, :failfactor, - :calck, :alias_u0, :maxiters, :maxtime, :callback, :isoutofdomain, - :unstable_check, :verbose, :merge_callbacks, :progress, :progress_steps, - :progress_name, :progress_message, :progress_id, :timeseries_errors, - :dense_errors, :weak_timeseries_errors, :weak_dense_errors, :wrap, - :calculate_error, :initializealg, :alg, :save_noise, :delta, :seed, - :alg_hints, :kwargshandle, :trajectories, :batch_size, :sensealg, - :advance_to_tstop, :stop_at_next_tstop, :u0, :p, :default_set, - :second_time, :prob_choice, :alias_jump, :alias_noise, :batch, - :nlsolve_kwargs, :odesolve_kwargs, :linsolve_kwargs, :ensemblealg, - :show_trace, :trace_level, :store_trace, :termination_condition, - :alias, :fit_parameters, :lb, :ub) # Added lb and ub - for kw in keywords - if kw ∉ allowed_keywords - throw(SciMLBase.KeywordArgumentError(kw)) - end - end -end - include("public.jl") include("utils.jl") include("verbosity.jl") diff --git a/lib/NonlinearSolveBase/src/solve.jl b/lib/NonlinearSolveBase/src/solve.jl index 8afaf0c5d..36e8a602f 100644 --- a/lib/NonlinearSolveBase/src/solve.jl +++ b/lib/NonlinearSolveBase/src/solve.jl @@ -171,42 +171,6 @@ function solve_call(prob::SteadyStateProblem, kwargs...) end -function solve_call(prob::NonlinearLeastSquaresProblem, args...; - merge_callbacks = true, kwargshandle = nothing, kwargs...) - # Use NonlinearKeywordArgError which accepts lb and ub - kwargshandle = kwargshandle === nothing ? NonlinearKeywordArgError : kwargshandle - kwargshandle = has_kwargs(prob) && haskey(prob.kwargs, :kwargshandle) ? - prob.kwargs[:kwargshandle] : kwargshandle - - if has_kwargs(prob) - kwargs = isempty(prob.kwargs) ? kwargs : merge(values(prob.kwargs), kwargs) - end - - checkkwargs(kwargshandle; kwargs...) - if isdefined(prob, :u0) - if prob.u0 isa Array - if !isconcretetype(RecursiveArrayTools.recursive_unitless_eltype(prob.u0)) - throw(NonConcreteEltypeError(RecursiveArrayTools.recursive_unitless_eltype(prob.u0))) - end - - if !(eltype(prob.u0) <: Number) && !(eltype(prob.u0) <: Enum) - throw(NonNumberEltypeError(eltype(prob.u0))) - end - end - - if prob.u0 === nothing - return build_null_solution(prob, args...; kwargs...) - end - end - - if hasfield(typeof(prob), :f) && hasfield(typeof(prob.f), :f) && - prob.f.f isa EvalFunc - Base.invokelatest(__solve, prob, args...; kwargs...) - else - __solve(prob, args...; kwargs...) - end -end - function init( prob::AbstractNonlinearProblem, args...; sensealg = nothing, u0 = nothing, p = nothing, verbose = NonlinearVerbosity(), kwargs...) @@ -294,25 +258,6 @@ function init_call(_prob, args...; merge_callbacks=true, kwargshandle=nothing, end end -function init_call(prob::NonlinearLeastSquaresProblem, args...; - merge_callbacks = true, kwargshandle = nothing, kwargs...) - # Use NonlinearKeywordArgError which accepts lb and ub - kwargshandle = kwargshandle === nothing ? NonlinearKeywordArgError : kwargshandle - kwargshandle = has_kwargs(prob) && haskey(prob.kwargs, :kwargshandle) ? - prob.kwargs[:kwargshandle] : kwargshandle - if has_kwargs(prob) - kwargs = isempty(prob.kwargs) ? kwargs : merge(values(prob.kwargs), kwargs) - end - - checkkwargs(kwargshandle; kwargs...) - if hasfield(typeof(prob), :f) && hasfield(typeof(prob.f), :f) && - prob.f.f isa EvalFunc - Base.invokelatest(__init, prob, args...; kwargs...) - else - __init(prob, args...; kwargs...) - end -end - function SciMLBase.__solve( prob::AbstractNonlinearProblem, alg::AbstractNonlinearSolveAlgorithm, args...; kwargs...) cache = SciMLBase.__init(prob, alg, args...; kwargs...) From 8fd05da1516eff2b8967924f3cb72c7f3d7e49f2 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 18:55:40 -0500 Subject: [PATCH 14/18] Filter lb and ub from scipy kwargs in NonlinearSolveSciPy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter out :lb and :ub from kwargs passed to scipy since these bounds are extracted directly from the problem using hasproperty and passed to scipy separately via the bounds parameter. This is a minimal, non-invasive fix that only touches NonlinearSolveSciPy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 03bc8958d..deeae3056 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -135,7 +135,8 @@ function SciMLBase.__solve( end # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + # lb and ub are extracted from prob directly, not passed as kwargs + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) # Call scipy with conditional bounds argument if bounds === nothing @@ -189,7 +190,8 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + # lb and ub are extracted from prob directly, not passed as kwargs + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, @@ -228,7 +230,8 @@ function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan # Filter out Julia-specific kwargs that scipy doesn't understand - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) + # lb and ub are extracted from prob directly, not passed as kwargs + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) res = scipy_optimize[].root_scalar(py_f; method = alg.method, From 20fb06533199952e327f24b37c7db228766c9412 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 19:03:10 -0500 Subject: [PATCH 15/18] Revert "Filter lb and ub from scipy kwargs in NonlinearSolveSciPy" This reverts commit 8fd05da1516eff2b8967924f3cb72c7f3d7e49f2. --- lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index deeae3056..03bc8958d 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -135,8 +135,7 @@ function SciMLBase.__solve( end # Filter out Julia-specific kwargs that scipy doesn't understand - # lb and ub are extracted from prob directly, not passed as kwargs - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) # Call scipy with conditional bounds argument if bounds === nothing @@ -190,8 +189,7 @@ function SciMLBase.__solve(prob::SciMLBase.NonlinearProblem, alg::SciPyRoot; tol = abstol === nothing ? nothing : abstol # Filter out Julia-specific kwargs that scipy doesn't understand - # lb and ub are extracted from prob directly, not passed as kwargs - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) res = scipy_optimize[].root(py_f, collect(u0); method = alg.method, @@ -230,8 +228,7 @@ function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR a, b = prob.tspan # Filter out Julia-specific kwargs that scipy doesn't understand - # lb and ub are extracted from prob directly, not passed as kwargs - scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose, :lb, :ub)) + scipy_kwargs = Tuple(k => v for (k, v) in pairs(kwargs) if k ∉ (:alias, :verbose)) res = scipy_optimize[].root_scalar(py_f; method = alg.method, From ec166917b3c7bcd15e8afdaabb69cbe0eb9db040 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 19:07:44 -0500 Subject: [PATCH 16/18] Use allowsbounds trait and lb/ub fields from NonlinearLeastSquaresProblem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated NonlinearSolveSciPy and NonlinearSolveBase to use the new lb/ub fields added to NonlinearLeastSquaresProblem in SciMLBase.jl PR #1169: NonlinearSolveSciPy: - Import and use SciMLBase.allowsbounds trait - Implement allowsbounds trait methods: - allowsbounds(::SciPyLeastSquares) = true - allowsbounds(::SciPyRoot) = false - allowsbounds(::SciPyRootScalar) = false - Update __solve to get bounds from prob.lb and prob.ub instead of hasproperty NonlinearSolveBase: - Add bounds checking in solve_call for NonlinearLeastSquaresProblem - Error if algorithm doesn't support bounds but problem has them This properly separates concerns: - SciMLBase defines the problem interface with lb/ub fields - allowsbounds trait indicates algorithm support - NonlinearSolveBase validates compatibility - NonlinearSolveSciPy uses the fields directly Depends on: SciMLBase.jl #1169 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveBase/src/solve.jl | 8 ++++++++ .../src/NonlinearSolveSciPy.jl | 16 ++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/NonlinearSolveBase/src/solve.jl b/lib/NonlinearSolveBase/src/solve.jl index 36e8a602f..b18db6fde 100644 --- a/lib/NonlinearSolveBase/src/solve.jl +++ b/lib/NonlinearSolveBase/src/solve.jl @@ -136,6 +136,14 @@ function solve_call(_prob, args...; merge_callbacks = true, kwargshandle = nothi end checkkwargs(kwargshandle; kwargs...) + + # Check bounds support for NonlinearLeastSquaresProblem + if _prob isa SciMLBase.NonlinearLeastSquaresProblem && + (_prob.lb !== nothing || _prob.ub !== nothing) && + length(args) > 0 && !SciMLBase.allowsbounds(args[1]) + error("Algorithm $(args[1]) does not support bounds. Use an algorithm with allowsbounds(alg) == true.") + end + if isdefined(_prob, :u0) if _prob.u0 isa Array if !isconcretetype(RecursiveArrayTools.recursive_unitless_eltype(_prob.u0)) diff --git a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl index 03bc8958d..bf69f5451 100644 --- a/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl +++ b/lib/NonlinearSolveSciPy/src/NonlinearSolveSciPy.jl @@ -21,6 +21,7 @@ end using CommonSolve using SciMLBase +using SciMLBase: allowsbounds using NonlinearSolveBase: AbstractNonlinearSolveAlgorithm, construct_extension_function_wrapper @@ -123,12 +124,10 @@ function SciMLBase.__solve( # Construct Python residual py_f = _make_py_residual(prob.f, prob.p) - # Bounds handling (lb/ub may be missing) - has_lb = hasproperty(prob, :lb) - has_ub = hasproperty(prob, :ub) - if has_lb || has_ub - lb = has_lb ? getproperty(prob, :lb) : fill(-Inf, length(prob.u0)) - ub = has_ub ? getproperty(prob, :ub) : fill(Inf, length(prob.u0)) + # Bounds handling from problem fields + if prob.lb !== nothing || prob.ub !== nothing + lb = prob.lb !== nothing ? prob.lb : fill(-Inf, length(prob.u0)) + ub = prob.ub !== nothing ? prob.ub : fill(Inf, length(prob.u0)) bounds = (lb, ub) else bounds = nothing @@ -257,6 +256,11 @@ function CommonSolve.solve(prob::SciMLBase.IntervalNonlinearProblem, alg::SciPyR original = res, stats = stats) end +# Trait declarations +SciMLBase.allowsbounds(::SciPyLeastSquares) = true +SciMLBase.allowsbounds(::SciPyRoot) = false +SciMLBase.allowsbounds(::SciPyRootScalar) = false + @reexport using SciMLBase, NonlinearSolveBase export SciPyLeastSquares, SciPyLeastSquaresTRF, SciPyLeastSquaresDogbox, From c58b18e820fafae1209035e7763b552c617d54c9 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 19:19:29 -0500 Subject: [PATCH 17/18] Update bounds checking to support NonlinearProblem as well MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extended bounds checking in NonlinearSolveBase to support both NonlinearProblem and NonlinearLeastSquaresProblem with lb/ub fields. Updated SciMLBase PR to add lb/ub fields to both problem types: - Removed unnecessary UNSET_BOUNDS constant - Added lb/ub fields to NonlinearProblem - Updated ConstructionBase.constructorof for both problem types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/NonlinearSolveBase/src/solve.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/NonlinearSolveBase/src/solve.jl b/lib/NonlinearSolveBase/src/solve.jl index b18db6fde..581f8b46d 100644 --- a/lib/NonlinearSolveBase/src/solve.jl +++ b/lib/NonlinearSolveBase/src/solve.jl @@ -137,8 +137,9 @@ function solve_call(_prob, args...; merge_callbacks = true, kwargshandle = nothi checkkwargs(kwargshandle; kwargs...) - # Check bounds support for NonlinearLeastSquaresProblem - if _prob isa SciMLBase.NonlinearLeastSquaresProblem && + # Check bounds support for problems with bounds + if (_prob isa SciMLBase.NonlinearProblem || _prob isa SciMLBase.NonlinearLeastSquaresProblem) && + (hasfield(typeof(_prob), :lb) && hasfield(typeof(_prob), :ub)) && (_prob.lb !== nothing || _prob.ub !== nothing) && length(args) > 0 && !SciMLBase.allowsbounds(args[1]) error("Algorithm $(args[1]) does not support bounds. Use an algorithm with allowsbounds(alg) == true.") From 0d83bc675ca6a1fc8841f77bdc5dbf7e51c0cd63 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 13 Nov 2025 19:32:15 -0500 Subject: [PATCH 18/18] Bump SciMLBase compat to 2.127 for lb/ub fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated SciMLBase compat bounds to require version 2.127 which includes the lb and ub fields added to NonlinearProblem and NonlinearLeastSquaresProblem. Updated in: - Main Project.toml - lib/NonlinearSolveBase/Project.toml - lib/NonlinearSolveSciPy/Project.toml Closes dependency on SciMLBase.jl #1169 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Project.toml | 2 +- lib/NonlinearSolveBase/Project.toml | 2 +- lib/NonlinearSolveSciPy/Project.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 6fdcad198..8bb3832e2 100644 --- a/Project.toml +++ b/Project.toml @@ -108,7 +108,7 @@ Reexport = "1.2.2" ReverseDiff = "1.15" SciMLLogging = "1.3" SIAMFANLEquations = "1.0.1" -SciMLBase = "2.116" +SciMLBase = "2.127" SimpleNonlinearSolve = "2.11" SparseArrays = "1.10" SparseConnectivityTracer = "1" diff --git a/lib/NonlinearSolveBase/Project.toml b/lib/NonlinearSolveBase/Project.toml index 27c5166ae..29d41920e 100644 --- a/lib/NonlinearSolveBase/Project.toml +++ b/lib/NonlinearSolveBase/Project.toml @@ -82,7 +82,7 @@ Preferences = "1.4" Printf = "1.10" RecursiveArrayTools = "3" ReverseDiff = "1.15" -SciMLBase = "2.116" +SciMLBase = "2.127" SciMLJacobianOperators = "0.1.1" SciMLLogging = "1.3.1" SciMLOperators = "1.7" diff --git a/lib/NonlinearSolveSciPy/Project.toml b/lib/NonlinearSolveSciPy/Project.toml index 1fcc7f2af..2a35e8638 100644 --- a/lib/NonlinearSolveSciPy/Project.toml +++ b/lib/NonlinearSolveSciPy/Project.toml @@ -25,7 +25,7 @@ PrecompileTools = "1.2" PythonCall = "0.9" ReTestItems = "1.24" Reexport = "1.2.2" -SciMLBase = "2.116" +SciMLBase = "2.127" Test = "1.10" julia = "1.10"