From 230aab556cb2219b7b506cab8f76549a16cb9cc0 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Tue, 8 Sep 2026 19:28:54 -0400 Subject: [PATCH] Give the POD reduced system its own time span deim already inherited a time span; pod did not, so the POD Galerkin examples still had to repeat the full problem's. Thread tspan through _pod into the shared assembler, from prob.tspan for the problem method and get_tspan(sys) for the system method, and drop the now-redundant argument from the README, the docstring and the FitzHugh-Nagumo test. Completes the ModelOrderReduction side of #29, in 1.0.0 before release. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5 Agent-Harness: Claude Code 2.1.263 Agent-Model: claude-opus-5 Agent-Session: https://claude.ai/code/session_01H5RuLDtEazwTKPkNbPeZVu Claude-Session: https://claude.ai/code/session_01H5RuLDtEazwTKPkNbPeZVu --- AGENTS.md | 11 +++++++++-- README.md | 2 +- src/pod.jl | 22 ++++++++++++++++------ test/deim.jl | 7 +++---- test/pod.jl | 25 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4cc01b5f..f57217c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,8 +25,8 @@ - Source systems must list scalar unknowns (scalarized array elements or `mtkcompile` output). A `System` built without explicit unknowns collects both an array variable and its indexed elements, which no problem constructor accepts either. -- Both `DAEProblem(rom, nothing, tspan; build_initializeprob = false)` and - `ODEProblem(mtkcompile(rom), nothing, tspan; build_initializeprob = false)` are supported +- Both `DAEProblem(rom, nothing; build_initializeprob = false)` and + `ODEProblem(mtkcompile(rom), nothing; build_initializeprob = false)` are supported and tested. Reconstruct fields with `SymbolicIndexingInterface.observed`; the `sol[field]` convenience path hits https://github.com/SciML/ModelingToolkit.jl/issues/5072. - The reduced equation is array linear algebra: projected matrices, the stencil rows of the @@ -41,3 +41,10 @@ collide with a name a user typed. Collisions with names inherited from the source system, which happens when reducing an already-reduced model, are resolved by appending a counter. Closes https://github.com/SciML/ModelOrderReduction.jl/issues/28. +- The reduced system inherits a time span, for both `deim` and `pod`: from `prob.tspan` for + the problem method, and from `get_tspan(sys)` for the system method. A source without a + time span must yield a reduced system without one rather than a fabricated interval, and a + new reduction entry point has to thread `tspan` into `_assemble_reduced_system` the same + way. This needs the `tspan` field on `ModelingToolkit.System`, added in + ModelingToolkitBase 1.69.0 and guaranteed only from ModelingToolkit 11.42.0. + Closes https://github.com/SciML/ModelOrderReduction.jl/issues/29. diff --git a/README.md b/README.md index 99519d18..dd169cb7 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ sol = solve(full_prob) # POD Galerkin: one array reduced equation, O(1) symbolic residual in the grid size rom = pod(full_prob, sol, 4) -rom_prob = DAEProblem(rom, nothing, full_prob.tspan; build_initializeprob = false) +rom_prob = DAEProblem(rom, nothing; build_initializeprob = false) rom_sol = solve(rom_prob) z_approx = [SII.observed(rom, z)(u, rom_prob.p, tt) for (u, tt) in zip(rom_sol.u, rom_sol.t)] ``` diff --git a/src/pod.jl b/src/pod.jl index 4c03f26b..36ef3cf4 100644 --- a/src/pod.jl +++ b/src/pod.jl @@ -307,7 +307,8 @@ end function _pod( sys::System, snapshot::AbstractMatrix, pod_dim::Integer, name::Symbol; - snapshot_times = nothing, training_parameters = Dict{Any, Any}(), kwargs... + snapshot_times = nothing, training_parameters = Dict{Any, Any}(), + tspan = nothing, kwargs... ) rows = length(ModelingToolkit.unknowns(sys)) size(snapshot, 1) == rows || throw( @@ -339,7 +340,7 @@ function _pod( ) return _assemble_reduced_system( fom, snapshot, state_basis, reduced_state, rhs, array_parameters, - array_values, taken, name, initial_derivative; kwargs... + array_values, taken, name, initial_derivative; tspan, kwargs... ) end end @@ -353,7 +354,7 @@ function _pod( ) return _assemble_reduced_system( fom, snapshot, state_basis, reduced_state, rhs, array_parameters, array_values, - taken, name, initial_derivative; kwargs... + taken, name, initial_derivative; tspan, kwargs... ) end @@ -387,6 +388,10 @@ needed. The returned system always has one array differential equation for the r state and one reconstruction observed equation per source field. Unknowns eliminated during simplification are reconstructed by a least-squares fit to `snapshot`. +The reduced system inherits the time span of `sys`, so a problem can be built from it +without repeating one. Passing a time span explicitly still overrides it, and a source +system without one produces a reduced system without one. + Construct a `DAEProblem` with `build_initializeprob = false` from the returned system to keep array code generation, or call `ModelingToolkit.mtkcompile` on it and construct an `ODEProblem` to generate scalar code for the reduced equation. Reconstruct fields with @@ -421,7 +426,10 @@ function pod( sys::System, snapshot::AbstractMatrix, pod_dim::Integer; name::Symbol = Symbol(nameof(sys), :_pod), snapshot_times = nothing, kwargs... )::System - return _pod(sys, snapshot, pod_dim, name; snapshot_times, kwargs...) + return _pod( + sys, snapshot, pod_dim, name; + snapshot_times, tspan = ModelingToolkit.get_tspan(sys), kwargs... + ) end """ @@ -440,6 +448,8 @@ saved solutions `sol` as the training snapshot. `DAEProblem` returned by MethodOfLines. The saved states are the snapshot columns, the saved times supply the independent variable for time-dependent terms, and the parameter values of `prob` are used for training and become the defaults of the reduced system. +The reduced system also inherits the time span of `prob`, which is the interval it was +trained on, so a problem can be built from it without repeating one. `sol` may be the `SciMLBase.PDETimeSeriesSolution` returned by MethodOfLines or its underlying `SciMLBase.AbstractODESolution`. See the system method for the reduction itself and for how to construct problems from the returned system. @@ -469,7 +479,7 @@ full_problem = discretize(pde_system, discretization; fallback = false) full_solution = solve(full_problem) reduced_system = pod(full_problem, full_solution, 4) reduced_problem = DAEProblem( - reduced_system, nothing, full_problem.tspan; build_initializeprob = false + reduced_system, nothing; build_initializeprob = false ) ``` """ @@ -496,7 +506,7 @@ function pod( reduced_name = isnothing(name) ? Symbol(nameof(sys), :_pod) : name return _pod( sys, snapshot, pod_dim, reduced_name; - snapshot_times = sol.t, training_parameters, kwargs... + snapshot_times = sol.t, training_parameters, tspan = prob.tspan, kwargs... ) end diff --git a/test/deim.jl b/test/deim.jl index 689e8800..abaaf530 100644 --- a/test/deim.jl +++ b/test/deim.jl @@ -71,10 +71,9 @@ reconstruction_parameters = filter(ModelingToolkit.get_ps(deim_sys)) do paramete end @test length(reconstruction_parameters) == 1 -deim_prob = DAEProblem( - deim_sys, nothing, full_prob.tspan; - build_initializeprob = false, -) +# No time span here: the reduced system carries the one it was trained on. +deim_prob = DAEProblem(deim_sys, nothing; build_initializeprob = false) +@test deim_prob.tspan == full_prob.tspan deim_sol = solve(deim_prob; saveat = 1.0) @test successful_retcode(deim_sol) diff --git a/test/pod.jl b/test/pod.jl index 1b582ca8..9836f774 100644 --- a/test/pod.jl +++ b/test/pod.jl @@ -128,3 +128,28 @@ end ) @test norm(reconstruction - truth) / norm(truth) < 0.01 end + +@testset "the POD reduced system carries a tspan" begin + @variables g(t)[1:6] + g0 = [0.4, 0.3, 0.2, 0.1, -0.2, 0.5] + gstates = ModelingToolkit.Symbolics.unwrap.(ModelingToolkit.Symbolics.scalarize(g)) + span = (0.0, 0.5) + @named source = System( + [D(g) ~ -g - g .^ 3], t, gstates, []; + initial_conditions = [g => g0, D(g) => -g0 - g0 .^ 3], + ) + source_problem = DAEProblem(complete(source), nothing, span; build_initializeprob = false) + source_solution = solve(source_problem; saveat = 0.05, abstol = 1.0e-10, reltol = 1.0e-10) + @test successful_retcode(source_solution) + + from_problem = pod(source_problem, source_solution, 3) + @test ModelingToolkit.get_tspan(from_problem) == span + @test DAEProblem(from_problem, nothing; build_initializeprob = false).tspan == span + + snapshot = [sin(0.3 * i * j) + cos(0.2 * i * (j + 1)) for i in 1:6, j in 1:8] + @mtkcompile with_span = System([D(g) ~ -g - g .^ 3], t; tspan = span) + @test ModelingToolkit.get_tspan(pod(with_span, snapshot, 2)) == span + + @mtkcompile without_span = System([D(g) ~ -g - g .^ 3], t) + @test ModelingToolkit.get_tspan(pod(without_span, snapshot, 2)) === nothing +end