Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
4 changes: 4 additions & 0 deletions src/summary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function write_summary(
print(io, " \"cpu_threads\": $(info.cpu_threads),\n")
print(io, " \"ram_gb\": $(@sprintf "%.2f" info.ram_gb),\n")
print(io, " \"total_time_s\": $(@sprintf "%.2f" info.total_time_s),\n")
print(io, " \"solver\": \"$(_esc_json(info.solver))\",\n")
print(io, " \"models\": [\n")
for (i, r) in enumerate(results)
sep = i < length(results) ? "," : ""
Expand Down Expand Up @@ -74,6 +75,7 @@ Parsed contents of a single `summary.json` file.
- `cpu_threads` — number of logical CPU threads
- `ram_gb` — total system RAM in GiB
- `total_time_s` — wall-clock duration of the full test run in seconds
- `solver` — fully-qualified solver name, e.g. `"DifferentialEquations.Rodas5P"`
- `models` — vector of per-model dicts; each has keys
`"name"`, `"export"`, `"parse"`, `"sim"`, `"cmp_total"`, `"cmp_pass"`
"""
Expand All @@ -92,6 +94,7 @@ struct RunSummary
cpu_threads :: Int
ram_gb :: Float64
total_time_s :: Float64
solver :: String
models :: Vector{Dict{String,Any}}
end

Expand Down Expand Up @@ -150,6 +153,7 @@ function load_summary(results_root::String)::Union{RunSummary,Nothing}
_int("cpu_threads"),
_float("ram_gb"),
_float("total_time_s"),
_str("solver"),
models,
)
end
Loading
Loading