diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 36b0782c..cd7b7f24 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,5 @@ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 -enable-beta-ecosystems: true # Julia ecosystem updates: - package-ecosystem: "github-actions" directory: "/" # Location of package manifests diff --git a/.github/workflows/DocPreviewCleanup.yml b/.github/workflows/DocPreviewCleanup.yml new file mode 100644 index 00000000..7b694802 --- /dev/null +++ b/.github/workflows/DocPreviewCleanup.yml @@ -0,0 +1,17 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + doc-preview-cleanup: + uses: "SciML/.github/.github/workflows/docs-preview-cleanup.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/Downgrade.yml b/.github/workflows/Downgrade.yml index 04346b7f..f5a7aaa0 100644 --- a/.github/workflows/Downgrade.yml +++ b/.github/workflows/Downgrade.yml @@ -12,26 +12,16 @@ on: - 'docs/**' jobs: test: - if: false # Disabled: JET test dependency incompatible with downgrade. See issue #522 for details. - runs-on: ubuntu-latest + name: "Downgrade" strategy: + fail-fast: false matrix: group: - Core - downgrade_mode: ['alldeps'] - julia-version: ['1.10'] - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v3 - with: - version: ${{ matrix.julia-version }} - - uses: julia-actions/julia-downgrade-compat@v2 -# if: ${{ matrix.version == '1.6' }} - with: - skip: Pkg,TOML - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - with: - ALLOW_RERESOLVE: false - env: - GROUP: ${{ matrix.group }} + uses: "SciML/.github/.github/workflows/downgrade.yml@v1" + with: + julia-version: "1.10" + group: "${{ matrix.group }}" + skip: "Pkg,TOML" + allow-reresolve: false + secrets: "inherit" diff --git a/.github/workflows/DowngradeSublibraries.yml b/.github/workflows/DowngradeSublibraries.yml new file mode 100644 index 00000000..128b69d1 --- /dev/null +++ b/.github/workflows/DowngradeSublibraries.yml @@ -0,0 +1,23 @@ +name: Downgrade Sublibraries +on: + pull_request: + branches: + - master + paths-ignore: + - 'docs/**' + push: + branches: + - master + paths-ignore: + - 'docs/**' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} +jobs: + downgrade-sublibraries: + uses: "SciML/.github/.github/workflows/sublibrary-downgrade.yml@v1" + secrets: "inherit" + with: + julia-version: "lts" + skip: "RecursiveArrayTools, Pkg, TOML, Statistics, LinearAlgebra, SparseArrays, InteractiveUtils, Random, Test" + allow-reresolve: true diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index ec171e7c..44b3a01a 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -7,15 +7,10 @@ on: jobs: test: - name: ${{ matrix.package.repo }}/${{ matrix.package.group }}/${{ matrix.julia-version }} - runs-on: ${{ matrix.os }} - env: - GROUP: ${{ matrix.package.group }} + name: ${{ matrix.package.repo }}/${{ matrix.package.group }} strategy: fail-fast: false matrix: - julia-version: [1] - os: [ubuntu-latest] package: - {user: SciML, repo: SciMLBase.jl, group: Core} - {user: SciML, repo: DiffEqBase.jl, group: Core} @@ -32,38 +27,10 @@ jobs: - {user: SciML, repo: SciMLSensitivity.jl, group: Core5} - {user: SciML, repo: SciMLSensitivity.jl, group: Core6} - {user: SciML, repo: LabelledArrays.jl, group: RecursiveArrayTools} - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v3 - with: - version: ${{ matrix.julia-version }} - arch: x64 - - uses: julia-actions/julia-buildpkg@latest - - name: Clone Downstream - uses: actions/checkout@v6 - with: - repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} - path: downstream - - name: Load this and run the downstream tests - shell: julia --color=yes --project=downstream {0} - run: | - using Pkg - try - # force it to use this PR's version of the package - Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps - Pkg.update() - Pkg.test(coverage=true) # resolver may fail with test time deps - catch err - err isa Pkg.Resolve.ResolverError || rethrow() - # If we can't resolve that means this is incompatible by SemVer and this is fine - # It means we marked this as a breaking change, so we don't need to worry about - # Mistakenly introducing a breaking change, as we have intentionally made one - @info "Not compatible with this release. No problem." exception=err - exit(0) # Exit immediately, as a success - end - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v6 - with: - files: lcov.info - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + uses: "SciML/.github/.github/workflows/downstream.yml@v1" + with: + owner: "${{ matrix.package.user }}" + repo: "${{ matrix.package.repo }}" + group: "${{ matrix.package.group }}" + julia-version: "1" + secrets: "inherit" diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index d22e82d3..32f2e1fb 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -11,9 +11,6 @@ on: jobs: runic: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: fredrikekre/runic-action@v1 - with: - version: '1' + name: "Runic" + uses: "SciML/.github/.github/workflows/runic.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/SpellCheck.yml b/.github/workflows/SpellCheck.yml index 746b039a..a134515e 100644 --- a/.github/workflows/SpellCheck.yml +++ b/.github/workflows/SpellCheck.yml @@ -4,10 +4,6 @@ on: [pull_request] jobs: typos-check: - name: Spell Check with Typos - runs-on: ubuntu-latest - steps: - - name: Checkout Actions Repository - uses: actions/checkout@v6 - - name: Check spelling - uses: crate-ci/typos@v1.18.0 \ No newline at end of file + name: "Spell Check with Typos" + uses: "SciML/.github/.github/workflows/spellcheck.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/SublibraryCI.yml b/.github/workflows/SublibraryCI.yml new file mode 100644 index 00000000..6cec66bb --- /dev/null +++ b/.github/workflows/SublibraryCI.yml @@ -0,0 +1,22 @@ +name: Sublibrary CI + +on: + pull_request: + branches: + - master + paths-ignore: + - 'docs/**' + push: + branches: + - master + paths-ignore: + - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +jobs: + sublibraries: + uses: "SciML/.github/.github/workflows/sublibrary-project-tests.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 9a97a50f..267e3d8d 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -18,11 +18,7 @@ concurrency: jobs: tests: - name: "Tests - ${{ matrix.group }} - Julia ${{ matrix.version }}" - permissions: - actions: write - contents: read - runs-on: ubuntu-latest + name: "Tests" strategy: fail-fast: false matrix: @@ -40,31 +36,10 @@ jobs: exclude: - version: "pre" group: "nopre" - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v3 - with: - version: "${{ matrix.version }}" - - uses: julia-actions/cache@v3 - with: - token: "${{ secrets.GITHUB_TOKEN }}" - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - env: - GROUP: "${{ matrix.group }}" - # Run tests with 2 threads so that threaded regression tests (e.g. the - # @.. thread=true VectorOfArray{SArray} test for issue #570) actually - # exercise FastBroadcast's Polyester-backed multi-thread batch-split - # path. With the default single thread, that path is a no-op and the - # regression is not covered. Mirrors SciML/OrdinaryDiffEq.jl's - # SublibraryCI.yml which passes JULIA_NUM_THREADS on the runtest step. - JULIA_NUM_THREADS: "2" - - uses: julia-actions/julia-processcoverage@v1 - with: - directories: "src,ext" - - uses: codecov/codecov-action@v6 - if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}" - with: - files: lcov.info - token: "${{ secrets.CODECOV_TOKEN }}" - fail_ci_if_error: true + uses: "SciML/.github/.github/workflows/tests.yml@v1" + with: + julia-version: "${{ matrix.version }}" + group: "${{ matrix.group }}" + num-threads: "2" + coverage-directories: "src,ext" + secrets: "inherit" diff --git a/Project.toml b/Project.toml index 6898fb8b..d61458c8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RecursiveArrayTools" uuid = "731186ca-8d62-57ce-b412-fbd966d074cd" authors = ["Chris Rackauckas "] -version = "4.3.0" +version = "4.3.1" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" @@ -51,7 +51,7 @@ RecursiveArrayToolsZygoteExt = "Zygote" [compat] Adapt = "4" Aqua = "0.8" -ArrayInterface = "7.16" +ArrayInterface = "7.17.0" CUDA = "5, 6.0" DocStringExtensions = "0.9.3" FastBroadcast = "1.3" @@ -76,7 +76,7 @@ StaticArraysCore = "1.4.2" Statistics = "1.10, 1.11" StructArrays = "0.7" SymbolicIndexingInterface = "0.3.42" -Tables = "1.12" +Tables = "1.12.1" Test = "1" Tracker = "0.2.34" Unitful = "1" @@ -106,4 +106,4 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [targets] -test = ["Aqua", "FastBroadcast", "ForwardDiff", "KernelAbstractions", "Measurements", "Mooncake", "NLsolve", "Pkg", "Polyester", "Random", "SafeTestsets", "SparseArrays", "StaticArrays", "Statistics", "StructArrays", "Tables", "Test", "Unitful", "Zygote"] +test = ["Aqua", "FastBroadcast", "ForwardDiff", "KernelAbstractions", "Measurements", "Mooncake", "NLsolve", "Pkg", "Polyester", "Random", "ReverseDiff", "SafeTestsets", "SparseArrays", "StaticArrays", "Statistics", "StructArrays", "Tables", "Test", "Unitful", "Zygote"] diff --git a/docs/src/recursive_array_functions.md b/docs/src/recursive_array_functions.md index 1bb31691..48eae61a 100644 --- a/docs/src/recursive_array_functions.md +++ b/docs/src/recursive_array_functions.md @@ -8,6 +8,7 @@ and do not require that the RecursiveArrayTools types are used. ```@docs recursivecopy recursivecopy! +recursivecopyto! vecvecapply copyat_or_push! ``` diff --git a/ext/RecursiveArrayToolsZygoteExt.jl b/ext/RecursiveArrayToolsZygoteExt.jl index e4ef4c1f..5dc9032f 100644 --- a/ext/RecursiveArrayToolsZygoteExt.jl +++ b/ext/RecursiveArrayToolsZygoteExt.jl @@ -4,6 +4,7 @@ using RecursiveArrayTools using Zygote using Zygote: FillArrays, ChainRulesCore, literal_getproperty, @adjoint +using Zygote.ChainRulesCore: AbstractZero function ChainRulesCore.rrule( T::Type{<:RecursiveArrayTools.GPUArraysCore.AbstractGPUArray}, @@ -90,7 +91,7 @@ end function vofa_u_adjoint(d, A::RecursiveArrayTools.AbstractVectorOfArray) m = map(enumerate(d)) do (idx, d_i) - isnothing(d_i) && return zero(A.u[idx]) + (isnothing(d_i) || d_i isa AbstractZero) && return zero(A.u[idx]) d_i end return VectorOfArray(m) @@ -98,7 +99,7 @@ end function vofa_u_adjoint(d, A::RecursiveArrayTools.AbstractDiffEqArray) m = map(enumerate(d)) do (idx, d_i) - isnothing(d_i) && return zero(A.u[idx]) + (isnothing(d_i) || d_i isa AbstractZero) && return zero(A.u[idx]) d_i end return DiffEqArray(m, A.t) @@ -106,7 +107,7 @@ end @adjoint function literal_getproperty(A::ArrayPartition, ::Val{:x}) function literal_ArrayPartition_x_adjoint(d) - (ArrayPartition((isnothing(d[i]) ? zero(A.x[i]) : d[i] for i in 1:length(d))...),) + (ArrayPartition((isnothing(d[i]) || d[i] isa AbstractZero ? zero(A.x[i]) : d[i] for i in 1:length(d))...),) end A.x, literal_ArrayPartition_x_adjoint end diff --git a/lib/RecursiveArrayToolsRaggedArrays/Project.toml b/lib/RecursiveArrayToolsRaggedArrays/Project.toml index b031648d..c75a306b 100644 --- a/lib/RecursiveArrayToolsRaggedArrays/Project.toml +++ b/lib/RecursiveArrayToolsRaggedArrays/Project.toml @@ -12,11 +12,11 @@ SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" [compat] Adapt = "4" -ArrayInterface = "7" +ArrayInterface = "7.17.0" LinearAlgebra = "1.10" RecursiveArrayTools = "4" -StaticArraysCore = "1.4" -SymbolicIndexingInterface = "0.3.35" +StaticArraysCore = "1.4.2" +SymbolicIndexingInterface = "0.3.42" julia = "1.10" [extras] diff --git a/src/RecursiveArrayTools.jl b/src/RecursiveArrayTools.jl index 681e7474..a86797c2 100644 --- a/src/RecursiveArrayTools.jl +++ b/src/RecursiveArrayTools.jl @@ -189,7 +189,8 @@ module RecursiveArrayTools export DEFAULT_PLOT_FUNC, plottable_indices, plot_indices, getindepsym_defaultt, interpret_vars, add_labels!, diffeq_to_arrays, solplot_vecs_and_labels - export recursivecopy, recursivecopy!, recursivefill!, vecvecapply, copyat_or_push!, + export recursivecopy, recursivecopy!, recursivecopyto!, recursivefill!, vecvecapply, + copyat_or_push!, vecvec_to_mat, recursive_one, recursive_mean, recursive_bottom_eltype, recursive_unitless_bottom_eltype, recursive_unitless_eltype diff --git a/src/array_partition.jl b/src/array_partition.jl index 0f2eba96..c0bf2cae 100644 --- a/src/array_partition.jl +++ b/src/array_partition.jl @@ -361,6 +361,23 @@ function recursivecopy!( return A end +function recursivecopyto!(A::ArrayPartition, B::ArrayPartition) + for (a, b) in zip(A.x, B.x) + recursivecopyto!(a, b) + end + return A +end + +function recursivecopyto!( + A::ArrayPartition{T, S}, + B::ArrayPartition{T, S} + ) where {T, S <: Tuple{Vararg{AbstractVectorOfArray}}} + for i in eachindex(A.x, B.x) + recursivecopyto!(A.x[i], B.x[i]) + end + return A +end + function recursive_mean(A::ArrayPartition) n = npartitions(A) if n == 0 diff --git a/src/utils.jl b/src/utils.jl index e9d10295..b96224cb 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -45,7 +45,9 @@ recursivecopy!(b::AbstractArray{T, N}, a::AbstractArray{T, N}) ``` A recursive `copy!` function. Acts like a `deepcopy!` on arrays of arrays, but -like `copy!` on arrays of scalars. +like `copy!` on arrays of scalars. Requires `b` and `a` to have matching `ndims`; +use [`recursivecopyto!`](@ref) for the `copyto!`-style linear-index variant that +allows mismatched shapes. """ function recursivecopy! end @@ -105,6 +107,68 @@ function recursivecopy!(b::AbstractVectorOfArray, a::AbstractVectorOfArray) return b end +""" +```julia +recursivecopyto!(b::AbstractArray, a::AbstractArray) +``` + +A recursive `copyto!` function. Acts like a `deepcopy!` on arrays of arrays, but +like `copyto!` on arrays of scalars. + +Unlike [`recursivecopy!`](@ref), this does not require `b` and `a` to have matching +`ndims` or axes; only that `length(b) >= length(a)`. Elements are copied in linear +(column-major) order, matching the semantics of `Base.copyto!`. Use this when +flattening/reshaping between destination and source is intended, e.g. copying a +`Vector` into a `Matrix` of the same total length. +""" +function recursivecopyto! end + +function recursivecopyto!(b::AbstractArray, a::AbstractArray) + return copyto!(b, a) +end + +function recursivecopyto!( + b::AbstractArray{T}, + a::AbstractArray{T2} + ) where { + T <: StaticArraysCore.StaticArray, + T2 <: StaticArraysCore.StaticArray, + } + @inbounds for (ib, ia) in zip(eachindex(b), eachindex(a)) + # TODO: Check for `setindex!`` and use `copy!(b[i],a[i])` or `b[i] = a[i]`, see #19 + b[ib] = copy(a[ia]) + end + return b +end + +function recursivecopyto!( + b::AbstractArray{T}, + a::AbstractArray{T2} + ) where { + T <: Union{AbstractArray, AbstractVectorOfArray}, + T2 <: Union{AbstractArray, AbstractVectorOfArray}, + } + if ArrayInterface.ismutable(T) + @inbounds for (ib, ia) in zip(eachindex(b), eachindex(a)) + recursivecopyto!(b[ib], a[ia]) + end + else + copyto!(b, a) + end + return b +end + +function recursivecopyto!(b::AbstractVectorOfArray, a::AbstractVectorOfArray) + @inbounds for i in eachindex(b.u, a.u) + if ArrayInterface.ismutable(b.u[i]) || b.u[i] isa AbstractVectorOfArray + recursivecopyto!(b.u[i], a.u[i]) + else + b.u[i] = recursivecopy(a.u[i]) + end + end + return b +end + """ ```julia recursivefill!(b::AbstractArray{T, N}, a) diff --git a/src/vector_of_array.jl b/src/vector_of_array.jl index 191a5eb2..ac673613 100644 --- a/src/vector_of_array.jl +++ b/src/vector_of_array.jl @@ -797,6 +797,15 @@ Base.@propagate_inbounds function Base.getindex(A::AbstractVectorOfArray, _arg, end symtype = symbolic_type(_arg) elsymtype = symbolic_type(eltype(_arg)) + # For symbolic indices, `A[sym, :]` is semantically equivalent to `A[sym]` + # (the no-args symbolic getindex already returns the full timeseries). + # Routing through the no-args path here also avoids a broadcast shape bug + # in SymbolicIndexingInterface's `GetStateIndex` when the underlying index + # is a `Vector{Int}` (array-symbolic) combined with a `Colon` time index. + if (symtype != NotSymbolic() || elsymtype != NotSymbolic()) && + length(args) == 1 && args[1] === Colon() + return A[_arg] + end return if symtype == NotSymbolic() && elsymtype == NotSymbolic() if _arg isa Union{Tuple, AbstractArray} && @@ -1128,10 +1137,14 @@ vecarr_to_vectors(VA::AbstractVectorOfArray) = [VA[i, :] for i in eachindex(VA.u # linear algebra ArrayInterface.issingular(va::AbstractVectorOfArray) = ArrayInterface.issingular(Matrix(va)) -# Type-stable sum/mapreduce that avoids inference issues on Julia 1.10 -# with deeply nested VectorOfArray type parameters +# The `::` assertion pins the return type so Julia 1.10's inference does not bail +# (widening to `Any`) on the self-recursion of `sum(::AbstractVectorOfArray)` for +# deeply nested VectorOfArrays. It must be the type `sum` actually produces -- the +# `add_sum` accumulator type -- and NOT the element type `T`, which over-constrains +# the result for promoting eltypes (`Int8` -> `Int64`) and for AD scalars whose +# summation drops metadata (e.g. `ReverseDiff.TrackedReal` origin); see issue #595. function Base.sum(VA::AbstractVectorOfArray{T}) where {T} - return sum(sum, VA.u)::T + return sum(sum, VA.u)::Base.promote_op(Base.add_sum, T, T) end function Base.sum(f::F, VA::AbstractVectorOfArray{T}) where {F, T} diff --git a/test/adjoints.jl b/test/adjoints.jl index fec7f548..4bb1a644 100644 --- a/test/adjoints.jl +++ b/test/adjoints.jl @@ -1,4 +1,5 @@ -using RecursiveArrayTools, Zygote, ForwardDiff, Test +using RecursiveArrayTools, Zygote, ForwardDiff, ReverseDiff, Test +using Zygote: ChainRulesCore function loss(x) return sum(abs2, Array(VectorOfArray([x .* i for i in 1:5]))) @@ -90,3 +91,30 @@ voa_gs, = Zygote.gradient(voa) do x sum(sum.(x.u)) end @test voa_gs isa RecursiveArrayTools.VectorOfArray + +# issue #595: `sum(::VectorOfArray)` must not assert the element type, which +# discarded ReverseDiff's `TrackedReal` origin and threw a `TypeError`. +let g = ReverseDiff.gradient(x -> sum(VectorOfArray([x])), float.(1:3)) + @test g ≈ ones(3) +end +let g = ReverseDiff.gradient(x -> sum(VectorOfArray([VectorOfArray([x])])), float.(1:3)) + @test g ≈ ones(3) +end + +# A `ZeroTangent`/`AbstractZero` slice in the cotangent (structural zero gradient) +# must be treated like an absent gradient instead of erroring on `size(::ZeroTangent)`. +let ext = Base.get_extension(RecursiveArrayTools, :RecursiveArrayToolsZygoteExt) + voa = VectorOfArray([Float64[i, i, i] for i in 1:3]) + dea = DiffEqArray([Float64[i, i, i] for i in 1:3], 1:3) + d = Any[Float64[1, 1, 1], ChainRulesCore.ZeroTangent(), Float64[3, 3, 3]] + expected = [Float64[1, 1, 1], Float64[0, 0, 0], Float64[3, 3, 3]] + + g_voa = ext.vofa_u_adjoint(d, voa) + @test g_voa isa VectorOfArray + @test g_voa.u == expected + + g_dea = ext.vofa_u_adjoint(d, dea) + @test g_dea isa DiffEqArray + @test g_dea.u == expected + @test g_dea.t == dea.t +end diff --git a/test/downstream/Project.toml b/test/downstream/Project.toml index 5e7108d3..ad7277e8 100644 --- a/test/downstream/Project.toml +++ b/test/downstream/Project.toml @@ -4,7 +4,9 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" MonteCarloMeasurements = "0987c9cc-fe09-11e8-30f0-b96dd679fdca" NLsolve = "2774e3e8-f4cf-5e23-947b-6d7e65073b56" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" +RecursiveArrayToolsShorthandConstructors = "39fb7555-b4ad-4efd-8abe-30331df017d3" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" @@ -14,10 +16,11 @@ Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [compat] ArrayInterface = "7" -ModelingToolkit = "8.33, 9" +ModelingToolkit = "8.33, 9, 10, 11" MonteCarloMeasurements = "1.1" NLsolve = "4" -OrdinaryDiffEq = "6.31" +OrdinaryDiffEq = "6.31, 7" +OrdinaryDiffEqRosenbrock = "1, 2" StaticArrays = "1" SymbolicIndexingInterface = "0.3" Tables = "1" diff --git a/test/downstream/downstream_events.jl b/test/downstream/downstream_events.jl index 230dcbb0..2118cbc7 100644 --- a/test/downstream/downstream_events.jl +++ b/test/downstream/downstream_events.jl @@ -1,4 +1,4 @@ -using OrdinaryDiffEq, StaticArrays, RecursiveArrayTools +using OrdinaryDiffEq, StaticArrays, RecursiveArrayTools, RecursiveArrayToolsShorthandConstructors u0 = AP[SVector{1}(50.0), SVector{1}(0.0)] tspan = (0.0, 15.0) diff --git a/test/downstream/odesolve.jl b/test/downstream/odesolve.jl index 2ca163ab..5fe162d8 100644 --- a/test/downstream/odesolve.jl +++ b/test/downstream/odesolve.jl @@ -1,4 +1,5 @@ -using OrdinaryDiffEq, NLsolve, RecursiveArrayTools, Test, ArrayInterface, StaticArrays +using OrdinaryDiffEq, OrdinaryDiffEqRosenbrock, NLsolve, RecursiveArrayTools, + RecursiveArrayToolsShorthandConstructors, Test, ArrayInterface, StaticArrays function lorenz(du, u, p, t) du[1] = 10.0 * (u[2] - u[1]) du[2] = u[1] * (28.0 - u[3]) - u[2] @@ -9,7 +10,7 @@ u0 = AP[[1.0, 0.0], [0.0]] tspan = (0.0, 100.0) prob = ODEProblem(lorenz, u0, tspan) sol = solve(prob, Tsit5()) -sol = solve(prob, AutoTsit5(Rosenbrock23(autodiff = false))) +sol = solve(prob, AutoTsit5(Rosenbrock23(autodiff = AutoFiniteDiff()))) sol = solve(prob, AutoTsit5(Rosenbrock23())) @test all(Array(sol) .== sol) @@ -72,4 +73,4 @@ end u = fill(SVector{2}(ones(2)), 2, 3) ode = ODEProblem(rhs!, VectorOfArray(u), (0.0, 1.0)) sol = solve(ode, Tsit5()) -@test SciMLBase.successful_retcode(sol) +@test successful_retcode(sol) diff --git a/test/downstream/symbol_indexing.jl b/test/downstream/symbol_indexing.jl index b420d80c..3cbae750 100644 --- a/test/downstream/symbol_indexing.jl +++ b/test/downstream/symbol_indexing.jl @@ -77,7 +77,7 @@ ts = 0:0.5:10 sol_ts = sol(ts) @assert sol_ts isa DiffEqArray test_tables_interface( - sol_ts, [:timestamp, Symbol("x(t)"), Symbol("y(t)")], + sol_ts, [:timestamp; Symbol.(string.(unknowns(lv)))], hcat(ts, Array(sol_ts)') ) diff --git a/test/interface_tests.jl b/test/interface_tests.jl index 91a68a76..694334fc 100644 --- a/test/interface_tests.jl +++ b/test/interface_tests.jl @@ -70,6 +70,13 @@ push!(testda, [-1, -2, -3, -4]) @inferred sum(testva) @inferred sum(VA[VA[zeros(4, 4)]]) @inferred mapreduce(string, *, testva) + +# sum must follow Base's promotion, not the element type (issue #595): +# `Int8` elements sum to `Int64`, so asserting the result is `::Int8` is wrong. +@test sum(VA[Int8[1, 2, 3]]) === Int64(6) +@test sum(VA[Int8[1, 2], Int8[3, 4]]) === Int64(10) +@test sum(VA[Float32[1, 2, 3]]) === 6.0f0 +@inferred sum(VA[Int8[1, 2, 3]]) # Type stability for `end` indexing (issue #525) testva_end = VectorOfArray(fill(fill(2.0, 2), 10)) # Use lastindex directly since `end` doesn't work in SafeTestsets diff --git a/test/mooncake.jl b/test/mooncake.jl index 6a0ff28e..ef27e802 100644 --- a/test/mooncake.jl +++ b/test/mooncake.jl @@ -14,7 +14,7 @@ using RecursiveArrayTools, Mooncake, Test @testset "ArrayPartition increment_and_get_rdata!" begin @test Base.get_extension(RecursiveArrayTools, :RecursiveArrayToolsMooncakeExt) !== - nothing + nothing # Tangent produced by an upstream ChainRule. t = ArrayPartition([1.0, 2.0], [3.0, 4.0]) diff --git a/test/named_array_partition_tests.jl b/test/named_array_partition_tests.jl index 1cc9f952..2f219ed2 100644 --- a/test/named_array_partition_tests.jl +++ b/test/named_array_partition_tests.jl @@ -58,21 +58,21 @@ end @test x[[1, 2]] == [1.0, 1.0] @test x[[1, 4]] == [1.0, 2.0] - @test x[1:2] isa Vector{Float64} - @test x[1:end] isa Vector{Float64} + @test x[1:2] isa Vector{Float64} + @test x[1:end] isa Vector{Float64} @test x[[1, 4]] isa Vector{Float64} # Inferred return types: Vector, not Union - @test (@inferred x[1:2]) isa Vector{Float64} - @test (@inferred x[1:length(x)]) isa Vector{Float64} - @test (@inferred x[[1, 4]]) isa Vector{Float64} + @test (@inferred x[1:2]) isa Vector{Float64} + @test (@inferred x[1:length(x)]) isa Vector{Float64} + @test (@inferred x[[1, 4]]) isa Vector{Float64} # `similar` with a non-matching dims falls back to the backing array; # with matching dims keeps the NamedArrayPartition wrapper. @test similar(x, Float64, (2,)) isa Vector{Float64} - @test similar(x, (2,)) isa Vector{Float64} + @test similar(x, (2,)) isa Vector{Float64} @test similar(x, Float64, size(x)) isa NamedArrayPartition - @test similar(x, size(x)) isa NamedArrayPartition + @test similar(x, size(x)) isa NamedArrayPartition # Scalar indexing untouched and type-stable @test x[1] == 1.0 diff --git a/test/utils_test.jl b/test/utils_test.jl index 96446d00..ef25ea99 100644 --- a/test/utils_test.jl +++ b/test/utils_test.jl @@ -152,6 +152,87 @@ end @test a.u[1][1] == 1.0 end +@testset "recursivecopyto!" begin + # Same-shape scalar arrays — should match copyto! + b = zeros(3) + a = [1.0, 2.0, 3.0] + recursivecopyto!(b, a) + @test b == a + + b = zeros(2, 2) + a = [1.0 2.0; 3.0 4.0] + recursivecopyto!(b, a) + @test b == a + + # Issue #589: Matrix ← Vector of matching length (rejected by recursivecopy!, + # allowed by recursivecopyto!). + b = zeros(2, 3) + a = collect(1.0:6.0) + recursivecopyto!(b, a) + @test b == reshape(a, 2, 3) + @test_throws MethodError recursivecopy!(b, a) + + # Vector ← Matrix + b = zeros(6) + a = reshape(collect(1.0:6.0), 2, 3) + recursivecopyto!(b, a) + @test b == collect(1.0:6.0) + + # Different-shape matrices, same total length + b = zeros(2, 3) + a = reshape(collect(1.0:6.0), 3, 2) + recursivecopyto!(b, a) + @test vec(b) == 1.0:6.0 + + # dst longer than src — tail untouched, matches Base.copyto! + b = ones(5) + a = [10.0, 20.0, 30.0] + recursivecopyto!(b, a) + @test b == [10.0, 20.0, 30.0, 1.0, 1.0] + + # dst shorter than src — BoundsError, matches Base.copyto! + b = zeros(2) + a = [1.0, 2.0, 3.0] + @test_throws BoundsError recursivecopyto!(b, a) + + # Nested: Vector of Vectors, matching shapes + a = [ones(3), 2 * ones(3)] + b = [zeros(3), zeros(3)] + recursivecopyto!(b, a) + @test b[1] == ones(3) && b[2] == 2 * ones(3) + # Verify deep copy semantics — mutating dst leaves src untouched + b[1][1] = 99.0 + @test a[1][1] == 1.0 + + # Nested with shape mismatch at the leaves — inner copyto! handles it + a = [collect(1.0:6.0), collect(7.0:12.0)] + b = [zeros(2, 3), zeros(2, 3)] + recursivecopyto!(b, a) + @test b[1] == reshape(1.0:6.0, 2, 3) + @test b[2] == reshape(7.0:12.0, 2, 3) + + # Static array element + a = [@SVector([1.0, 2.0]), @SVector([3.0, 4.0])] + b = [@SVector(zeros(2)), @SVector(zeros(2))] + recursivecopyto!(b, a) + @test b == a + + # ArrayPartition with matching shapes (sanity — parity with recursivecopy!) + A = ArrayPartition(zeros(2), zeros(3)) + B = ArrayPartition([1.0, 2.0], [3.0, 4.0, 5.0]) + recursivecopyto!(A, B) + @test A.x[1] == [1.0, 2.0] + @test A.x[2] == [3.0, 4.0, 5.0] + + # VectorOfArray + u1 = VA[zeros(MVector{2, Float64}), zeros(MVector{2, Float64})] + u2 = VA[fill(4, MVector{2, Float64}), 2 .* ones(MVector{2, Float64})] + recursivecopyto!(u1, u2) + @test u1.u[1] == [4.0, 4.0] + @test u1.u[2] == [2.0, 2.0] + @test u1.u[1] isa MVector +end + @testset "VectorOfArray similar with nested scalar leaves" begin a = VA[ones(2), VA[1.0, 1.0]] b = similar(a, Float64)