Skip to content

Commit 694e03e

Browse files
AnHeuermannclaude
andauthored
More error columns (#35)
* Finer error reports for parsing Base Modelica * Added separate log files and timings for steps of parsing: * Phase 2a: ANTLR parser * Phase 2b: Base Modelica → ModelingToolkit conversion * Phase 2c: ODEProblem generation * Update HTML report and overview. * Updated summary.json Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 83437b3 commit 694e03e

File tree

8 files changed

+274
-84
lines changed

8 files changed

+274
-84
lines changed

.github/scripts/gen_landing_page.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,15 @@ def load_runs(site_root: Path) -> list[dict]:
7979
except Exception:
8080
continue
8181

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

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

135+
def _parse_sub_cell(n_sub, n_prev):
136+
if n_sub is None:
137+
return '<td class="na">—</td>'
138+
return _pct_cell(n_sub, n_prev)
139+
126140
if runs:
127141
rows = []
128142
for r in runs:
@@ -140,13 +154,15 @@ def render(runs: list[dict]) -> str:
140154
<td>{r['date']}</td>
141155
<td>{r['duration']}</td>
142156
{_pct_cell(r['n_exp'], r['total'])}
143-
{_pct_cell(r['n_par'], r['n_exp'])}
157+
{_parse_sub_cell(r['n_antlr'], r['n_exp'])}
158+
{_parse_sub_cell(r['n_mtk'], r['n_antlr'] if r['n_antlr'] is not None else r['n_exp'])}
159+
{_parse_sub_cell(r['n_ode'], r['n_mtk'] if r['n_mtk'] is not None else r['n_exp'])}
144160
{_pct_cell(r['n_sim'], r['n_par'])}
145161
{cmp_cell}
146162
</tr>""")
147163
rows_html = "\n".join(rows)
148164
else:
149-
rows_html = ' <tr><td colspan="10" class="na" style="text-align:center">No results yet.</td></tr>'
165+
rows_html = ' <tr><td colspan="12" class="na" style="text-align:center">No results yet.</td></tr>'
150166

151167
return f"""\
152168
<!DOCTYPE html>
@@ -180,7 +196,9 @@ def render(runs: list[dict]) -> str:
180196
<th>Date</th>
181197
<th>Duration</th>
182198
<th>BM Export</th>
183-
<th>BM Parse</th>
199+
<th>ANTLR</th>
200+
<th>BM→MTK</th>
201+
<th>ODEProblem</th>
184202
<th>MTK Sim</th>
185203
<th>Ref Cmp</th>
186204
</tr>

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
1010
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1111
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1212
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
13+
ModelingToolkitBase = "7771a370-6774-4173-bd38-47e70ca0b839"
1314
OMJulia = "0f4fe800-344e-11e9-2949-fb537ad918e1"
1415
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
1516
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"

src/parse_bm.jl

Lines changed: 139 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,159 @@
22

33
import BaseModelica
44
import Logging
5+
import ModelingToolkit
6+
import ModelingToolkitBase
57

68
"""
7-
run_parse(bm_path, model_dir, model) → (success, time, error, ode_prob)
9+
run_parse(bm_path, model_dir, model) → NamedTuple
810
9-
Parse a Base Modelica `.bmo` file with BaseModelica.jl and create an
10-
`ODEProblem` from the `Experiment` annotation.
11-
Writes a `<model>_parsing.log` file in `model_dir`.
12-
Returns `nothing` as the fourth element on failure.
11+
Parse a Base Modelica `.bmo` file with BaseModelica.jl in three sub-steps and
12+
create an `ODEProblem` from the `Experiment` annotation.
13+
14+
Sub-steps timed and reported separately, each with its own log file:
15+
1. **ANTLR parser** — `<model>_antlr.log` — parses the `.bmo` file into an AST.
16+
2. **BM → MTK** — `<model>_mtk.log` — converts the AST into a
17+
`ModelingToolkit.System`.
18+
3. **ODEProblem** — `<model>_ode.log` — builds the `ODEProblem` using the
19+
`Experiment` annotation.
20+
21+
Returns a NamedTuple with fields:
22+
- `success`, `time`, `error` — overall result
23+
- `ode_prob` — the `ODEProblem` (or `nothing` on failure)
24+
- `antlr_success`, `antlr_time`
25+
- `mtk_success`, `mtk_time`
26+
- `ode_success`, `ode_time`
1327
"""
1428
function run_parse(bm_path::String, model_dir::String,
15-
model::String)::Tuple{Bool,Float64,String,Any}
16-
parse_success = false
17-
parse_time = 0.0
18-
parse_error = ""
29+
model::String)
30+
antlr_success = false; antlr_time = 0.0; antlr_error = ""
31+
mtk_success = false; mtk_time = 0.0; mtk_error = ""
32+
ode_success = false; ode_time = 0.0; ode_error = ""
1933
ode_prob = nothing
34+
package = nothing
35+
sys = nothing
2036

2137
isdir(model_dir) || mkpath(model_dir)
22-
log_file = open(joinpath(model_dir, "$(model)_parsing.log"), "w")
23-
stdout_pipe = Pipe()
24-
println(log_file, "Model: $model")
25-
logger = Logging.SimpleLogger(log_file, Logging.Debug)
38+
39+
# ── Step 1: ANTLR parser ──────────────────────────────────────────────────
40+
log1 = open(joinpath(model_dir, "$(model)_antlr.log"), "w")
41+
pipe1 = Pipe()
42+
logger = Logging.SimpleLogger(log1, Logging.Debug)
43+
println(log1, "Model: $model")
2644
t0 = time()
2745
try
28-
# create_odeproblem returns an ODEProblem using the Experiment
29-
# annotation for StartTime/StopTime/Tolerance/Interval.
30-
# Redirect Julia log output to the log file and stdout/stderr to a
31-
# buffer so they can be appended after the summary lines.
32-
ode_prob = redirect_stdout(stdout_pipe) do
33-
redirect_stderr(stdout_pipe) do
46+
package = redirect_stdout(pipe1) do
47+
redirect_stderr(pipe1) do
3448
Logging.with_logger(logger) do
35-
BaseModelica.create_odeproblem(bm_path)
49+
BaseModelica.parse_file_antlr(bm_path)
3650
end
3751
end
3852
end
39-
parse_time = time() - t0
40-
parse_success = true
53+
antlr_time = time() - t0
54+
antlr_success = true
4155
catch e
42-
parse_time = time() - t0
43-
parse_error = sprint(showerror, e, catch_backtrace())
56+
antlr_time = time() - t0
57+
antlr_error = sprint(showerror, e, catch_backtrace())
58+
end
59+
close(pipe1.in)
60+
captured = read(pipe1.out, String)
61+
println(log1, "Time: $(round(antlr_time; digits=3)) s")
62+
println(log1, "Success: $antlr_success")
63+
isempty(captured) || print(log1, "\n--- Parser output ---\n", captured)
64+
isempty(antlr_error) || println(log1, "\n--- Error ---\n$antlr_error")
65+
close(log1)
66+
67+
# ── Step 2: Base Modelica → ModelingToolkit ───────────────────────────────
68+
if antlr_success
69+
log2 = open(joinpath(model_dir, "$(model)_mtk.log"), "w")
70+
pipe2 = Pipe()
71+
logger = Logging.SimpleLogger(log2, Logging.Debug)
72+
println(log2, "Model: $model")
73+
t0 = time()
74+
try
75+
sys = redirect_stdout(pipe2) do
76+
redirect_stderr(pipe2) do
77+
Logging.with_logger(logger) do
78+
BaseModelica.baseModelica_to_ModelingToolkit(package)
79+
end
80+
end
81+
end
82+
mtk_time = time() - t0
83+
mtk_success = true
84+
catch e
85+
mtk_time = time() - t0
86+
mtk_error = sprint(showerror, e, catch_backtrace())
87+
end
88+
close(pipe2.in)
89+
captured = read(pipe2.out, String)
90+
println(log2, "Time: $(round(mtk_time; digits=3)) s")
91+
println(log2, "Success: $mtk_success")
92+
isempty(captured) || print(log2, "\n--- Parser output ---\n", captured)
93+
isempty(mtk_error) || println(log2, "\n--- Error ---\n$mtk_error")
94+
close(log2)
4495
end
45-
close(stdout_pipe.in)
46-
captured = read(stdout_pipe.out, String)
47-
println(log_file, "Time: $(round(parse_time; digits=3)) s")
48-
println(log_file, "Success: $parse_success")
49-
isempty(captured) || print(log_file, "\n--- Parser output ---\n", captured)
50-
isempty(parse_error) || println(log_file, "\n--- Error ---\n$parse_error")
51-
close(log_file)
52-
53-
return parse_success, parse_time, parse_error, ode_prob
96+
97+
# ── Step 3: ODEProblem generation ─────────────────────────────────────────
98+
if mtk_success
99+
log3 = open(joinpath(model_dir, "$(model)_ode.log"), "w")
100+
pipe3 = Pipe()
101+
logger = Logging.SimpleLogger(log3, Logging.Debug)
102+
println(log3, "Model: $model")
103+
t0 = time()
104+
try
105+
# Extract experiment annotation from the parsed package
106+
annotation = nothing
107+
try
108+
annotation = package.model.long_class_specifier.composition.annotation
109+
catch; end
110+
exp_params = BaseModelica.parse_experiment_annotation(annotation)
111+
112+
ode_prob = redirect_stdout(pipe3) do
113+
redirect_stderr(pipe3) do
114+
Logging.with_logger(logger) do
115+
_mv = ModelingToolkitBase.MissingGuessValue.Constant(0.0)
116+
if !isnothing(exp_params)
117+
tspan = (exp_params.StartTime, exp_params.StopTime)
118+
extra_kw = isnothing(exp_params.Interval) ?
119+
(reltol = exp_params.Tolerance,) :
120+
(reltol = exp_params.Tolerance,
121+
saveat = exp_params.Interval)
122+
ModelingToolkit.ODEProblem(sys, [], tspan;
123+
missing_guess_value = _mv, extra_kw...)
124+
else
125+
ModelingToolkit.ODEProblem(sys, [], (0.0, 1.0);
126+
missing_guess_value = _mv)
127+
end
128+
end
129+
end
130+
end
131+
ode_time = time() - t0
132+
ode_success = true
133+
catch e
134+
ode_time = time() - t0
135+
ode_error = sprint(showerror, e, catch_backtrace())
136+
end
137+
close(pipe3.in)
138+
captured = read(pipe3.out, String)
139+
println(log3, "Time: $(round(ode_time; digits=3)) s")
140+
println(log3, "Success: $ode_success")
141+
isempty(captured) || print(log3, "\n--- Parser output ---\n", captured)
142+
isempty(ode_error) || println(log3, "\n--- Error ---\n$ode_error")
143+
close(log3)
144+
end
145+
146+
first_error = !isempty(antlr_error) ? antlr_error :
147+
!isempty(mtk_error) ? mtk_error : ode_error
148+
return (
149+
success = ode_success,
150+
time = antlr_time + mtk_time + ode_time,
151+
error = first_error,
152+
ode_prob = ode_prob,
153+
antlr_success = antlr_success,
154+
antlr_time = antlr_time,
155+
mtk_success = mtk_success,
156+
mtk_time = mtk_time,
157+
ode_success = ode_success,
158+
ode_time = ode_time,
159+
)
54160
end

src/pipeline.jl

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,20 @@ function test_model(omc::OMJulia.OMCSession,
7373
# Phase 1 ──────────────────────────────────────────────────────────────────
7474
exp_ok, exp_t, exp_err = run_export(omc, model, model_dir, bm_path)
7575
exp_ok || return ModelResult(
76-
model, false, exp_t, exp_err, false, 0.0, "", false, 0.0, "", 0, 0, 0, "")
76+
model, false, exp_t, exp_err,
77+
false, 0.0, "",
78+
false, 0.0, false, 0.0, false, 0.0,
79+
false, 0.0, "", 0, 0, 0, "")
7780

7881
# Phase 2 ──────────────────────────────────────────────────────────────────
79-
par_ok, par_t, par_err, ode_prob = run_parse(bm_path, model_dir, model)
80-
par_ok || return ModelResult(
81-
model, true, exp_t, exp_err, false, par_t, par_err, false, 0.0, "", 0, 0, 0, "")
82+
par = run_parse(bm_path, model_dir, model)
83+
par.success || return ModelResult(
84+
model, true, exp_t, exp_err,
85+
false, par.time, par.error,
86+
par.antlr_success, par.antlr_time,
87+
par.mtk_success, par.mtk_time,
88+
par.ode_success, par.ode_time,
89+
false, 0.0, "", 0, 0, 0, "")
8290

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

98106
# Phase 3 ──────────────────────────────────────────────────────────────────
99-
sim_ok, sim_t, sim_err, sol = run_simulate(ode_prob, model_dir, model;
107+
sim_ok, sim_t, sim_err, sol = run_simulate(par.ode_prob, model_dir, model;
100108
settings = sim_settings,
101109
csv_max_size_mb, cmp_signals)
102110

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

115123
return ModelResult(
116124
model,
117-
true, exp_t, exp_err,
118-
true, par_t, par_err,
125+
true, exp_t, exp_err,
126+
true, par.time, par.error,
127+
par.antlr_success, par.antlr_time,
128+
par.mtk_success, par.mtk_time,
129+
par.ode_success, par.ode_time,
119130
sim_ok, sim_t, sim_err,
120131
cmp_total, cmp_pass, cmp_skip, cmp_csv)
121132
end

0 commit comments

Comments
 (0)