Skip to content
Merged
16 changes: 14 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ jobs:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-buildpkg@v1
- name: Build package
run: |
julia --project=. -e '
using Pkg
Pkg.add(name="BaseModelica", rev="main")
Pkg.instantiate()
'
- uses: julia-actions/julia-runtest@v1

sanity:
Expand Down Expand Up @@ -82,7 +88,13 @@ jobs:
version: '1.12'
arch: x64
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-buildpkg@v1
- name: Build package
run: |
julia --project=. -e '
using Pkg
Pkg.add(name="BaseModelica", rev="main")
Pkg.instantiate()
'

- name: Sanity check ChuaCircuit
run: |
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/msl-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ on:
required: false
default: '^(?!Modelica\.Clocked)'
type: string
solver:
description: 'ODE solver algorithm (any DifferentialEquations.jl algorithm name, e.g. Rodas5P, FBDF)'
required: false
default: 'Rodas5P'
type: string

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

steps:
- name: Checkout source
Expand Down Expand Up @@ -123,6 +129,9 @@ jobs:
run: |
julia --project=. -e '
using BaseModelicaLibraryTesting
using DifferentialEquations
solver_name = get(ENV, "SOLVER", "Rodas5P")
configure_simulate!(solver = getproperty(DifferentialEquations, Symbol(solver_name))())
filter_str = get(ENV, "FILTER", "")
main(
library = ENV["LIB_NAME"],
Expand Down
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ authors = ["AnHeuermann"]
BaseModelica = "a17d5099-185d-4ff5-b5d3-51aa4569e56d"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
OMJulia = "0f4fe800-344e-11e9-2949-fb537ad918e1"
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,55 @@ main(

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

### Changing the ODE Solver

By default the simulation uses `Rodas5P()`. To switch to a different solver,
call `configure_simulate!` before `main`:

```julia
using BaseModelicaLibraryTesting
using DifferentialEquations

configure_simulate!(solver = FBDF())

main(
library = "Modelica",
version = "4.1.0",
omc_exe = "omc",
ref_root = "MAP-LIB_ReferenceResults"
)
```

Any SciML-compatible ODE/DAE algorithm (e.g. `QNDF()`, `Rodas4()`) can be
passed to `solver`.

```bash
python -m http.server -d results/main/Modelica/4.1.0/
```

## GitHub Actions — Manual MSL Test

The [MSL Test & GitHub Pages][msl-action-url] workflow runs automatically every
day at 03:00 UTC. It can also be triggered manually from the GitHub Actions UI:

1. Go to **Actions → MSL Test & GitHub Pages**
2. Click **Run workflow**
3. Fill in the options and click **Run workflow**

The following inputs are available:

| Input | Default | Description |
| ----- | ------- | ----------- |
| `library` | `Modelica` | Modelica library name |
| `lib_version` | `4.1.0` | Library version to test |
| `bm_version` | `main` | BaseModelica.jl branch, tag, or version |
| `bm_options` | `scalarize,moveBindings,inlineFunctions` | Comma-separated `--baseModelicaOptions` passed to OpenModelica during Base Modelica export |
| `filter` | `^(?!Modelica\.Clocked)` | Julia regex to restrict which models are tested (empty string runs all models) |
| `solver` | `Rodas5P` | Any `DifferentialEquations.jl` algorithm name (e.g. `Rodas5P`, `Rodas5Pr`, `FBDF`) |

Results are published to [GitHub Pages][msl-pages-url] under
`results/<bm_version>/<library>/<lib_version>/`.

## License

This package is available under the [OSMC-PL License][osmc-license-file] and the
Expand All @@ -68,6 +113,7 @@ file for details.
[build-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml/badge.svg?branch=main
[build-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml?query=branch%3Amain
[msl-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml/badge.svg?branch=main
[msl-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml
[msl-pages-url]: https://openmodelica.github.io/BaseModelicaLibraryTesting.jl/
[openmodelica-url]: https://openmodelica.org/
[basemodelicajl-url]: https://github.com/SciML/BaseModelica.jl
Expand Down
5 changes: 3 additions & 2 deletions src/BaseModelicaLibraryTesting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Pkg
import OMJulia
import OMJulia: sendExpression
import BaseModelica
import DifferentialEquations: solve, Rodas5P, ReturnCode
import DifferentialEquations
import ModelingToolkit
import Dates: now
import Printf: @sprintf
Expand All @@ -21,11 +21,12 @@ include("pipeline.jl")
# ── Public API ─────────────────────────────────────────────────────────────────

# Shared types and constants
export ModelResult, CompareSettings, RunInfo
export ModelResult, CompareSettings, SimulateSettings, RunInfo
export LIBRARY, LIBRARY_VERSION, CMP_REL_TOL, CMP_ABS_TOL

# Comparison configuration
export configure_comparison!, compare_settings
export configure_simulate!, simulate_settings

# Pipeline phases
export run_export # Phase 1: Base Modelica export via OMC
Expand Down
2 changes: 2 additions & 0 deletions src/compare.jl
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ function compare_with_reference(
signals::Vector{String} = String[],
)::Tuple{Int,Int,Int,String}

isdir(model_dir) || mkpath(model_dir)

times, ref_data = _read_ref_csv(ref_csv_path)
isempty(times) && return 0, 0, 0, ""

Expand Down
1 change: 1 addition & 0 deletions src/parse_bm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function run_parse(bm_path::String, model_dir::String,
parse_error = ""
ode_prob = nothing

isdir(model_dir) || mkpath(model_dir)
log_file = open(joinpath(model_dir, "$(model)_parsing.log"), "w")
stdout_pipe = Pipe()
println(log_file, "Model: $model")
Expand Down
15 changes: 12 additions & 3 deletions src/pipeline.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ end

Run the four-phase pipeline for a single model and return its result.
"""
function test_model(omc::OMJulia.OMCSession, model::String, results_root::String,
ref_root::String; csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
function test_model(omc::OMJulia.OMCSession,
model::String,
results_root::String,
ref_root::String;
sim_settings ::SimulateSettings = _SIM_SETTINGS,
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
model_dir = joinpath(results_root, "files", model)
mkpath(model_dir)

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

# Phase 3 ──────────────────────────────────────────────────────────────────
sim_ok, sim_t, sim_err, sol = run_simulate(ode_prob, model_dir, model;
settings = sim_settings,
csv_max_size_mb, cmp_signals)

# Phase 4 (optional) ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -132,6 +137,7 @@ function main(;
results_root :: String = "",
ref_root :: String = get(ENV, "MAPLIB_REF", ""),
bm_options :: String = get(ENV, "BM_OPTIONS", "scalarize,moveBindings,inlineFunctions"),
sim_settings :: SimulateSettings = _SIM_SETTINGS,
csv_max_size_mb :: Int = CSV_MAX_SIZE_MB,
)
t0 = time()
Expand Down Expand Up @@ -200,7 +206,7 @@ function main(;

for (i, model) in enumerate(models)
@info "[$i/$(length(models))] $model"
result = test_model(omc, model, results_root, ref_root; csv_max_size_mb)
result = test_model(omc, model, results_root, ref_root; sim_settings, csv_max_size_mb)
push!(results, result)

phase = if result.sim_success && result.cmp_total > 0
Expand Down Expand Up @@ -245,6 +251,9 @@ function main(;
length(cpu_info),
Sys.total_memory() / 1024^3,
time() - t0,
let s = _SIM_SETTINGS.solver
"$(parentmodule(typeof(s))).$(nameof(typeof(s)))"
end,
)

generate_report(results, results_root, info; csv_max_size_mb)
Expand Down
1 change: 1 addition & 0 deletions src/report.jl
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function generate_report(results::Vector{ModelResult}, results_root::String,
OpenModelica: $(info.omc_version)<br>
OMC options: <code>$(info.omc_options)</code><br>
BaseModelica.jl: $(basemodelica_jl_version)<br>
Solver: <code>$(info.solver)</code><br>
Filter: $(var_filter)<br>
Reference results: $(ref_results)</p>
<p>CPU: $(info.cpu_model) ($(info.cpu_threads) threads)<br>
Expand Down
110 changes: 92 additions & 18 deletions src/simulate.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
# ── Phase 3: ODE simulation with DifferentialEquations / MTK ──────────────────

import DifferentialEquations: solve, Rodas5P, ReturnCode
import DifferentialEquations
import Logging
import ModelingToolkit
import Printf: @sprintf

"""Module-level default simulation settings. Modify via `configure_simulate!`."""
const _SIM_SETTINGS = SimulateSettings(solver = DifferentialEquations.Rodas5P())

"""
configure_simulate!(; solver, saveat_n) → SimulateSettings

Update the module-level simulation settings in-place and return them.

# Keyword arguments
- `solver` — any SciML ODE/DAE algorithm instance (e.g. `Rodas5P`, `FBDF()`).
- `saveat_n` — number of uniform time points for purely algebraic systems.

# Example

```julia
using OrdinaryDiffEqBDF
configure_simulate!(solver = FBDF())
```
"""
function configure_simulate!(;
solver :: Union{Any,Nothing} = nothing,
saveat_n :: Union{Int,Nothing} = nothing,
)
isnothing(solver) || (_SIM_SETTINGS.solver = solver)
isnothing(saveat_n) || (_SIM_SETTINGS.saveat_n = saveat_n)
return _SIM_SETTINGS
end

"""
simulate_settings() → SimulateSettings

Return the current module-level simulation settings.
"""
run_simulate(ode_prob, model_dir, model; cmp_signals, csv_max_size_mb) → (success, time, error, sol)
simulate_settings() = _SIM_SETTINGS

Solve `ode_prob` with Rodas5P (stiff solver). On success, also writes the
"""
run_simulate(ode_prob, model_dir, model; settings, cmp_signals, csv_max_size_mb) → (success, time, error, sol)

Solve `ode_prob` using the algorithm in `settings.solver`. On success, also writes the
solution as a CSV file `<Short>_sim.csv` in `model_dir`.
Writes a `<model>_sim.log` file in `model_dir`.
Returns `nothing` as the fourth element on failure.
Expand All @@ -20,36 +55,74 @@ of signals will be compared.
CSV files larger than `csv_max_size_mb` MiB are replaced with a
`<Short>_sim.csv.toobig` marker so that the report can note the omission.
"""
function run_simulate(ode_prob, model_dir::String,
function run_simulate(ode_prob,
model_dir::String,
model::String;
cmp_signals ::Vector{String} = String[],
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
sim_success = false
sim_time = 0.0
sim_error = ""
sol = nothing
settings ::SimulateSettings = _SIM_SETTINGS,
cmp_signals ::Vector{String} = String[],
csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}

sim_success = false
sim_time = 0.0
sim_error = ""
sol = nothing
solver_settings_string = ""

log_file = open(joinpath(model_dir, "$(model)_sim.log"), "w")
println(log_file, "Model: $model")
logger = Logging.SimpleLogger(log_file, Logging.Debug)
t0 = time()

solver = settings.solver
try
# Rodas5P handles stiff DAE-like systems well.
# Redirect all library log output (including Symbolics/MTK warnings)
# to the log file so they don't clutter stdout.
sol = Logging.with_logger(logger) do
# Overwrite saveat, always use dense output.
# For stateless models (no unknowns) the adaptive solver takes no
# internal steps and sol.t would be empty with saveat=[].
# Supply explicit time points so observed variables can be evaluated.
sys = ode_prob.f.sys
saveat = isempty(ModelingToolkit.unknowns(sys)) ?
collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = 500)) :
Float64[]
solve(ode_prob, Rodas5P(); saveat = saveat, dense = true)
sys = ode_prob.f.sys
n_unknowns = length(ModelingToolkit.unknowns(sys))

kwargs = if n_unknowns == 0
# No unknowns at all (e.g. BusUsage):
# Supply explicit time points so observed variables can be evaluated.
saveat_s = collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = settings.saveat_n))
(saveat = saveat_s, dense = true)
else
(saveat = Float64[], dense = true)
end

# Log solver settings — init returns NullODEIntegrator (no .opts)
# when the problem has no unknowns (u::Nothing), so only inspect
# opts when a real integrator is returned.
# Use our own `saveat` vector for the log: integ.opts.saveat is a
# BinaryHeap which does not support iterate/minimum/maximum.
integ = DifferentialEquations.init(ode_prob, solver; kwargs...)
saveat_s = kwargs.saveat
solver_settings_string = if hasproperty(integ, :opts)
sv_str = isempty(saveat_s) ? "[]" : "$(length(saveat_s)) points in [$(first(saveat_s)), $(last(saveat_s))]"
"""
Solver $(parentmodule(typeof(solver))).$(nameof(typeof(solver)))
saveat: $sv_str
abstol: $(@sprintf("%.2e", integ.opts.abstol))
reltol: $(@sprintf("%.2e", integ.opts.reltol))
adaptive: $(integ.opts.adaptive)
dense: $(integ.opts.dense)
"""
else
sv_str = isempty(saveat_s) ? "[]" : "$(length(saveat_s)) points in [$(first(saveat_s)), $(last(saveat_s))]"
"Solver (NullODEIntegrator — no unknowns)
saveat: $sv_str
dense: true"
end

# Solve
DifferentialEquations.solve(ode_prob, solver; kwargs...)
end
sim_time = time() - t0
if sol.retcode == ReturnCode.Success
if sol.retcode == DifferentialEquations.ReturnCode.Success
sys = sol.prob.f.sys
n_vars = length(ModelingToolkit.unknowns(sys))
n_obs = length(ModelingToolkit.observed(sys))
Expand All @@ -67,7 +140,8 @@ function run_simulate(ode_prob, model_dir::String,
sim_time = time() - t0
sim_error = sprint(showerror, e, catch_backtrace())
end
println(log_file, "Time: $(round(sim_time; digits=3)) s")
println(log_file, solver_settings_string)
println(log_file, "Time: $(round(sim_time; digits=3)) s")
println(log_file, "Success: $sim_success")
isempty(sim_error) || println(log_file, "\n--- Error ---\n$sim_error")
close(log_file)
Expand Down
Loading
Loading