Skip to content

Commit 4db2f3b

Browse files
AnHeuermannclaude
andcommitted
Add SimulateSettings, configurable solver, and fixture-based tests
Introduce SimulateSettings struct so the ODE solver can be selected via configure_simulate!() or passed directly to run_simulate/test_model/main. Switch the default solver from Rodas5P to Rodas5Pr and log resolved solver settings (abstol, reltol, adaptive, dense, saveat) by calling init() before solve(). Split runtests.jl into per-concern files and add two fixture-based tests that run without OMC: BusUsage (no-state model, saveat-grid path) and AmplifierWithOpAmpDetailed (parse + simulate + reference verification). Copy the required .bmo files and MAP-LIB reference results into test/fixtures/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6c3b29d commit 4db2f3b

13 files changed

Lines changed: 5555 additions & 146 deletions

src/BaseModelicaLibraryTesting.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Pkg
44
import OMJulia
55
import OMJulia: sendExpression
66
import BaseModelica
7-
import DifferentialEquations: init, solve, ReturnCode
7+
import DifferentialEquations
88
import OrdinaryDiffEqBDF
99
import ModelingToolkit
1010
import Dates: now
@@ -22,11 +22,12 @@ include("pipeline.jl")
2222
# ── Public API ─────────────────────────────────────────────────────────────────
2323

2424
# Shared types and constants
25-
export ModelResult, CompareSettings, RunInfo
25+
export ModelResult, CompareSettings, SimulateSettings, RunInfo
2626
export LIBRARY, LIBRARY_VERSION, CMP_REL_TOL, CMP_ABS_TOL
2727

2828
# Comparison configuration
2929
export configure_comparison!, compare_settings
30+
export configure_simulate!, simulate_settings
3031

3132
# Pipeline phases
3233
export run_export # Phase 1: Base Modelica export via OMC

src/pipeline.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ end
5858
Run the four-phase pipeline for a single model and return its result.
5959
"""
6060
function test_model(omc::OMJulia.OMCSession, model::String, results_root::String,
61-
ref_root::String; csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
61+
ref_root::String;
62+
sim_settings ::SimulateSettings = _SIM_SETTINGS,
63+
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
6264
model_dir = joinpath(results_root, "files", model)
6365
mkpath(model_dir)
6466

@@ -93,6 +95,7 @@ function test_model(omc::OMJulia.OMCSession, model::String, results_root::String
9395

9496
# Phase 3 ──────────────────────────────────────────────────────────────────
9597
sim_ok, sim_t, sim_err, sol = run_simulate(ode_prob, model_dir, model;
98+
settings = sim_settings,
9699
csv_max_size_mb, cmp_signals)
97100

98101
# Phase 4 (optional) ───────────────────────────────────────────────────────
@@ -132,6 +135,7 @@ function main(;
132135
results_root :: String = "",
133136
ref_root :: String = get(ENV, "MAPLIB_REF", ""),
134137
bm_options :: String = get(ENV, "BM_OPTIONS", "scalarize,moveBindings,inlineFunctions"),
138+
sim_settings :: SimulateSettings = _SIM_SETTINGS,
135139
csv_max_size_mb :: Int = CSV_MAX_SIZE_MB,
136140
)
137141
t0 = time()
@@ -200,7 +204,7 @@ function main(;
200204

201205
for (i, model) in enumerate(models)
202206
@info "[$i/$(length(models))] $model"
203-
result = test_model(omc, model, results_root, ref_root; csv_max_size_mb)
207+
result = test_model(omc, model, results_root, ref_root; sim_settings, csv_max_size_mb)
204208
push!(results, result)
205209

206210
phase = if result.sim_success && result.cmp_total > 0

src/simulate.jl

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,50 @@
11
# ── Phase 3: ODE simulation with DifferentialEquations / MTK ──────────────────
22

3-
import DifferentialEquations: init, solve, ReturnCode
3+
import DifferentialEquations
44
import OrdinaryDiffEqBDF
55
import Logging
66
import ModelingToolkit
77
import Printf: @sprintf
88

9+
"""Module-level default simulation settings. Modify via `configure_simulate!`."""
10+
const _SIM_SETTINGS = SimulateSettings(solver = DifferentialEquations.Rodas5Pr())
11+
12+
"""
13+
configure_simulate!(; solver, saveat_n) → SimulateSettings
14+
15+
Update the module-level simulation settings in-place and return them.
16+
17+
# Keyword arguments
18+
- `solver` — any SciML ODE/DAE algorithm instance (e.g. `FBDF()`, `Rodas5P()`).
19+
- `saveat_n` — number of uniform time points for purely algebraic systems.
20+
21+
# Example
22+
23+
```julia
24+
using OrdinaryDiffEqBDF
25+
configure_simulate!(solver = FBDF())
26+
```
27+
"""
28+
function configure_simulate!(;
29+
solver :: Union{Any,Nothing} = nothing,
30+
saveat_n :: Union{Int,Nothing} = nothing,
31+
)
32+
isnothing(solver) || (_SIM_SETTINGS.solver = solver)
33+
isnothing(saveat_n) || (_SIM_SETTINGS.saveat_n = saveat_n)
34+
return _SIM_SETTINGS
35+
end
36+
37+
"""
38+
simulate_settings() → SimulateSettings
39+
40+
Return the current module-level simulation settings.
41+
"""
42+
simulate_settings() = _SIM_SETTINGS
43+
944
"""
10-
run_simulate(ode_prob, model_dir, model; cmp_signals, csv_max_size_mb) → (success, time, error, sol)
45+
run_simulate(ode_prob, model_dir, model; settings, cmp_signals, csv_max_size_mb) → (success, time, error, sol)
1146
12-
Solve `ode_prob` with FBDF (stiff solver). On success, also writes the
47+
Solve `ode_prob` using the algorithm in `settings.solver`. On success, also writes the
1348
solution as a CSV file `<Short>_sim.csv` in `model_dir`.
1449
Writes a `<model>_sim.log` file in `model_dir`.
1550
Returns `nothing` as the fourth element on failure.
@@ -21,10 +56,12 @@ of signals will be compared.
2156
CSV files larger than `csv_max_size_mb` MiB are replaced with a
2257
`<Short>_sim.csv.toobig` marker so that the report can note the omission.
2358
"""
24-
function run_simulate(ode_prob, model_dir::String,
59+
function run_simulate(ode_prob,
60+
model_dir::String,
2561
model::String;
26-
cmp_signals ::Vector{String} = String[],
27-
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
62+
settings ::SimulateSettings = _SIM_SETTINGS,
63+
cmp_signals ::Vector{String} = String[],
64+
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
2865
sim_success = false
2966
sim_time = 0.0
3067
sim_error = ""
@@ -35,8 +72,9 @@ function run_simulate(ode_prob, model_dir::String,
3572
println(log_file, "Model: $model")
3673
logger = Logging.SimpleLogger(log_file, Logging.Debug)
3774
t0 = time()
75+
76+
solver = settings.solver
3877
try
39-
# FBDF handles stiff DAE-like systems and purely algebraic systems well.
4078
# Redirect all library log output (including Symbolics/MTK warnings)
4179
# to the log file so they don't clutter stdout.
4280
sol = Logging.with_logger(logger) do
@@ -46,27 +84,36 @@ function run_simulate(ode_prob, model_dir::String,
4684
# Supply explicit time points so observed variables can be evaluated.
4785
sys = ode_prob.f.sys
4886
saveat = isempty(ModelingToolkit.unknowns(sys)) ?
49-
collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = 500)) :
87+
collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = settings.saveat_n)) :
5088
Float64[]
5189
kwargs = (saveat = saveat, dense = true)
5290

53-
# Log solver settings
54-
initializedSolver = init(ode_prob, OrdinaryDiffEqBDF.FBDF(); kwargs...)
55-
solver_settings_string =
56-
"""
57-
OrdinaryDiffEqBDF.FBDF()
58-
saveat: $(let sv = initializedSolver.opts.saveat; isempty(sv) ? "[]" : "$(length(sv)) points in [$(first(sv)), $(last(sv))]" end)
59-
abstol: $(@sprintf("%.2e", initializedSolver.opts.abstol))
60-
reltol: $(@sprintf("%.2e", initializedSolver.opts.reltol))
61-
adaptive: $(initializedSolver.opts.adaptive)
62-
dense: $(initializedSolver.opts.dense)
63-
"""
91+
# Log solver settings — init returns NullODEIntegrator (no .opts)
92+
# when the problem has no unknowns (u::Nothing), so only inspect
93+
# opts when a real integrator is returned.
94+
integ = DifferentialEquations.init(ode_prob, solver; kwargs...)
95+
solver_settings_string = if hasproperty(integ, :opts)
96+
sv = integ.opts.saveat
97+
"""
98+
Solver $(parentmodule(typeof(solver))).$(nameof(typeof(solver)))
99+
saveat: $(isempty(sv) ? "[]" : "$(length(sv)) points in [$(first(sv)), $(last(sv))]")
100+
abstol: $(@sprintf("%.2e", integ.opts.abstol))
101+
reltol: $(@sprintf("%.2e", integ.opts.reltol))
102+
adaptive: $(integ.opts.adaptive)
103+
dense: $(integ.opts.dense)
104+
"""
105+
else
106+
sv_str = isempty(saveat) ? "[]" : "$(length(saveat)) points in [$(first(saveat)), $(last(saveat))]"
107+
"Solver (NullODEIntegrator — no unknowns)
108+
saveat: $sv_str
109+
dense: true"
110+
end
64111

65112
# Solve
66-
solve(ode_prob, OrdinaryDiffEqBDF.FBDF(); kwargs...)
113+
DifferentialEquations.solve(ode_prob, OrdinaryDiffEqBDF.FBDF(); kwargs...)
67114
end
68115
sim_time = time() - t0
69-
if sol.retcode == ReturnCode.Success
116+
if sol.retcode == DifferentialEquations.ReturnCode.Success
70117
sys = sol.prob.f.sys
71118
n_vars = length(ModelingToolkit.unknowns(sys))
72119
n_obs = length(ModelingToolkit.observed(sys))
@@ -84,7 +131,7 @@ function run_simulate(ode_prob, model_dir::String,
84131
sim_time = time() - t0
85132
sim_error = sprint(showerror, e, catch_backtrace())
86133
end
87-
println(log_file, "Solver settings: $solver_settings_string")
134+
println(log_file, solver_settings_string)
88135
println(log_file, "Time: $(round(sim_time; digits=3)) s")
89136
println(log_file, "Success: $sim_success")
90137
isempty(sim_error) || println(log_file, "\n--- Error ---\n$sim_error")

src/types.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ Base.@kwdef mutable struct CompareSettings
3737
error_fn :: Symbol = :mixed
3838
end
3939

40+
# ── Simulation settings ────────────────────────────────────────────────────────
41+
42+
"""
43+
SimulateSettings
44+
45+
Mutable configuration struct for ODE simulation.
46+
47+
# Fields
48+
- `solver` — any SciML ODE/DAE algorithm instance. Default: `nothing`,
49+
resolved to `Rodas5Pr()` when the module-level singleton is
50+
constructed in `simulate.jl`.
51+
- `saveat_n` — number of evenly-spaced time points used for purely algebraic
52+
systems (all mass-matrix rows zero). Default: `500`.
53+
"""
54+
Base.@kwdef mutable struct SimulateSettings
55+
solver :: Any = nothing
56+
saveat_n :: Int = 500
57+
end
58+
4059
# ── Run metadata ───────────────────────────────────────────────────────────────
4160

4261
"""

test/amplifier_with_op_amp.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@testset "AmplifierWithOpAmpDetailed verification" begin
2+
model = "Modelica.Electrical.Analog.Examples.AmplifierWithOpAmpDetailed"
3+
bmo_path = joinpath(FIXTURES, "$model.bmo")
4+
ref_dir = joinpath(FIXTURES, "AmplifierWithOpAmpDetailed")
5+
ref_csv = joinpath(ref_dir, "AmplifierWithOpAmpDetailed.csv")
6+
sig_file = joinpath(ref_dir, "comparisonSignals.txt")
7+
signals = String.(filter(s -> lowercase(s) != "time" && !isempty(s),
8+
strip.(readlines(sig_file))))
9+
mktempdir() do tmpdir
10+
par_ok, _, par_err, ode_prob = run_parse(bmo_path, tmpdir, model)
11+
@test par_ok
12+
par_ok || @warn "Parse error: $par_err"
13+
14+
if par_ok
15+
sim_ok, _, sim_err, sol = run_simulate(ode_prob, tmpdir, model;
16+
cmp_signals = signals)
17+
@test sim_ok
18+
sim_ok || @warn "Simulation error: $sim_err"
19+
20+
if sim_ok
21+
total, pass, skip, _ = compare_with_reference(
22+
sol, ref_csv, tmpdir, model; signals)
23+
@test pass == total
24+
@info "AmplifierWithOpAmpDetailed: $pass/$total signals pass (skip=$skip)"
25+
end
26+
end
27+
end
28+
end

test/bus_usage.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@testset "BusUsage simulation (no states)" begin
2+
# Modelica.Blocks.Examples.BusUsage has no unknowns after structural_simplify.
3+
# The saveat-grid path in run_simulate must handle this without error.
4+
model = "Modelica.Blocks.Examples.BusUsage"
5+
bmo_path = joinpath(FIXTURES, "$model.bmo")
6+
mktempdir() do tmpdir
7+
par_ok, _, par_err, ode_prob = run_parse(bmo_path, tmpdir, model)
8+
@test par_ok
9+
par_ok || @warn "Parse error: $par_err"
10+
11+
if par_ok
12+
sim_ok, _, sim_err, _ = run_simulate(ode_prob, tmpdir, model)
13+
@test sim_ok
14+
sim_ok || @warn "Simulation error: $sim_err"
15+
end
16+
end
17+
end

test/chua_circuit.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@testset "ChuaCircuit pipeline" begin
2+
tmpdir = mktempdir()
3+
model_dir = joinpath(tmpdir, "files", TEST_MODEL_CHUA)
4+
mkpath(model_dir)
5+
bm_path = replace(abspath(joinpath(model_dir, "$TEST_MODEL_CHUA.bmo")), "\\" => "/")
6+
7+
omc = OMJulia.OMCSession(TEST_OMC)
8+
try
9+
OMJulia.sendExpression(omc, """setCommandLineOptions("--baseModelica --baseModelicaOptions=scalarize,moveBindings -d=evaluateAllParameters")""")
10+
ok = OMJulia.sendExpression(omc, """loadModel(Modelica, {"4.1.0"})""")
11+
@test ok == true
12+
13+
exp_ok, _, exp_err = run_export(omc, TEST_MODEL_CHUA, model_dir, bm_path)
14+
@test exp_ok
15+
exp_ok || @warn "Export error: $exp_err"
16+
17+
if exp_ok
18+
par_ok, _, par_err, ode_prob = run_parse(bm_path, model_dir, TEST_MODEL_CHUA)
19+
@test par_ok
20+
par_ok || @warn "Parse error: $par_err"
21+
22+
if par_ok
23+
sim_ok, _, sim_err, _ = run_simulate(ode_prob, model_dir, TEST_MODEL_CHUA)
24+
@test sim_ok
25+
sim_ok || @warn "Simulation error: $sim_err"
26+
end
27+
end
28+
finally
29+
OMJulia.quit(omc)
30+
end
31+
end

0 commit comments

Comments
 (0)