Skip to content

Commit 83437b3

Browse files
AnHeuermannclaude
andauthored
Make solver configurable (#33)
* 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 b257ceb commit 83437b3

15 files changed

Lines changed: 328 additions & 146 deletions

.github/workflows/CI.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ jobs:
4949
version: ${{ matrix.version }}
5050
arch: ${{ matrix.arch }}
5151
- uses: julia-actions/cache@v3
52-
- uses: julia-actions/julia-buildpkg@v1
52+
- name: Build package
53+
run: |
54+
julia --project=. -e '
55+
using Pkg
56+
Pkg.add(name="BaseModelica", rev="main")
57+
Pkg.instantiate()
58+
'
5359
- uses: julia-actions/julia-runtest@v1
5460

5561
sanity:
@@ -82,7 +88,13 @@ jobs:
8288
version: '1.12'
8389
arch: x64
8490
- uses: julia-actions/cache@v3
85-
- uses: julia-actions/julia-buildpkg@v1
91+
- name: Build package
92+
run: |
93+
julia --project=. -e '
94+
using Pkg
95+
Pkg.add(name="BaseModelica", rev="main")
96+
Pkg.instantiate()
97+
'
8698
8799
- name: Sanity check ChuaCircuit
88100
run: |

.github/workflows/msl-test.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ on:
3636
required: false
3737
default: '^(?!Modelica\.Clocked)'
3838
type: string
39+
solver:
40+
description: 'ODE solver algorithm (any DifferentialEquations.jl algorithm name, e.g. Rodas5P, FBDF)'
41+
required: false
42+
default: 'Rodas5P'
43+
type: string
3944

4045
concurrency:
4146
group: pages-${{ inputs.library || 'Modelica' }}-${{ inputs.lib_version || '4.1.0' }}-${{ inputs.bm_version || 'main' }}
@@ -58,6 +63,7 @@ jobs:
5863
BM_VERSION_INPUT: ${{ inputs.bm_version || 'main' }}
5964
BM_OPTIONS: ${{ inputs.bm_options || 'scalarize,moveBindings,inlineFunctions' }}
6065
FILTER: ${{ inputs.filter || '^(?!Modelica\.Clocked)' }}
66+
SOLVER: ${{ inputs.solver || 'Rodas5P' }}
6167

6268
steps:
6369
- name: Checkout source
@@ -123,6 +129,9 @@ jobs:
123129
run: |
124130
julia --project=. -e '
125131
using BaseModelicaLibraryTesting
132+
using DifferentialEquations
133+
solver_name = get(ENV, "SOLVER", "Rodas5P")
134+
configure_simulate!(solver = getproperty(DifferentialEquations, Symbol(solver_name))())
126135
filter_str = get(ENV, "FILTER", "")
127136
main(
128137
library = ENV["LIB_NAME"],

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ authors = ["AnHeuermann"]
77
BaseModelica = "a17d5099-185d-4ff5-b5d3-51aa4569e56d"
88
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
99
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
10+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1011
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1112
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
1213
OMJulia = "0f4fe800-344e-11e9-2949-fb537ad918e1"

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,55 @@ main(
5555

5656
Preview the generated HTML report at `main/Modelica/4.1.0/report.html`.
5757

58+
### Changing the ODE Solver
59+
60+
By default the simulation uses `Rodas5P()`. To switch to a different solver,
61+
call `configure_simulate!` before `main`:
62+
63+
```julia
64+
using BaseModelicaLibraryTesting
65+
using DifferentialEquations
66+
67+
configure_simulate!(solver = FBDF())
68+
69+
main(
70+
library = "Modelica",
71+
version = "4.1.0",
72+
omc_exe = "omc",
73+
ref_root = "MAP-LIB_ReferenceResults"
74+
)
75+
```
76+
77+
Any SciML-compatible ODE/DAE algorithm (e.g. `QNDF()`, `Rodas4()`) can be
78+
passed to `solver`.
79+
5880
```bash
5981
python -m http.server -d results/main/Modelica/4.1.0/
6082
```
6183

84+
## GitHub Actions — Manual MSL Test
85+
86+
The [MSL Test & GitHub Pages][msl-action-url] workflow runs automatically every
87+
day at 03:00 UTC. It can also be triggered manually from the GitHub Actions UI:
88+
89+
1. Go to **Actions → MSL Test & GitHub Pages**
90+
2. Click **Run workflow**
91+
3. Fill in the options and click **Run workflow**
92+
93+
The following inputs are available:
94+
95+
| Input | Default | Description |
96+
| ----- | ------- | ----------- |
97+
| `library` | `Modelica` | Modelica library name |
98+
| `lib_version` | `4.1.0` | Library version to test |
99+
| `bm_version` | `main` | BaseModelica.jl branch, tag, or version |
100+
| `bm_options` | `scalarize,moveBindings,inlineFunctions` | Comma-separated `--baseModelicaOptions` passed to OpenModelica during Base Modelica export |
101+
| `filter` | `^(?!Modelica\.Clocked)` | Julia regex to restrict which models are tested (empty string runs all models) |
102+
| `solver` | `Rodas5P` | Any `DifferentialEquations.jl` algorithm name (e.g. `Rodas5P`, `Rodas5Pr`, `FBDF`) |
103+
104+
Results are published to [GitHub Pages][msl-pages-url] under
105+
`results/<bm_version>/<library>/<lib_version>/`.
106+
62107
## License
63108

64109
This package is available under the [OSMC-PL License][osmc-license-file] and the
@@ -68,6 +113,7 @@ file for details.
68113
[build-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml/badge.svg?branch=main
69114
[build-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml?query=branch%3Amain
70115
[msl-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml/badge.svg?branch=main
116+
[msl-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml
71117
[msl-pages-url]: https://openmodelica.github.io/BaseModelicaLibraryTesting.jl/
72118
[openmodelica-url]: https://openmodelica.org/
73119
[basemodelicajl-url]: https://github.com/SciML/BaseModelica.jl

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: solve, Rodas5P, ReturnCode
7+
import DifferentialEquations
88
import ModelingToolkit
99
import Dates: now
1010
import Printf: @sprintf
@@ -21,11 +21,12 @@ include("pipeline.jl")
2121
# ── Public API ─────────────────────────────────────────────────────────────────
2222

2323
# Shared types and constants
24-
export ModelResult, CompareSettings, RunInfo
24+
export ModelResult, CompareSettings, SimulateSettings, RunInfo
2525
export LIBRARY, LIBRARY_VERSION, CMP_REL_TOL, CMP_ABS_TOL
2626

2727
# Comparison configuration
2828
export configure_comparison!, compare_settings
29+
export configure_simulate!, simulate_settings
2930

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

src/compare.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ function compare_with_reference(
368368
signals::Vector{String} = String[],
369369
)::Tuple{Int,Int,Int,String}
370370

371+
isdir(model_dir) || mkpath(model_dir)
372+
371373
times, ref_data = _read_ref_csv(ref_csv_path)
372374
isempty(times) && return 0, 0, 0, ""
373375

src/parse_bm.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function run_parse(bm_path::String, model_dir::String,
1818
parse_error = ""
1919
ode_prob = nothing
2020

21+
isdir(model_dir) || mkpath(model_dir)
2122
log_file = open(joinpath(model_dir, "$(model)_parsing.log"), "w")
2223
stdout_pipe = Pipe()
2324
println(log_file, "Model: $model")

src/pipeline.jl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ end
5757
5858
Run the four-phase pipeline for a single model and return its result.
5959
"""
60-
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
60+
function test_model(omc::OMJulia.OMCSession,
61+
model::String,
62+
results_root::String,
63+
ref_root::String;
64+
sim_settings ::SimulateSettings = _SIM_SETTINGS,
65+
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
6266
model_dir = joinpath(results_root, "files", model)
6367
mkpath(model_dir)
6468

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

9498
# Phase 3 ──────────────────────────────────────────────────────────────────
9599
sim_ok, sim_t, sim_err, sol = run_simulate(ode_prob, model_dir, model;
100+
settings = sim_settings,
96101
csv_max_size_mb, cmp_signals)
97102

98103
# Phase 4 (optional) ───────────────────────────────────────────────────────
@@ -132,6 +137,7 @@ function main(;
132137
results_root :: String = "",
133138
ref_root :: String = get(ENV, "MAPLIB_REF", ""),
134139
bm_options :: String = get(ENV, "BM_OPTIONS", "scalarize,moveBindings,inlineFunctions"),
140+
sim_settings :: SimulateSettings = _SIM_SETTINGS,
135141
csv_max_size_mb :: Int = CSV_MAX_SIZE_MB,
136142
)
137143
t0 = time()
@@ -200,7 +206,7 @@ function main(;
200206

201207
for (i, model) in enumerate(models)
202208
@info "[$i/$(length(models))] $model"
203-
result = test_model(omc, model, results_root, ref_root; csv_max_size_mb)
209+
result = test_model(omc, model, results_root, ref_root; sim_settings, csv_max_size_mb)
204210
push!(results, result)
205211

206212
phase = if result.sim_success && result.cmp_total > 0
@@ -245,6 +251,9 @@ function main(;
245251
length(cpu_info),
246252
Sys.total_memory() / 1024^3,
247253
time() - t0,
254+
let s = _SIM_SETTINGS.solver
255+
"$(parentmodule(typeof(s))).$(nameof(typeof(s)))"
256+
end,
248257
)
249258

250259
generate_report(results, results_root, info; csv_max_size_mb)

src/report.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ function generate_report(results::Vector{ModelResult}, results_root::String,
159159
OpenModelica: $(info.omc_version)<br>
160160
OMC options: <code>$(info.omc_options)</code><br>
161161
BaseModelica.jl: $(basemodelica_jl_version)<br>
162+
Solver: <code>$(info.solver)</code><br>
162163
Filter: $(var_filter)<br>
163164
Reference results: $(ref_results)</p>
164165
<p>CPU: $(info.cpu_model) ($(info.cpu_threads) threads)<br>

src/simulate.jl

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

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

8+
"""Module-level default simulation settings. Modify via `configure_simulate!`."""
9+
const _SIM_SETTINGS = SimulateSettings(solver = DifferentialEquations.Rodas5P())
10+
11+
"""
12+
configure_simulate!(; solver, saveat_n) → SimulateSettings
13+
14+
Update the module-level simulation settings in-place and return them.
15+
16+
# Keyword arguments
17+
- `solver` — any SciML ODE/DAE algorithm instance (e.g. `Rodas5P`, `FBDF()`).
18+
- `saveat_n` — number of uniform time points for purely algebraic systems.
19+
20+
# Example
21+
22+
```julia
23+
using OrdinaryDiffEqBDF
24+
configure_simulate!(solver = FBDF())
25+
```
26+
"""
27+
function configure_simulate!(;
28+
solver :: Union{Any,Nothing} = nothing,
29+
saveat_n :: Union{Int,Nothing} = nothing,
30+
)
31+
isnothing(solver) || (_SIM_SETTINGS.solver = solver)
32+
isnothing(saveat_n) || (_SIM_SETTINGS.saveat_n = saveat_n)
33+
return _SIM_SETTINGS
34+
end
35+
36+
"""
37+
simulate_settings() → SimulateSettings
38+
39+
Return the current module-level simulation settings.
840
"""
9-
run_simulate(ode_prob, model_dir, model; cmp_signals, csv_max_size_mb) → (success, time, error, sol)
41+
simulate_settings() = _SIM_SETTINGS
1042

11-
Solve `ode_prob` with Rodas5P (stiff solver). On success, also writes the
43+
"""
44+
run_simulate(ode_prob, model_dir, model; settings, cmp_signals, csv_max_size_mb) → (success, time, error, sol)
45+
46+
Solve `ode_prob` using the algorithm in `settings.solver`. On success, also writes the
1247
solution as a CSV file `<Short>_sim.csv` in `model_dir`.
1348
Writes a `<model>_sim.log` file in `model_dir`.
1449
Returns `nothing` as the fourth element on failure.
@@ -20,36 +55,74 @@ of signals will be compared.
2055
CSV files larger than `csv_max_size_mb` MiB are replaced with a
2156
`<Short>_sim.csv.toobig` marker so that the report can note the omission.
2257
"""
23-
function run_simulate(ode_prob, model_dir::String,
58+
function run_simulate(ode_prob,
59+
model_dir::String,
2460
model::String;
25-
cmp_signals ::Vector{String} = String[],
26-
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
27-
sim_success = false
28-
sim_time = 0.0
29-
sim_error = ""
30-
sol = nothing
61+
settings ::SimulateSettings = _SIM_SETTINGS,
62+
cmp_signals ::Vector{String} = String[],
63+
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
64+
65+
sim_success = false
66+
sim_time = 0.0
67+
sim_error = ""
68+
sol = nothing
69+
solver_settings_string = ""
3170

3271
log_file = open(joinpath(model_dir, "$(model)_sim.log"), "w")
3372
println(log_file, "Model: $model")
3473
logger = Logging.SimpleLogger(log_file, Logging.Debug)
3574
t0 = time()
75+
76+
solver = settings.solver
3677
try
37-
# Rodas5P handles stiff DAE-like systems well.
3878
# Redirect all library log output (including Symbolics/MTK warnings)
3979
# to the log file so they don't clutter stdout.
4080
sol = Logging.with_logger(logger) do
4181
# Overwrite saveat, always use dense output.
4282
# For stateless models (no unknowns) the adaptive solver takes no
4383
# internal steps and sol.t would be empty with saveat=[].
4484
# Supply explicit time points so observed variables can be evaluated.
45-
sys = ode_prob.f.sys
46-
saveat = isempty(ModelingToolkit.unknowns(sys)) ?
47-
collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = 500)) :
48-
Float64[]
49-
solve(ode_prob, Rodas5P(); saveat = saveat, dense = true)
85+
sys = ode_prob.f.sys
86+
n_unknowns = length(ModelingToolkit.unknowns(sys))
87+
88+
kwargs = if n_unknowns == 0
89+
# No unknowns at all (e.g. BusUsage):
90+
# Supply explicit time points so observed variables can be evaluated.
91+
saveat_s = collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = settings.saveat_n))
92+
(saveat = saveat_s, dense = true)
93+
else
94+
(saveat = Float64[], dense = true)
95+
end
96+
97+
# Log solver settings — init returns NullODEIntegrator (no .opts)
98+
# when the problem has no unknowns (u::Nothing), so only inspect
99+
# opts when a real integrator is returned.
100+
# Use our own `saveat` vector for the log: integ.opts.saveat is a
101+
# BinaryHeap which does not support iterate/minimum/maximum.
102+
integ = DifferentialEquations.init(ode_prob, solver; kwargs...)
103+
saveat_s = kwargs.saveat
104+
solver_settings_string = if hasproperty(integ, :opts)
105+
sv_str = isempty(saveat_s) ? "[]" : "$(length(saveat_s)) points in [$(first(saveat_s)), $(last(saveat_s))]"
106+
"""
107+
Solver $(parentmodule(typeof(solver))).$(nameof(typeof(solver)))
108+
saveat: $sv_str
109+
abstol: $(@sprintf("%.2e", integ.opts.abstol))
110+
reltol: $(@sprintf("%.2e", integ.opts.reltol))
111+
adaptive: $(integ.opts.adaptive)
112+
dense: $(integ.opts.dense)
113+
"""
114+
else
115+
sv_str = isempty(saveat_s) ? "[]" : "$(length(saveat_s)) points in [$(first(saveat_s)), $(last(saveat_s))]"
116+
"Solver (NullODEIntegrator — no unknowns)
117+
saveat: $sv_str
118+
dense: true"
119+
end
120+
121+
# Solve
122+
DifferentialEquations.solve(ode_prob, solver; kwargs...)
50123
end
51124
sim_time = time() - t0
52-
if sol.retcode == ReturnCode.Success
125+
if sol.retcode == DifferentialEquations.ReturnCode.Success
53126
sys = sol.prob.f.sys
54127
n_vars = length(ModelingToolkit.unknowns(sys))
55128
n_obs = length(ModelingToolkit.observed(sys))
@@ -67,7 +140,8 @@ function run_simulate(ode_prob, model_dir::String,
67140
sim_time = time() - t0
68141
sim_error = sprint(showerror, e, catch_backtrace())
69142
end
70-
println(log_file, "Time: $(round(sim_time; digits=3)) s")
143+
println(log_file, solver_settings_string)
144+
println(log_file, "Time: $(round(sim_time; digits=3)) s")
71145
println(log_file, "Success: $sim_success")
72146
isempty(sim_error) || println(log_file, "\n--- Error ---\n$sim_error")
73147
close(log_file)

0 commit comments

Comments
 (0)