From a483f6c0fde64b0b7ad8952422d255b5cf7d51c8 Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 04:54:45 +0300 Subject: [PATCH 1/7] CI: scope CompatHelper GITHUB_TOKEN to least privilege Add an explicit permissions block (contents: write, pull-requests: write) to CompatHelper.yml, narrowing this workflow's token from the repo-wide write default. Complements enabling the repo policy 'Allow GitHub Actions to create and approve pull requests' (Settings > Actions > General), which was the actual fix for the recurring CompatHelper 403 on PR creation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/CompatHelper.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 2eaa139..bd79fbc 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -3,6 +3,14 @@ on: schedule: - cron: 0 0 * * * workflow_dispatch: +# Least-privilege: scope this workflow's GITHUB_TOKEN to exactly what +# CompatHelper needs (read the repo, open the compat PR) instead of the +# repo-wide `write` default. PR *creation* is additionally gated by the +# repo setting "Allow GitHub Actions to create and approve pull requests" +# (Settings > Actions > General > Workflow permissions), enabled 2026-06-09. +permissions: + contents: write + pull-requests: write jobs: CompatHelper: runs-on: ubuntu-latest From 3728d840609bd21f28a86ff2a4680955a1d61d14 Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 05:02:41 +0300 Subject: [PATCH 2/7] Compat: allow SciMLBase 3.x Widen SciMLBase from "2.53" to "2.53, 3". CI-verified compatible: the full 262-test suite plus docs build pass against SciMLBase 3.x across Julia 1.10/1.12/pre on x64+x86 (CompatHelper PR #21). The package's SciMLBase usage is limited to stable API (NonlinearProblem, isinplace, ReturnCode, NLStats, build_solution, __solve), unaffected by the 2.x -> 3.x major bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 767038e..14fe618 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" [compat] CommonSolve = "0.2" LineSearch = "0.1" -SciMLBase = "2.53" +SciMLBase = "2.53, 3" julia = "1.10" [extras] From f43da65bab2985a7907f9a89360350150560cc0b Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 05:02:41 +0300 Subject: [PATCH 3/7] CI: drop Dependabot julia ecosystem in favor of CompatHelper CompatHelper and Dependabot's julia ecosystem were both managing Julia dependencies, producing duplicate PRs (e.g. the same SciMLBase bump as #16 and #21). Standardize on CompatHelper (registry-aware, the Julia-ecosystem norm). Keep the github-actions ecosystem, which CompatHelper does not cover. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/dependabot.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f06744..d0bb828 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,15 +10,7 @@ updates: # github-actions: # patterns: # - "*" - - package-ecosystem: "julia" - directories: - - "/" - - "/docs" - - "/test" - schedule: - interval: "weekly" - # To group all Julia dependency updates into a single PR, uncomment the following: - # groups: - # julia-dependencies: - # patterns: - # - "*" + # Julia dependency updates are handled by CompatHelper + # (.github/workflows/CompatHelper.yml) — the registry-aware Julia-ecosystem + # standard — so a "julia" package-ecosystem is intentionally NOT declared here + # to avoid duplicate PRs. Do not re-add one. From 0c61027601fbb9167dbfc3394e30b3ed9a59381b Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 05:16:07 +0300 Subject: [PATCH 4/7] Changelog: open [Unreleased] with SciMLBase 3.x compat widening Records the SciMLBase = "2.53, 3" widening (commit 3728d84) as the entry-in-waiting for the next release. CI/dependabot hygiene commits are intentionally omitted from the package changelog (dev-infra, not user-facing). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7a018..f00a75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to **DFMethods.jl** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **`SciMLBase` compatibility widened to allow 3.x** — `[compat]` bound + `SciMLBase = "2.53"` → `"2.53, 3"`. The package's SciMLBase surface is + limited to stable API (`NonlinearProblem`, `isinplace`, `ReturnCode`, + `NLStats`, `build_solution`, `__solve`) and is unaffected by the + 2.x → 3.x major bump; the full test suite plus docs build pass against + SciMLBase 3.x across Julia 1.10 / 1.12 / pre on x64 + x86. The v2.x + range is retained, so this is a drop-in widening for existing v0.3.2 + user code. + +### Compatibility + +- Julia ≥ 1.10 (unchanged). +- `SciMLBase` v2.53+ **or** v3.x (widened this cycle; previously v2.x only). +- `CommonSolve` v0.2.x, `LineSearch` v0.1.x (unchanged). + ## [0.3.2] — 2026-05-25 ### Added @@ -360,6 +379,7 @@ for constrained nonlinear equations $F(x) = 0$ on a closed convex set $X$. - `SciMLBase` v2.x - `CommonSolve` v0.2.x +[Unreleased]: https://github.com/mmogib/DFMethods.jl/compare/v0.3.2...HEAD [0.3.2]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.2 [0.3.1]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.1 [0.3.0]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.0 From 6cd8521f834220edecd6dbb73c71a77f79f05b2c Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 06:21:25 +0300 Subject: [PATCH 5/7] SciML: route reltol/maxtime/verbose through solve; absorb all common kwargs DFProjection now honors the full set of SciMLBase common solver options that map onto its callback-based stopping system, closing the gap where reltol (and others) were silently ignored: - reltol -> RelResidualTol (new DFProjection field reltol::Float64 = 0.0) - maxtime -> MaxTime (new DFProjection field maxtime::Float64 = Inf) - verbose -> warns on non-Success exit unless false (SciML convention) - abstol/maxiters unchanged All other standard keywords (termination_condition, internalnorm, alias_u0, show_trace, store_trace, trace_level) are accepted and absorbed without error. An internal auto_stopping flag preserves an explicit stopping= rule across solve-time overrides (warns rather than clobbers). Default stopping is byte-identical for code that sets neither reltol nor maxtime. 262 -> 291 tests (new SciML keyword-routing testset). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 24 ++++++ src/algorithm.jl | 61 +++++++++++---- src/nonlinearsolve_integration.jl | 126 ++++++++++++++++++++++++------ test/runtests.jl | 109 ++++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00a75d..0c3d9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Full SciML common-solver keyword support in `solve` / `init`.** + `DFProjection` now honors the standard SciMLBase nonlinear-solve + keywords that map onto its callback-based stopping system (previously + only `abstol` and `maxiters` were honored; `reltol` and others were + silently ignored): + - `reltol` → `RelResidualTol` (`‖F(z_k)‖ ≤ reltol·‖F(x_0)‖`). New + `DFProjection` field `reltol::Float64 = 0.0` (`0` disables). + - `maxtime` → `MaxTime` (wall-clock seconds). New `DFProjection` field + `maxtime::Float64 = Inf` (`Inf` / `nothing` disables). + - `abstol` → `AbsResidualTol`, `maxiters` → `MaxIters` (unchanged). + Each defaults to the matching `DFProjection` field and is overridden + per-`solve`. All other standard keywords (`termination_condition`, + `internalnorm`, `alias_u0`, `show_trace`, `store_trace`, `trace_level`) + are accepted and absorbed without error, so + `solve(prob, ::DFProjection; any_standard_kwarg…)` never throws. The + default stopping rule is unchanged for code that doesn't set `reltol` / + `maxtime` (still `AnyOf(AbsResidualTol(abstol), MaxIters(maxiters))`). + ### Changed +- **`solve` warns on non-convergence (SciML convention).** A non-`Success` + (early) exit now emits a warning unless `verbose = false` is passed. + Earlier releases never warned; solver results are unchanged — only + diagnostic output differs. - **`SciMLBase` compatibility widened to allow 3.x** — `[compat]` bound `SciMLBase = "2.53"` → `"2.53, 3"`. The package's SciMLBase surface is limited to stable API (`NonlinearProblem`, `isinplace`, `ReturnCode`, diff --git a/src/algorithm.jl b/src/algorithm.jl index e201c64..7c52f74 100644 --- a/src/algorithm.jl +++ b/src/algorithm.jl @@ -18,8 +18,8 @@ """ DFProjection(; direction, linesearch, inertial, iterate_update, - abstol, maxiters, stopping, ζ, inner_maxiter, maxbt, - callbacks) + abstol, reltol, maxiters, maxtime, stopping, ζ, + inner_maxiter, maxbt, callbacks) Concrete derivative-free projection algorithm with pluggable components. The constraint set lives on the **problem**, not on the algorithm — see @@ -31,21 +31,26 @@ The constraint set lives on the **problem**, not on the algorithm — see - `linesearch::LineSearch.AbstractLineSearchAlgorithm` — line search. Default: `ResidualNormBacktrack()`. - `inertial::AbstractInertialRule` — inertial rule. Default: `Inertial(0.25)`. - `iterate_update::AbstractIterateUpdate` — post-line-search iterate update strategy. Default: `SolodovSvaiterProjection()`. Alternatives: `DirectUpdate()`, `HalpernUpdate(β)`. -- `abstol::Float64` — residual tolerance used to build the default stopping. Default: `1e-6`. +- `abstol::Float64` — absolute residual tolerance used to build the default stopping. Default: `1e-6`. +- `reltol::Float64` — relative residual tolerance (target `‖F(z_k)‖ ≤ reltol·‖F(x_0)‖`) used to build the default stopping. `0` disables it. Default: `0.0`. - `maxiters::Int` — outer-iteration cap used to build the default stopping. Default: `2000`. -- `stopping::AbstractStoppingCriterion` — full stopping rule. If not supplied, built as `AnyOf(AbsResidualTol(abstol), MaxIters(maxiters))`. +- `maxtime::Float64` — wall-clock budget in seconds used to build the default stopping. `Inf` disables it. Default: `Inf`. +- `stopping::AbstractStoppingCriterion` — full stopping rule. If not supplied, built from the knobs above as `AnyOf(AbsResidualTol(abstol)[, RelResidualTol(reltol)], MaxIters(maxiters)[, MaxTime(maxtime)])` — the bracketed criteria appear only when `reltol > 0` / `maxtime` is finite. - `ζ::Float64` — approximate-projection tolerance factor used by `SolodovSvaiterProjection`. Default: `0.5`. - `inner_maxiter::Int` — max inner-projection iterations (Dykstra). Default: `500`. - `maxbt::Int` — max line-search backtracks per iteration. Default: `50`. - `callbacks::Vector{<:AbstractCallback}` — observer callbacks fired during the solve. Default: empty. # Stopping criteria -`abstol` and `maxiters` are convenience knobs that build the default -stopping rule. For composite or domain-specific criteria, pass -`stopping = AnyOf(RelResidualTol(...), StepNormTol(...), MaxTime(...), …)`; -the `abstol`/`maxiters` fields are still stored (for introspection and -SciMLBase kwarg overrides) but `step!` ignores them in favour of the -supplied `stopping`. +`abstol`, `reltol`, `maxiters`, and `maxtime` are convenience knobs that +build the default stopping rule (mirroring the SciML common-solver +options of the same name). For composite or domain-specific criteria, +pass `stopping = AnyOf(StepNormTol(...), DirectionNormTol(...), …)`; the +knob fields are still stored (for introspection and SciMLBase kwarg +overrides) but `step!` ignores them in favour of the supplied `stopping`. +When a `stopping` rule is supplied explicitly, `solve`-time tolerance +keywords (`abstol`/`reltol`/`maxiters`/`maxtime`) cannot be re-applied and +are reported via a `verbose` warning. """ struct DFProjection{Dir<:AbstractSearchDirection, LS<:LineSearch.AbstractLineSearchAlgorithm, @@ -57,8 +62,11 @@ struct DFProjection{Dir<:AbstractSearchDirection, linesearch::LS inertial::In abstol::Float64 + reltol::Float64 maxiters::Int + maxtime::Float64 stopping::Stop + auto_stopping::Bool ζ::Float64 inner_maxiter::Int maxbt::Int @@ -71,7 +79,9 @@ function DFProjection(; linesearch = ResidualNormBacktrack(), inertial = Inertial(0.25), abstol::Real = 1e-6, + reltol::Real = 0.0, maxiters::Int = 2000, + maxtime::Real = Inf, stopping::Union{Nothing, AbstractStoppingCriterion} = nothing, ζ::Real = 0.5, inner_maxiter::Int = 500, @@ -79,15 +89,38 @@ function DFProjection(; iterate_update = SolodovSvaiterProjection(), callbacks::Vector{<:AbstractCallback} = AbstractCallback[], ) - stop = stopping === nothing ? - AnyOf(AbsResidualTol(Float64(abstol)), MaxIters(maxiters)) : - stopping + auto = stopping === nothing + stop = auto ? _default_stopping(abstol, reltol, maxiters, maxtime) : stopping return DFProjection(direction, linesearch, inertial, - Float64(abstol), maxiters, stop, + Float64(abstol), Float64(reltol), maxiters, Float64(maxtime), + stop, auto, Float64(ζ), inner_maxiter, maxbt, iterate_update, callbacks) end +# Build the default stopping rule from the convenience tolerance/budget +# knobs. `RelResidualTol` is added only when `reltol > 0` and `MaxTime` +# only when `maxtime` is finite, so the common cases `DFProjection()` and +# `DFProjection(; abstol, maxiters)` produce exactly +# `AnyOf(AbsResidualTol(abstol), MaxIters(maxiters))` — byte-identical to +# earlier releases (no behavior change for code that doesn't set reltol / +# maxtime). +function _default_stopping(abstol::Real, reltol::Real, maxiters::Integer, maxtime::Real) + abs_c = AbsResidualTol(Float64(abstol)) + iter_c = MaxIters(Int(maxiters)) + use_rel = reltol > 0 + use_time = isfinite(maxtime) + if use_rel && use_time + return AnyOf(abs_c, RelResidualTol(Float64(reltol)), iter_c, MaxTime(Float64(maxtime))) + elseif use_rel + return AnyOf(abs_c, RelResidualTol(Float64(reltol)), iter_c) + elseif use_time + return AnyOf(abs_c, iter_c, MaxTime(Float64(maxtime))) + else + return AnyOf(abs_c, iter_c) + end +end + # ============================================================================ # DFProjectionCache: mutable state for one solve # ============================================================================ diff --git a/src/nonlinearsolve_integration.jl b/src/nonlinearsolve_integration.jl index b064402..11f5e51 100644 --- a/src/nonlinearsolve_integration.jl +++ b/src/nonlinearsolve_integration.jl @@ -112,16 +112,44 @@ end # Effective algorithm with SciML kwarg overrides # ============================================================================ -function _alg_with_overrides(alg::DFProjection, abstol::Real, maxiters::Int) - if abstol == alg.abstol && maxiters == alg.maxiters +function _alg_with_overrides(alg::DFProjection, abstol::Real, reltol::Real, + maxiters::Integer, maxtime::Real; verbose::Bool = true) + if abstol == alg.abstol && reltol == alg.reltol && + maxiters == alg.maxiters && maxtime == alg.maxtime return alg end + if !alg.auto_stopping + # The user supplied an explicit `stopping` rule; it governs + # termination. Folding tolerance overrides into it would silently + # override the user's intent, so keep the rule as-is and warn. + verbose && @warn("DFProjection: a custom `stopping` criterion is set, so the " * + "solve-time tolerance/budget keyword(s) are ignored. " * + "Encode tolerances directly in the `stopping` rule.") + return DFProjection(; + direction = alg.direction, + linesearch = alg.linesearch, + inertial = alg.inertial, + abstol = Float64(abstol), + reltol = Float64(reltol), + maxiters = Int(maxiters), + maxtime = Float64(maxtime), + stopping = alg.stopping, # preserve the custom rule + ζ = alg.ζ, + inner_maxiter = alg.inner_maxiter, + maxbt = alg.maxbt, + iterate_update = alg.iterate_update, + callbacks = alg.callbacks, + ) + end + # Default knob-built stopping: rebuild it from the overridden knobs. return DFProjection(; direction = alg.direction, linesearch = alg.linesearch, inertial = alg.inertial, abstol = Float64(abstol), - maxiters = maxiters, + reltol = Float64(reltol), + maxiters = Int(maxiters), + maxtime = Float64(maxtime), ζ = alg.ζ, inner_maxiter = alg.inner_maxiter, maxbt = alg.maxbt, @@ -138,14 +166,17 @@ end DFSciMLCache Wrapper cache returned by `CommonSolve.init(prob, alg; …)`. Carries -the original `NonlinearProblem`, the user-facing algorithm, and the -inner Phase 2 `DFProjectionCache`. `CommonSolve.solve!` drives the inner -cache to termination and packages the result as a `NonlinearSolution`. +the original `NonlinearProblem`, the user-facing algorithm, the inner +Phase 2 `DFProjectionCache`, and the resolved `verbose` flag (used by +`solve!` to decide whether to warn on a non-`Success` exit). +`CommonSolve.solve!` drives the inner cache to termination and packages +the result as a `NonlinearSolution`. """ mutable struct DFSciMLCache{Prob, Alg<:DFProjection, Inner<:DFProjectionCache} prob::Prob alg::Alg inner::Inner + verbose::Bool end # ============================================================================ @@ -153,28 +184,65 @@ end # ============================================================================ """ - CommonSolve.init(prob::NonlinearProblem, alg::DFProjection; abstol, maxiters, kwargs...) + CommonSolve.init(prob::NonlinearProblem, alg::DFProjection; + abstol, reltol, maxiters, maxtime, verbose, kwargs...) -> DFSciMLCache -Build a cache for `solve(prob, alg; …)`. Accepts SciML's standard -`abstol` and `maxiters` kwargs (overriding `alg.abstol` / `alg.maxiters`); -other kwargs are absorbed without effect (Phase 3 polish: route -`verbose`, `callback`, etc.). +Build a cache for `solve(prob, alg; …)`. Honors the SciML common-solver +options that map onto DFMethods' callback-based stopping system: + +- `abstol` → `AbsResidualTol(abstol)` +- `reltol` → `RelResidualTol(reltol)` (target `‖F(z_k)‖ ≤ reltol·‖F(x_0)‖`) +- `maxiters` → `MaxIters(maxiters)` +- `maxtime` → `MaxTime(maxtime)` seconds (`nothing` ⇒ no limit) +- `verbose` → toggles the non-convergence warning emitted by `solve!` + +Each defaults to the matching field on `alg`; any that is supplied +overrides it by rebuilding the default stopping rule. If `alg` was built +with an explicit `stopping=`, these tolerance keywords cannot be applied +and (when `verbose`) a warning is emitted. + +All other SciML common-solver keywords — `termination_condition`, +`internalnorm`, `alias_u0`, `show_trace`, `store_trace`, `trace_level` — +are accepted and absorbed without error. DFMethods uses its own +callback-based termination and the Euclidean residual norm, so +`solve(prob, ::DFProjection; any_standard_kwarg…)` never throws. """ function CommonSolve.init(prob::Union{SciMLBase.NonlinearProblem, ConstrainedNonlinearProblem}, alg::DFProjection; - abstol::Real = alg.abstol, - maxiters::Int = alg.maxiters, + abstol = nothing, + reltol = nothing, + maxiters = nothing, + maxtime = nothing, + verbose = true, kwargs...) - F = _wrap_problem_F(prob) + F = _wrap_problem_F(prob) # Preserve eltype of u0; init_cache derives T from this (with Int → Float64 # fallback). The foundational change for end-to-end T-genericity in v0.3.0. - x0 = collect(_problem_u0(prob)) - set = _constraint_set(prob) - alg_eff = _alg_with_overrides(alg, abstol, maxiters) - inner = init_cache(F, x0, alg_eff; set = set) - return DFSciMLCache(prob, alg, inner) + x0 = collect(_problem_u0(prob)) + set = _constraint_set(prob) + + # Resolve effective SciML common-solver options. A `nothing` sentinel + # means "not supplied by the caller" → fall back to the algorithm's + # stored value. `maxtime === nothing` doubles as SciML's "no time limit". + eff_abstol = abstol === nothing ? alg.abstol : Float64(abstol) + eff_reltol = reltol === nothing ? alg.reltol : Float64(reltol) + eff_maxiters = maxiters === nothing ? alg.maxiters : Int(maxiters) + eff_maxtime = maxtime === nothing ? alg.maxtime : Float64(maxtime) + # `verbose` is a Bool in the common interface; tolerate (and ignore) + # any other type a native-solver caller might pass, never throwing. + vflag = verbose isa Bool ? verbose : true + + any_override = !(abstol === nothing && reltol === nothing && + maxiters === nothing && maxtime === nothing) + alg_eff = any_override ? + _alg_with_overrides(alg, eff_abstol, eff_reltol, eff_maxiters, eff_maxtime; + verbose = vflag) : + alg + + inner = init_cache(F, x0, alg_eff; set = set) + return DFSciMLCache(prob, alg, inner, vflag) end # ============================================================================ @@ -203,14 +271,26 @@ function CommonSolve.solve!(cache::DFSciMLCache) inner.resid = sqrt(s) inner.n_evals += 1 end - # Fire :terminate (observers see final state; stopping criteria ignore). - _fire!(inner, cache.alg, :terminate) + # Fire :terminate on the *effective* algorithm — `inner.alg` already + # carries any solve-time overrides (observers see the final state; + # stopping criteria no-op at :terminate). + _fire!(inner, inner.alg, :terminate) resid_vec = inner.F(inner.x) - stats = SciMLBase.NLStats(inner.n_evals + 1, 0, 0, 0, inner.k) + retcode = _to_sciml_retcode(inner.retcode) + stats = SciMLBase.NLStats(inner.n_evals + 1, 0, 0, 0, inner.k) + + # SciML convention: warn on a non-`Success` (early) exit unless the + # caller passed `verbose = false`. The residual is included so a + # near-zero degenerate exit is self-evidently benign to the reader. + if cache.verbose && retcode !== SciMLBase.ReturnCode.Success + @warn("DFProjection did not converge to the requested tolerance.", + retcode = inner.retcode, residual = inner.resid, iterations = inner.k) + end + return SciMLBase.build_solution(_inner_problem(cache.prob), cache.alg, inner.x, resid_vec; - retcode = _to_sciml_retcode(inner.retcode), + retcode = retcode, stats = stats) end diff --git a/test/runtests.jl b/test/runtests.jl index 3cda2a7..bf75340 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1141,6 +1141,115 @@ end @test alg.maxiters == 10 # but unused by step! end + @testset "SciML common-solver keyword routing" begin + # Authoritative option list: + # https://docs.sciml.ai/NonlinearSolve/stable/basics/solve/ + + @testset "constructor reltol/maxtime build the default stopping" begin + a = DFProjection(; reltol = 1e-3) + @test a.reltol == 1e-3 + @test a.auto_stopping + @test any(c isa RelResidualTol && c.rtol == 1e-3 for c in a.stopping.criteria) + + b = DFProjection(; maxtime = 5.0) + @test b.maxtime == 5.0 + @test any(c isa MaxTime && c.maxtime == 5.0 for c in b.stopping.criteria) + + # Default omits both → stays AnyOf(AbsResidualTol, MaxIters). + d = DFProjection() + @test d.reltol == 0.0 + @test d.maxtime == Inf + @test !any(c isa RelResidualTol for c in d.stopping.criteria) + @test !any(c isa MaxTime for c in d.stopping.criteria) + end + + @testset "solve keywords rebuild the effective stopping" begin + f(u, p) = copy(u) + prob = SciMLBase.NonlinearProblem(f, [1.0, 1.0]) + base = DFProjection() + + c1 = init(prob, base; reltol = 1e-3) + @test c1.inner.alg.reltol == 1e-3 + @test any(c isa RelResidualTol && c.rtol == 1e-3 + for c in c1.inner.alg.stopping.criteria) + + c2 = init(prob, base; maxtime = 2.0) + @test c2.inner.alg.maxtime == 2.0 + @test any(c isa MaxTime && c.maxtime == 2.0 + for c in c2.inner.alg.stopping.criteria) + + c3 = init(prob, base; abstol = 1e-9, maxiters = 321) + @test c3.inner.alg.abstol == 1e-9 + @test c3.inner.alg.maxiters == 321 + + # No tol kwargs → no rebuild (same alg object flows through). + @test init(prob, base).inner.alg === base + end + + @testset "reltol drives convergence (behavioral)" begin + F2(u, p) = u .- p + x0 = [1.0, -1.0]; target = [0.3, -0.2] + prob = SciMLBase.NonlinearProblem(F2, x0, target) + alg = DFProjection(; inertial = NoInertial()) + # Absurdly tight abstol so only the relative test can stop us. + sol = solve(prob, alg; abstol = 1e-14, reltol = 1e-2, maxiters = 2000) + @test sol.retcode == SciMLBase.ReturnCode.Success + @test norm(sol.resid) <= 1e-2 * norm(x0 .- target) + 1e-12 + end + + @testset "maxtime stops the solve (behavioral)" begin + F4(u, p) = u .- sin.(u) .- 1.0 + prob = SciMLBase.NonlinearProblem(F4, ones(20)) + alg = DFProjection(; inertial = NoInertial()) + # Zero time budget → first :post_iter MaxTime check fires. + sol = solve(prob, alg; maxtime = 0.0, abstol = 1e-12, verbose = false) + @test sol.retcode == SciMLBase.ReturnCode.Terminated + end + + @testset "all standard SciML kwargs absorbed without error" begin + f(u, p) = copy(u) + prob = SciMLBase.NonlinearProblem(f, [0.5, 0.5]) + sol = solve(prob, DFProjection(; maxiters = 200); + termination_condition = nothing, + internalnorm = norm, + alias_u0 = false, + show_trace = Val(false), + store_trace = Val(false), + trace_level = nothing) + @test sol isa SciMLBase.AbstractNonlinearSolution + end + + @testset "verbose toggles the non-convergence warning" begin + # Nonlinear F (not solved in a single projection step) so + # maxiters=1 yields a genuine non-Success MaxIters exit. + F5(u, p) = u .- sin.(u) .- 1.0 + prob = SciMLBase.NonlinearProblem(F5, ones(8)) + alg = DFProjection(; maxiters = 1) + + @test init(prob, alg; verbose = false).verbose == false + @test init(prob, alg).verbose == true + + # Default verbose=true → a non-Success exit warns. + sol = @test_logs (:warn,) match_mode = :any solve(prob, alg) + @test sol.retcode != SciMLBase.ReturnCode.Success + # verbose=false → no warn-level log. + @test_logs match_mode = :all min_level = Base.CoreLogging.Warn solve(prob, alg; verbose = false) + end + + @testset "custom stopping + tol override warns, preserves rule" begin + f(u, p) = copy(u) + prob = SciMLBase.NonlinearProblem(f, [1.0, 1.0]) + custom = AnyOf(AbsResidualTol(1e-4), MaxIters(50)) + alg = DFProjection(; stopping = custom) + @test !alg.auto_stopping + + c = @test_logs (:warn,) match_mode = :any init(prob, alg; reltol = 1e-3) + @test c.inner.alg.stopping === custom # custom rule preserved + # verbose=false silences the conflict warning. + @test_logs match_mode = :all min_level = Base.CoreLogging.Warn init(prob, alg; reltol = 1e-3, verbose = false) + end + end + @testset "end-to-end: RelResidualTol converges" begin F2(u, p) = u .- p target = [0.3, -0.2] From 59ddde4996c4193e4cda53e9ed01f11415ffd6e3 Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 06:21:26 +0300 Subject: [PATCH 6/7] Docs: verbose=false on @example solves that intentionally don't converge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit solve() now warns on a non-Success exit (SciML convention), so three @example blocks demonstrating early-exit retcodes would render a stray warning. The retcode is shown programmatically, so the warning adds nothing -- pass verbose=false on those: - quickstart.md : the maxiters=2 MaxIters demo - tutorial.md §4 : the MaxTime(0.05) capped-budget demo - tutorial.md §6 : the comparative sweep Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/src/quickstart.md | 2 +- docs/src/tutorial.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md index 67a9e28..9004952 100644 --- a/docs/src/quickstart.md +++ b/docs/src/quickstart.md @@ -132,7 +132,7 @@ Custom observers subtype [`AbstractCallback`](@ref) and add a method on ```@example quick using SciMLBase # for ReturnCode -sol_short = solve(prob, DFProjection(); maxiters = 2) +sol_short = solve(prob, DFProjection(); maxiters = 2, verbose = false) # we expect (and inspect) a non-Success retcode sol_short.retcode == ReturnCode.MaxIters ``` diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index eb99fd3..fae9386 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -161,7 +161,7 @@ alg_capped = DFProjection(; MaxIters(10_000), MaxTime(0.05)), # 50 ms ) -wall = @elapsed sol = solve(prob, alg_capped) +wall = @elapsed sol = solve(prob, alg_capped; verbose = false) # MaxTime is the expected exit here (retcode = sol.retcode, iters = sol.stats.nsteps, wallclock_seconds = round(wall; digits = 3)) ``` @@ -260,7 +260,7 @@ for (pname, n_p, F_p) in problems x0_p = ones(n_p) for (lname, ls) in line_searches sol = solve(NonlinearProblem(F_p, x0_p), - DFProjection(; linesearch = ls)) + DFProjection(; linesearch = ls); verbose = false) # some configs may not converge; we record it push!(results, ( problem = pname, linesearch = lname, From df66097dde171017bd023d6375c6d84291614738 Mon Sep 17 00:00:00 2001 From: Mohammed Alshahrani Date: Tue, 9 Jun 2026 06:25:30 +0300 Subject: [PATCH 7/7] Release v0.3.3 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++-- CITATION.cff | 4 ++-- Project.toml | 2 +- README.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3d9c5..7f7d4e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to **DFMethods.jl** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.3.3] — 2026-06-09 ### Added @@ -403,7 +403,7 @@ for constrained nonlinear equations $F(x) = 0$ on a closed convex set $X$. - `SciMLBase` v2.x - `CommonSolve` v0.2.x -[Unreleased]: https://github.com/mmogib/DFMethods.jl/compare/v0.3.2...HEAD +[0.3.3]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.3 [0.3.2]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.2 [0.3.1]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.1 [0.3.0]: https://github.com/mmogib/DFMethods.jl/releases/tag/v0.3.0 diff --git a/CITATION.cff b/CITATION.cff index 36d4bdc..9d749d2 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -9,8 +9,8 @@ authors: orcid: "https://orcid.org/0000-0002-1367-646X" email: mshahrani@kfupm.edu.sa affiliation: "King Fahd University of Petroleum and Minerals" -version: 0.3.2 -date-released: "2026-05-25" +version: 0.3.3 +date-released: "2026-06-09" repository-code: "https://github.com/mmogib/DFMethods.jl" url: "https://mmogib.github.io/DFMethods.jl/stable/" license: MIT diff --git a/Project.toml b/Project.toml index 14fe618..e8c8055 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DFMethods" uuid = "5fd0b45f-fdf3-4567-a8b1-5e033765ff5d" authors = ["Mohammed Alshahrani "] -version = "0.3.2" +version = "0.3.3" [deps] CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" diff --git a/README.md b/README.md index 8281ca8..d3d3936 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ If you use DFMethods.jl in research, please cite it via the Zenodo DOI: @software{Alshahrani_DFMethods_2026, author = {Alshahrani, Mohammed}, title = {{DFMethods.jl: A configurable framework for derivative-free projection methods for constrained nonlinear equations}}, - month = may, + month = jun, year = 2026, publisher = {Zenodo}, - version = {v0.3.2}, + version = {v0.3.3}, doi = {10.5281/zenodo.20350220}, url = {https://doi.org/10.5281/zenodo.20350220} }