|
1 | 1 | # ── Phase 3: ODE simulation with DifferentialEquations / MTK ────────────────── |
2 | 2 |
|
3 | 3 | import DifferentialEquations |
| 4 | +import LinearAlgebra |
4 | 5 | import OrdinaryDiffEqBDF |
5 | 6 | import Logging |
6 | 7 | import ModelingToolkit |
7 | 8 | import Printf: @sprintf |
8 | 9 |
|
9 | 10 | """Module-level default simulation settings. Modify via `configure_simulate!`.""" |
10 | | -const _SIM_SETTINGS = SimulateSettings(solver = DifferentialEquations.Rodas5Pr()) |
| 11 | +const _SIM_SETTINGS = SimulateSettings(solver = DifferentialEquations.Rodas5P()) |
11 | 12 |
|
12 | 13 | """ |
13 | 14 | configure_simulate!(; solver, saveat_n) → SimulateSettings |
@@ -82,21 +83,50 @@ function run_simulate(ode_prob, |
82 | 83 | # For stateless models (no unknowns) the adaptive solver takes no |
83 | 84 | # internal steps and sol.t would be empty with saveat=[]. |
84 | 85 | # Supply explicit time points so observed variables can be evaluated. |
85 | | - sys = ode_prob.f.sys |
86 | | - saveat = isempty(ModelingToolkit.unknowns(sys)) ? |
87 | | - collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = settings.saveat_n)) : |
88 | | - Float64[] |
89 | | - kwargs = (saveat = saveat, dense = true) |
| 86 | + sys = ode_prob.f.sys |
| 87 | + M = ode_prob.f.mass_matrix |
| 88 | + unknowns = ModelingToolkit.unknowns(sys) |
| 89 | + n_unknowns = length(unknowns) |
| 90 | + n_diff = if M isa LinearAlgebra.UniformScaling |
| 91 | + n_unknowns |
| 92 | + else |
| 93 | + count(!iszero, LinearAlgebra.diag(M)) |
| 94 | + end |
| 95 | + |
| 96 | + @show n_diff |
| 97 | + |
| 98 | + kwargs = if n_unknowns == 0 |
| 99 | + # No unknowns at all (e.g. BusUsage): the solver takes no |
| 100 | + # internal steps with saveat=[], leaving sol.t empty. |
| 101 | + # Use a fixed grid + adaptive=false so observed variables |
| 102 | + # can be evaluated. |
| 103 | + t0_s, t1_s = ode_prob.tspan |
| 104 | + saveat_s = collect(range(t0_s, t1_s; length = settings.saveat_n)) |
| 105 | + dt_s = saveat_s[2] - saveat_s[1] |
| 106 | + (saveat = saveat_s, adaptive = false, dt = dt_s, dense = false) |
| 107 | + elseif n_diff == 0 |
| 108 | + # Algebraic unknowns only (e.g. CharacteristicIdealDiodes): |
| 109 | + # the solver must take adaptive steps to track discontinuities. |
| 110 | + # Keep saveat=[] + dense=true so the solver drives its own |
| 111 | + # step selection; dense output is unreliable but the solution |
| 112 | + # values at each step are correct. |
| 113 | + (saveat = Float64[], dense = true) |
| 114 | + else |
| 115 | + (saveat = Float64[], dense = true) |
| 116 | + end |
90 | 117 |
|
91 | 118 | # Log solver settings — init returns NullODEIntegrator (no .opts) |
92 | 119 | # when the problem has no unknowns (u::Nothing), so only inspect |
93 | 120 | # opts when a real integrator is returned. |
| 121 | + # Use our own `saveat` vector for the log: integ.opts.saveat is a |
| 122 | + # BinaryHeap which does not support iterate/minimum/maximum. |
94 | 123 | integ = DifferentialEquations.init(ode_prob, solver; kwargs...) |
| 124 | + saveat = kwargs.saveat |
95 | 125 | solver_settings_string = if hasproperty(integ, :opts) |
96 | | - sv = integ.opts.saveat |
| 126 | + sv_str = isempty(saveat) ? "[]" : "$(length(saveat)) points in [$(first(saveat)), $(last(saveat))]" |
97 | 127 | """ |
98 | 128 | Solver $(parentmodule(typeof(solver))).$(nameof(typeof(solver))) |
99 | | - saveat: $(isempty(sv) ? "[]" : "$(length(sv)) points in [$(first(sv)), $(last(sv))]") |
| 129 | + saveat: $sv_str |
100 | 130 | abstol: $(@sprintf("%.2e", integ.opts.abstol)) |
101 | 131 | reltol: $(@sprintf("%.2e", integ.opts.reltol)) |
102 | 132 | adaptive: $(integ.opts.adaptive) |
|
0 commit comments