diff --git a/.github/workflows/CI_SCCNonlinearSolve.yml b/.github/workflows/CI_SCCNonlinearSolve.yml new file mode 100644 index 000000000..a00e938e6 --- /dev/null +++ b/.github/workflows/CI_SCCNonlinearSolve.yml @@ -0,0 +1,109 @@ +name: CI (SCCNonlinearSolve) + +on: + pull_request: + branches: + - master + paths: + - "lib/SCCNonlinearSolve/**" + - ".github/workflows/CI_SCCNonlinearSolve.yml" + - "lib/NonlinearSolveBase/**" + - "lib/SciMLJacobianOperators/**" + push: + branches: + - master + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - "1.10" + - "1" + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + - uses: actions/cache@v4 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - name: "Install Dependencies and Run Tests" + run: | + import Pkg + Pkg.Registry.update() + # Install packages present in subdirectories + dev_pks = Pkg.PackageSpec[] + for path in ("lib/SciMLJacobianOperators", "lib/NonlinearSolveBase") + push!(dev_pks, Pkg.PackageSpec(; path)) + end + Pkg.develop(dev_pks) + Pkg.instantiate() + Pkg.test(; coverage="user") + shell: julia --color=yes --code-coverage=user --depwarn=yes --project=lib/SCCNonlinearSolve {0} + - uses: julia-actions/julia-processcoverage@v1 + with: + directories: lib/SCCNonlinearSolve/src,lib/NonlinearSolveBase/src,lib/NonlinearSolveBase/ext,lib/SciMLJacobianOperators/src + - uses: codecov/codecov-action@v4 + with: + file: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + fail_ci_if_error: false + + downgrade: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: + - "1.10" + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + - uses: julia-actions/julia-downgrade-compat@v1 + with: + skip: NonlinearSolveBase, SciMLJacobianOperators + - name: "Install Dependencies and Run Tests" + run: | + import Pkg + Pkg.Registry.update() + # Install packages present in subdirectories + dev_pks = Pkg.PackageSpec[] + for path in ("lib/SciMLJacobianOperators", "lib/NonlinearSolveBase") + push!(dev_pks, Pkg.PackageSpec(; path)) + end + Pkg.develop(dev_pks) + Pkg.instantiate() + Pkg.test(; coverage="user") + shell: julia --color=yes --code-coverage=user --depwarn=yes --project=lib/SCCNonlinearSolve {0} + - uses: julia-actions/julia-processcoverage@v1 + with: + directories: lib/SCCNonlinearSolve/src,lib/NonlinearSolveBase/src,lib/NonlinearSolveBase/ext,lib/SciMLJacobianOperators/src + - uses: codecov/codecov-action@v4 + with: + file: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + fail_ci_if_error: false diff --git a/lib/SCCNonlinearSolve/LICENSE b/lib/SCCNonlinearSolve/LICENSE new file mode 100644 index 000000000..abb594d1e --- /dev/null +++ b/lib/SCCNonlinearSolve/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 SciML + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/SCCNonlinearSolve/Project.toml b/lib/SCCNonlinearSolve/Project.toml new file mode 100644 index 000000000..389323487 --- /dev/null +++ b/lib/SCCNonlinearSolve/Project.toml @@ -0,0 +1,48 @@ +name = "SCCNonlinearSolve" +uuid = "9dfe8606-65a1-4bb3-9748-cb89d1561431" +authors = ["Avik Pal and contributors"] +version = "1.0.0" + +[deps] +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" + +[compat] +Aqua = "0.8" +BenchmarkTools = "1.5.0" +CommonSolve = "0.2.4" +ExplicitImports = "1.5" +Hwloc = "3" +InteractiveUtils = "<0.0.1, 1" +NonlinearProblemLibrary = "0.1.2" +NonlinearSolveFirstOrder = "1" +Pkg = "1.10" +PrecompileTools = "1.2" +ReTestItems = "1.24" +Reexport = "1" +SciMLBase = "2.60" +StableRNGs = "1" +StaticArrays = "1.9.8" +SymbolicIndexingInterface = "0.3.30" +Test = "1.10" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" +Hwloc = "0e44f5e4-bd66-52a0-8798-143a42290a1d" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +NonlinearSolveFirstOrder = "5959db7a-ea39-4486-b5fe-2dd0bf03d60d" +NonlinearProblemLibrary = "b7050fa9-e91f-4b37-bcee-a89a063da141" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "BenchmarkTools", "ExplicitImports", "Hwloc", "InteractiveUtils", "NonlinearSolveFirstOrder", "NonlinearProblemLibrary", "Pkg", "ReTestItems", "StableRNGs", "StaticArrays", "Test"] diff --git a/lib/SCCNonlinearSolve/src/SCCNonlinearSolve.jl b/lib/SCCNonlinearSolve/src/SCCNonlinearSolve.jl new file mode 100644 index 000000000..3c8cc8691 --- /dev/null +++ b/lib/SCCNonlinearSolve/src/SCCNonlinearSolve.jl @@ -0,0 +1,38 @@ +module SCCNonlinearSolve + +import SciMLBase +import CommonSolve +import SymbolicIndexingInterface + +function CommonSolve.solve(prob::SciMLBase.SCCNonlinearProblem, alg; kwargs...) + numscc = length(prob.probs) + sols = [SciMLBase.build_solution( + prob, nothing, prob.u0, convert(eltype(prob.u0), NaN) * prob.u0) + for prob in prob.probs] + u = reduce(vcat, [prob.u0 for prob in prob.probs]) + resid = copy(u) + + lasti = 1 + for i in 1:numscc + prob.explictfuns![i]( + SymbolicIndexingInterface.parameter_values(prob.probs[i]), sols) + sol = SciMLBase.solve(prob.probs[i], alg; kwargs...) + _sol = SciMLBase.build_solution( + prob.probs[i], nothing, sol.u, sol.resid, retcode = sol.retcode) + sols[i] = _sol + lasti = i + if !SciMLBase.successful_retcode(_sol) + break + end + end + + # TODO: fix allocations with a lazy concatenation + u .= reduce(vcat, sols) + resid .= reduce(vcat, getproperty.(sols, :resid)) + + retcode = sols[lasti].retcode + + SciMLBase.build_solution(prob, alg, u, resid; retcode, original = sols) +end + +end diff --git a/lib/SCCNonlinearSolve/test/core_tests.jl b/lib/SCCNonlinearSolve/test/core_tests.jl new file mode 100644 index 000000000..92843421d --- /dev/null +++ b/lib/SCCNonlinearSolve/test/core_tests.jl @@ -0,0 +1,65 @@ +@testsetup module CoreRootfindTesting + +include("../../../common/common_rootfind_testing.jl") + +end + +@testitem "Manual SCC" setup=[CoreRootfindTesting] tags=[:core] begin + using NonlinearSolveFirstOrder + function f(du, u, p) + du[1] = cos(u[2]) - u[1] + du[2] = sin(u[1] + u[2]) + u[2] + du[3] = 2u[4] + u[3] + 1.0 + du[4] = u[5]^2 + u[4] + du[5] = u[3]^2 + u[5] + du[6] = u[1] + u[2] + u[3] + u[4] + u[5] + 2.0u[6] + 2.5u[7] + 1.5u[8] + du[7] = u[1] + u[2] + u[3] + 2.0u[4] + u[5] + 4.0u[6] - 1.5u[7] + 1.5u[8] + du[8] = u[1] + 2.0u[2] + 3.0u[3] + 5.0u[4] + 6.0u[5] + u[6] - u[7] - u[8] + end + prob = NonlinearProblem(f, zeros(8)) + sol = solve(prob, NewtonRaphson()) + + u0 = zeros(2) + p = zeros(3) + + function f1(du, u, p) + du[1] = cos(u[2]) - u[1] + du[2] = sin(u[1] + u[2]) + u[2] + end + explicitfun1(p, sols) = nothing + prob1 = NonlinearProblem( + NonlinearFunction{true, SciMLBase.NoSpecialize}(f1), zeros(2), p) + sol1 = solve(prob1, NewtonRaphson()) + + function f2(du, u, p) + du[1] = 2u[2] + u[1] + 1.0 + du[2] = u[3]^2 + u[2] + du[3] = u[1]^2 + u[3] + end + explicitfun2(p, sols) = nothing + prob2 = NonlinearProblem( + NonlinearFunction{true, SciMLBase.NoSpecialize}(f2), zeros(3), p) + sol2 = solve(prob2, NewtonRaphson()) + + function f3(du, u, p) + du[1] = p[1] + 2.0u[1] + 2.5u[2] + 1.5u[3] + du[2] = p[2] + 4.0u[1] - 1.5u[2] + 1.5u[3] + du[3] = p[3] + +u[1] - u[2] - u[3] + end + prob3 = NonlinearProblem( + NonlinearFunction{true, SciMLBase.NoSpecialize}(f3), zeros(3), p) + function explicitfun3(p, sols) + p[1] = sols[1][1] + sols[1][2] + sols[2][1] + sols[2][2] + sols[2][3] + p[2] = sols[1][1] + sols[1][2] + sols[2][1] + 2.0sols[2][2] + sols[2][3] + p[3] = sols[1][1] + 2.0sols[1][2] + 3.0sols[2][1] + 5.0sols[2][2] + + 6.0sols[2][3] + end + explicitfun3(p, [sol1, sol2]) + sol3 = solve(prob3, NewtonRaphson()) + manualscc = [sol1; sol2; sol3] + + sccprob = SciMLBase.SCCNonlinearProblem([prob1, prob2, prob3], + SciMLBase.Void{Any}.([explicitfun1, explicitfun2, explicitfun3])) + scc_sol = solve(sccprob, NewtonRaphson()) + @test sol ≈ manualscc ≈ scc_sol +end diff --git a/lib/SCCNonlinearSolve/test/qa_tests.jl b/lib/SCCNonlinearSolve/test/qa_tests.jl new file mode 100644 index 000000000..93111c1cc --- /dev/null +++ b/lib/SCCNonlinearSolve/test/qa_tests.jl @@ -0,0 +1,25 @@ +@testitem "Aqua" tags=[:core] begin + using Aqua, SCCNonlinearSolve + + Aqua.test_all( + SCCNonlinearSolve; + piracies = false, ambiguities = false, stale_deps = false, deps_compat = false + ) + Aqua.test_stale_deps( + SCCNonlinearSolve; ignore = [:SciMLJacobianOperators, :NonlinearSolveBase]) + Aqua.test_deps_compat( + SCCNonlinearSolve; ignore = [:SciMLJacobianOperators, :NonlinearSolveBase]) + Aqua.test_piracies( + SCCNonlinearSolve; treat_as_own = [SCCNonlinearSolve.SciMLBase.solve]) + Aqua.test_ambiguities(SCCNonlinearSolve; recursive = false) +end + +@testitem "Explicit Imports" tags=[:core] begin + using ExplicitImports, SciMLBase, SCCNonlinearSolve + + @test check_no_implicit_imports( + SCCNonlinearSolve; skip = (Base, Core, SciMLBase) + ) === nothing + @test check_no_stale_explicit_imports(SCCNonlinearSolve) === nothing + @test check_all_qualified_accesses_via_owners(SCCNonlinearSolve) === nothing +end diff --git a/lib/SCCNonlinearSolve/test/runtests.jl b/lib/SCCNonlinearSolve/test/runtests.jl new file mode 100644 index 000000000..451760225 --- /dev/null +++ b/lib/SCCNonlinearSolve/test/runtests.jl @@ -0,0 +1,25 @@ +using ReTestItems, SCCNonlinearSolve, Hwloc, InteractiveUtils, Pkg + +@info sprint(InteractiveUtils.versioninfo) + +const GROUP = lowercase(get(ENV, "GROUP", "All")) + +const RETESTITEMS_NWORKERS = parse( + Int, get(ENV, "RETESTITEMS_NWORKERS", + string(min(ifelse(Sys.iswindows(), 0, Hwloc.num_physical_cores()), 4)) + ) +) +const RETESTITEMS_NWORKER_THREADS = parse(Int, + get( + ENV, "RETESTITEMS_NWORKER_THREADS", + string(max(Hwloc.num_virtual_cores() ÷ max(RETESTITEMS_NWORKERS, 1), 1)) + ) +) + +@info "Running tests for group: $(GROUP) with $(RETESTITEMS_NWORKERS) workers" + +ReTestItems.runtests( + SCCNonlinearSolve; tags = (GROUP == "all" ? nothing : [Symbol(GROUP)]), + nworkers = RETESTITEMS_NWORKERS, nworker_threads = RETESTITEMS_NWORKER_THREADS, + testitem_timeout = 3600 +)