Skip to content
Merged
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
26 changes: 22 additions & 4 deletions .github/scripts/gen_landing_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,15 @@ def load_runs(site_root: Path) -> list[dict]:
except Exception:
continue

models = data.get("models", [])
models = data.get("models", [])
n = len(models)
n_exp = sum(1 for m in models if m.get("export", False))
# Sub-steps — fall back to overall "parse" flag for older summary.json files
# that predate the three-step split.
has_substeps = any("antlr" in m for m in models)
n_antlr = sum(1 for m in models if m.get("antlr", m.get("parse", False))) if has_substeps else None
n_mtk = sum(1 for m in models if m.get("mtk", m.get("parse", False))) if has_substeps else None
n_ode = sum(1 for m in models if m.get("ode", m.get("parse", False))) if has_substeps else None
n_par = sum(1 for m in models if m.get("parse", False))
n_sim = sum(1 for m in models if m.get("sim", False))

Expand All @@ -100,6 +106,9 @@ def load_runs(site_root: Path) -> list[dict]:
"omc_version": data.get("omc_version", "?"),
"total": n,
"n_exp": n_exp,
"n_antlr": n_antlr,
"n_mtk": n_mtk,
"n_ode": n_ode,
"n_par": n_par,
"n_sim": n_sim,
"n_cmp": n_cmp,
Expand All @@ -123,6 +132,11 @@ def _pct_cell(num: int, den: int) -> str:
def render(runs: list[dict]) -> str:
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")

def _parse_sub_cell(n_sub, n_prev):
if n_sub is None:
return '<td class="na">—</td>'
return _pct_cell(n_sub, n_prev)

if runs:
rows = []
for r in runs:
Expand All @@ -140,13 +154,15 @@ def render(runs: list[dict]) -> str:
<td>{r['date']}</td>
<td>{r['duration']}</td>
{_pct_cell(r['n_exp'], r['total'])}
{_pct_cell(r['n_par'], r['n_exp'])}
{_parse_sub_cell(r['n_antlr'], r['n_exp'])}
{_parse_sub_cell(r['n_mtk'], r['n_antlr'] if r['n_antlr'] is not None else r['n_exp'])}
{_parse_sub_cell(r['n_ode'], r['n_mtk'] if r['n_mtk'] is not None else r['n_exp'])}
{_pct_cell(r['n_sim'], r['n_par'])}
{cmp_cell}
</tr>""")
rows_html = "\n".join(rows)
else:
rows_html = ' <tr><td colspan="10" class="na" style="text-align:center">No results yet.</td></tr>'
rows_html = ' <tr><td colspan="12" class="na" style="text-align:center">No results yet.</td></tr>'

return f"""\
<!DOCTYPE html>
Expand Down Expand Up @@ -180,7 +196,9 @@ def render(runs: list[dict]) -> str:
<th>Date</th>
<th>Duration</th>
<th>BM Export</th>
<th>BM Parse</th>
<th>ANTLR</th>
<th>BM→MTK</th>
<th>ODEProblem</th>
<th>MTK Sim</th>
<th>Ref Cmp</th>
</tr>
Expand Down
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
ModelingToolkitBase = "7771a370-6774-4173-bd38-47e70ca0b839"
OMJulia = "0f4fe800-344e-11e9-2949-fb537ad918e1"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Expand Down
172 changes: 139 additions & 33 deletions src/parse_bm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,159 @@

import BaseModelica
import Logging
import ModelingToolkit
import ModelingToolkitBase

"""
run_parse(bm_path, model_dir, model) → (success, time, error, ode_prob)
run_parse(bm_path, model_dir, model) → NamedTuple

Parse a Base Modelica `.bmo` file with BaseModelica.jl and create an
`ODEProblem` from the `Experiment` annotation.
Writes a `<model>_parsing.log` file in `model_dir`.
Returns `nothing` as the fourth element on failure.
Parse a Base Modelica `.bmo` file with BaseModelica.jl in three sub-steps and
create an `ODEProblem` from the `Experiment` annotation.

Sub-steps timed and reported separately, each with its own log file:
1. **ANTLR parser** — `<model>_antlr.log` — parses the `.bmo` file into an AST.
2. **BM → MTK** — `<model>_mtk.log` — converts the AST into a
`ModelingToolkit.System`.
3. **ODEProblem** — `<model>_ode.log` — builds the `ODEProblem` using the
`Experiment` annotation.

Returns a NamedTuple with fields:
- `success`, `time`, `error` — overall result
- `ode_prob` — the `ODEProblem` (or `nothing` on failure)
- `antlr_success`, `antlr_time`
- `mtk_success`, `mtk_time`
- `ode_success`, `ode_time`
"""
function run_parse(bm_path::String, model_dir::String,
model::String)::Tuple{Bool,Float64,String,Any}
parse_success = false
parse_time = 0.0
parse_error = ""
model::String)
antlr_success = false; antlr_time = 0.0; antlr_error = ""
mtk_success = false; mtk_time = 0.0; mtk_error = ""
ode_success = false; ode_time = 0.0; ode_error = ""
ode_prob = nothing
package = nothing
sys = 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")
logger = Logging.SimpleLogger(log_file, Logging.Debug)

# ── Step 1: ANTLR parser ──────────────────────────────────────────────────
log1 = open(joinpath(model_dir, "$(model)_antlr.log"), "w")
pipe1 = Pipe()
logger = Logging.SimpleLogger(log1, Logging.Debug)
println(log1, "Model: $model")
t0 = time()
try
# create_odeproblem returns an ODEProblem using the Experiment
# annotation for StartTime/StopTime/Tolerance/Interval.
# Redirect Julia log output to the log file and stdout/stderr to a
# buffer so they can be appended after the summary lines.
ode_prob = redirect_stdout(stdout_pipe) do
redirect_stderr(stdout_pipe) do
package = redirect_stdout(pipe1) do
redirect_stderr(pipe1) do
Logging.with_logger(logger) do
BaseModelica.create_odeproblem(bm_path)
BaseModelica.parse_file_antlr(bm_path)
end
end
end
parse_time = time() - t0
parse_success = true
antlr_time = time() - t0
antlr_success = true
catch e
parse_time = time() - t0
parse_error = sprint(showerror, e, catch_backtrace())
antlr_time = time() - t0
antlr_error = sprint(showerror, e, catch_backtrace())
end
close(pipe1.in)
captured = read(pipe1.out, String)
println(log1, "Time: $(round(antlr_time; digits=3)) s")
println(log1, "Success: $antlr_success")
isempty(captured) || print(log1, "\n--- Parser output ---\n", captured)
isempty(antlr_error) || println(log1, "\n--- Error ---\n$antlr_error")
close(log1)

# ── Step 2: Base Modelica → ModelingToolkit ───────────────────────────────
if antlr_success
log2 = open(joinpath(model_dir, "$(model)_mtk.log"), "w")
pipe2 = Pipe()
logger = Logging.SimpleLogger(log2, Logging.Debug)
println(log2, "Model: $model")
t0 = time()
try
sys = redirect_stdout(pipe2) do
redirect_stderr(pipe2) do
Logging.with_logger(logger) do
BaseModelica.baseModelica_to_ModelingToolkit(package)
end
end
end
mtk_time = time() - t0
mtk_success = true
catch e
mtk_time = time() - t0
mtk_error = sprint(showerror, e, catch_backtrace())
end
close(pipe2.in)
captured = read(pipe2.out, String)
println(log2, "Time: $(round(mtk_time; digits=3)) s")
println(log2, "Success: $mtk_success")
isempty(captured) || print(log2, "\n--- Parser output ---\n", captured)
isempty(mtk_error) || println(log2, "\n--- Error ---\n$mtk_error")
close(log2)
end
close(stdout_pipe.in)
captured = read(stdout_pipe.out, String)
println(log_file, "Time: $(round(parse_time; digits=3)) s")
println(log_file, "Success: $parse_success")
isempty(captured) || print(log_file, "\n--- Parser output ---\n", captured)
isempty(parse_error) || println(log_file, "\n--- Error ---\n$parse_error")
close(log_file)

return parse_success, parse_time, parse_error, ode_prob

# ── Step 3: ODEProblem generation ─────────────────────────────────────────
if mtk_success
log3 = open(joinpath(model_dir, "$(model)_ode.log"), "w")
pipe3 = Pipe()
logger = Logging.SimpleLogger(log3, Logging.Debug)
println(log3, "Model: $model")
t0 = time()
try
# Extract experiment annotation from the parsed package
annotation = nothing
try
annotation = package.model.long_class_specifier.composition.annotation
catch; end
exp_params = BaseModelica.parse_experiment_annotation(annotation)

ode_prob = redirect_stdout(pipe3) do
redirect_stderr(pipe3) do
Logging.with_logger(logger) do
_mv = ModelingToolkitBase.MissingGuessValue.Constant(0.0)
if !isnothing(exp_params)
tspan = (exp_params.StartTime, exp_params.StopTime)
extra_kw = isnothing(exp_params.Interval) ?
(reltol = exp_params.Tolerance,) :
(reltol = exp_params.Tolerance,
saveat = exp_params.Interval)
ModelingToolkit.ODEProblem(sys, [], tspan;
missing_guess_value = _mv, extra_kw...)
else
ModelingToolkit.ODEProblem(sys, [], (0.0, 1.0);
missing_guess_value = _mv)
end
end
end
end
ode_time = time() - t0
ode_success = true
catch e
ode_time = time() - t0
ode_error = sprint(showerror, e, catch_backtrace())
end
close(pipe3.in)
captured = read(pipe3.out, String)
println(log3, "Time: $(round(ode_time; digits=3)) s")
println(log3, "Success: $ode_success")
isempty(captured) || print(log3, "\n--- Parser output ---\n", captured)
isempty(ode_error) || println(log3, "\n--- Error ---\n$ode_error")
close(log3)
end

first_error = !isempty(antlr_error) ? antlr_error :
!isempty(mtk_error) ? mtk_error : ode_error
return (
success = ode_success,
time = antlr_time + mtk_time + ode_time,
error = first_error,
ode_prob = ode_prob,
antlr_success = antlr_success,
antlr_time = antlr_time,
mtk_success = mtk_success,
mtk_time = mtk_time,
ode_success = ode_success,
ode_time = ode_time,
)
end
25 changes: 18 additions & 7 deletions src/pipeline.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,20 @@ function test_model(omc::OMJulia.OMCSession,
# Phase 1 ──────────────────────────────────────────────────────────────────
exp_ok, exp_t, exp_err = run_export(omc, model, model_dir, bm_path)
exp_ok || return ModelResult(
model, false, exp_t, exp_err, false, 0.0, "", false, 0.0, "", 0, 0, 0, "")
model, false, exp_t, exp_err,
false, 0.0, "",
false, 0.0, false, 0.0, false, 0.0,
false, 0.0, "", 0, 0, 0, "")

# Phase 2 ──────────────────────────────────────────────────────────────────
par_ok, par_t, par_err, ode_prob = run_parse(bm_path, model_dir, model)
par_ok || return ModelResult(
model, true, exp_t, exp_err, false, par_t, par_err, false, 0.0, "", 0, 0, 0, "")
par = run_parse(bm_path, model_dir, model)
par.success || return ModelResult(
model, true, exp_t, exp_err,
false, par.time, par.error,
par.antlr_success, par.antlr_time,
par.mtk_success, par.mtk_time,
par.ode_success, par.ode_time,
false, 0.0, "", 0, 0, 0, "")

# Resolve reference CSV and comparison signals early so phase 3 can filter
# the CSV output to only the signals that will actually be verified.
Expand All @@ -96,7 +104,7 @@ function test_model(omc::OMJulia.OMCSession,
end

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

Expand All @@ -114,8 +122,11 @@ function test_model(omc::OMJulia.OMCSession,

return ModelResult(
model,
true, exp_t, exp_err,
true, par_t, par_err,
true, exp_t, exp_err,
true, par.time, par.error,
par.antlr_success, par.antlr_time,
par.mtk_success, par.mtk_time,
par.ode_success, par.ode_time,
sim_ok, sim_t, sim_err,
cmp_total, cmp_pass, cmp_skip, cmp_csv)
end
Expand Down
Loading
Loading