dynamic-systems-testing-framework 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""What omc's ``<prefix>_init.xml`` says of a model's SIZE.
|
|
2
|
+
|
|
3
|
+
OpenModelica prints no translation statistics, so its statistics were
|
|
4
|
+
timing only: a model that gained a state said ``Continuous states: 2 -> 3``
|
|
5
|
+
on Dymola (its translation log) and nothing here. omc does write the
|
|
6
|
+
system's size, as attributes of the root element of the init file it
|
|
7
|
+
generates beside the executable (``fmiModelDescription
|
|
8
|
+
numberOfContinuousStates="3" …``). What that file holds of a quantity
|
|
9
|
+
Dymola's backend records is read into the SAME statistics key — one
|
|
10
|
+
structural check, one Statistics row, on both simulators.
|
|
11
|
+
|
|
12
|
+
Read from the file, at every read of a result: nothing is kept beside the
|
|
13
|
+
result for it, so a ``compare`` of an earlier run has it too. A baseline
|
|
14
|
+
accepted before this has no such statistic, and a statistic one side lacks
|
|
15
|
+
is no change (``comparator._check_structural_changes``, ``stats_rows.
|
|
16
|
+
changed``): the first accept after it records the number.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import xml.etree.ElementTree as ET
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
#: omc's attribute → the ``statistics.translation`` key Dymola's translation
|
|
29
|
+
#: log fills with the same quantity. Only what both simulators count the
|
|
30
|
+
#: same way: omc's other counts (event indicators, algebraic variables by
|
|
31
|
+
#: type, parameters by type) have no twin there — Dymola's ``free_parameters``
|
|
32
|
+
#: and ``scalar_unknowns`` are other quantities — and are not read.
|
|
33
|
+
SYSTEM_SIZE = {"numberOfContinuousStates": "continuous_time_states"}
|
|
34
|
+
|
|
35
|
+
#: A count as omc writes one: ASCII digits and nothing else.
|
|
36
|
+
_COUNT = re.compile(r"[0-9]+")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def system_size(init_xml: Path) -> dict[str, int]:
|
|
40
|
+
"""The ``statistics.translation`` entries *init_xml* gives; none for a
|
|
41
|
+
file that is not there, does not parse, or holds no count under an
|
|
42
|
+
attribute — an old omc, a build that failed before it wrote the file.
|
|
43
|
+
``0`` is a count: a model with no state has one. Only the root element's start tag is read: the file lists every
|
|
44
|
+
variable of the model, megabytes for a large one."""
|
|
45
|
+
try:
|
|
46
|
+
for _event, root in ET.iterparse(init_xml, events=("start",)):
|
|
47
|
+
attributes = dict(root.attrib)
|
|
48
|
+
break
|
|
49
|
+
else:
|
|
50
|
+
return {}
|
|
51
|
+
except (OSError, ET.ParseError):
|
|
52
|
+
logger.debug("no system size from %s", init_xml, exc_info=True)
|
|
53
|
+
return {}
|
|
54
|
+
found: dict[str, int] = {}
|
|
55
|
+
for attribute, key in SYSTEM_SIZE.items():
|
|
56
|
+
text = attributes.get(attribute)
|
|
57
|
+
# A count is a non-negative decimal integer, all of it — or it is
|
|
58
|
+
# not recorded. `int()` also reads `-1`, `+3`, `1_000`, ` 3 ` and
|
|
59
|
+
# the digits of other scripts, none of which omc writes for one.
|
|
60
|
+
if text is not None and _COUNT.fullmatch(text):
|
|
61
|
+
found[key] = int(text)
|
|
62
|
+
return found
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def with_system_size(stats: dict | None, init_xml: Path) -> dict | None:
|
|
66
|
+
"""*stats* with what *init_xml* says under ``translation``: a new dict,
|
|
67
|
+
or *stats* itself — None included — when the file says nothing."""
|
|
68
|
+
found = system_size(init_xml)
|
|
69
|
+
if not found:
|
|
70
|
+
return stats
|
|
71
|
+
merged = dict(stats or {})
|
|
72
|
+
merged["translation"] = {**(merged.get("translation") or {}), **found}
|
|
73
|
+
return merged
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = ["SYSTEM_SIZE", "system_size", "with_system_size"]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Parse OpenModelica's ``omc`` stdout for per-test results.
|
|
2
|
+
|
|
3
|
+
When ``omc`` runs a ``.mos`` that ends with a top-level ``simulate(...)`` call,
|
|
4
|
+
it REPL-echoes the returned record to stdout:
|
|
5
|
+
|
|
6
|
+
record SimulationResult
|
|
7
|
+
resultFile = "/path/to/result_res.mat",
|
|
8
|
+
simulationOptions = "...",
|
|
9
|
+
messages = "...",
|
|
10
|
+
timeFrontend = 0.19,
|
|
11
|
+
timeBackend = 0.04,
|
|
12
|
+
...
|
|
13
|
+
timeTotal = 1.25
|
|
14
|
+
end SimulationResult;
|
|
15
|
+
|
|
16
|
+
This module extracts that block, classifies success/failure, and surfaces any
|
|
17
|
+
Error/Warning/Notification lines that appeared BEFORE it. The return shape is
|
|
18
|
+
a small ``ParsedOmcOutput`` dataclass the backend consumes to populate
|
|
19
|
+
``TestRunResult``.
|
|
20
|
+
|
|
21
|
+
The parser is total — any shape of input produces a ``ParsedOmcOutput``, with
|
|
22
|
+
``success=False`` and ``timings=None`` on truncation / missing record.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ParsedOmcOutput:
|
|
33
|
+
"""Structured view of what the backend needs from omc stdout."""
|
|
34
|
+
|
|
35
|
+
success: bool
|
|
36
|
+
result_file: str = ""
|
|
37
|
+
messages: str = ""
|
|
38
|
+
timings: dict[str, float] | None = None
|
|
39
|
+
error_notices: list[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Match 'record SimulationResult … end SimulationResult;' lazily across
|
|
43
|
+
# however many lines the record spans.
|
|
44
|
+
_RECORD_RE = re.compile(
|
|
45
|
+
r"record SimulationResult(?P<body>.*?)end SimulationResult;",
|
|
46
|
+
re.DOTALL,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Match a quoted string-valued record field. The value may span multiple
|
|
50
|
+
# lines (OM's 'messages' often does). Handles embedded \" and \\ escapes.
|
|
51
|
+
_STR_FIELD_RE = re.compile(
|
|
52
|
+
r'\b(\w+)\s*=\s*"((?:\\.|[^"\\])*)"',
|
|
53
|
+
re.DOTALL,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Match a numeric-valued record field. Run AFTER stripping string fields from
|
|
57
|
+
# the body (digits in messages would otherwise get misread).
|
|
58
|
+
_NUM_FIELD_RE = re.compile(
|
|
59
|
+
r"\b(\w+)\s*=\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# OM field name -> our internal key
|
|
63
|
+
_TIMING_KEYS = {
|
|
64
|
+
"timeFrontend": "frontend",
|
|
65
|
+
"timeBackend": "backend",
|
|
66
|
+
"timeSimCode": "simcode",
|
|
67
|
+
"timeTemplates": "templates",
|
|
68
|
+
"timeCompile": "compile",
|
|
69
|
+
"timeSimulation": "simulation",
|
|
70
|
+
"timeTotal": "total",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Lines like 'Error: ...' / 'Warning: ...' / 'Notification: ...'
|
|
74
|
+
_NOTICE_RE = re.compile(
|
|
75
|
+
r"^\s*(?:Error|Warning|Notification)\b[: ].*$",
|
|
76
|
+
re.MULTILINE,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_omc_stdout(text: str) -> ParsedOmcOutput:
|
|
81
|
+
"""Parse captured omc stdout into a ``ParsedOmcOutput``."""
|
|
82
|
+
m = _RECORD_RE.search(text)
|
|
83
|
+
preamble = text if not m else text[: m.start()]
|
|
84
|
+
error_notices = [mo.group(0).strip() for mo in _NOTICE_RE.finditer(preamble)]
|
|
85
|
+
|
|
86
|
+
if not m:
|
|
87
|
+
return ParsedOmcOutput(success=False, error_notices=error_notices)
|
|
88
|
+
|
|
89
|
+
body = m.group("body")
|
|
90
|
+
|
|
91
|
+
# Extract string-valued fields first (they may contain digits we don't
|
|
92
|
+
# want to misinterpret as timings).
|
|
93
|
+
str_fields: dict[str, str] = {}
|
|
94
|
+
for fm in _STR_FIELD_RE.finditer(body):
|
|
95
|
+
str_fields[fm.group(1)] = fm.group(2)
|
|
96
|
+
|
|
97
|
+
# Strip strings from the body before pulling numbers.
|
|
98
|
+
body_no_strings = _STR_FIELD_RE.sub("", body)
|
|
99
|
+
timings: dict[str, float] = {}
|
|
100
|
+
for fm in _NUM_FIELD_RE.finditer(body_no_strings):
|
|
101
|
+
name = fm.group(1)
|
|
102
|
+
if name in _TIMING_KEYS:
|
|
103
|
+
try:
|
|
104
|
+
timings[_TIMING_KEYS[name]] = float(fm.group(2))
|
|
105
|
+
except ValueError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
result_file = str_fields.get("resultFile", "")
|
|
109
|
+
messages = str_fields.get("messages", "")
|
|
110
|
+
|
|
111
|
+
success = bool(result_file) and "Failed" not in messages
|
|
112
|
+
|
|
113
|
+
return ParsedOmcOutput(
|
|
114
|
+
success=success,
|
|
115
|
+
result_file=result_file,
|
|
116
|
+
messages=messages,
|
|
117
|
+
timings=timings or None,
|
|
118
|
+
error_notices=error_notices,
|
|
119
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Pure-text builders for OpenModelica .mos scripts.
|
|
2
|
+
|
|
3
|
+
No I/O beyond string assembly — trivially unit-testable. The backend calls
|
|
4
|
+
:func:`build_simulate_mos` per test, writes the result to
|
|
5
|
+
``<test_dir>/simulate.mos``, then invokes ``omc`` on it.
|
|
6
|
+
|
|
7
|
+
The .mos does NOT emit explicit timing output. When ``omc`` runs a script
|
|
8
|
+
non-interactively it REPL-echoes the top-level ``simulate(...)`` call as a
|
|
9
|
+
``record SimulationResult ... end SimulationResult;`` block that already
|
|
10
|
+
contains ``resultFile``, ``messages``, and the ``timeFrontend / Backend /
|
|
11
|
+
SimCode / Templates / Compile / Simulation / Total`` fields — everything we
|
|
12
|
+
need. ``log_parser.parse_omc_stdout`` pulls them from that block, so the
|
|
13
|
+
.mos stays small and the print/sentinel dance proved to be silently
|
|
14
|
+
unreliable (``res`` isn't in scope for the follow-up prints).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ...discovery.library_state import dependency_file
|
|
24
|
+
from ...discovery.test_registry import TestModel
|
|
25
|
+
from ...discovery.unit_tests import unit_test_variables
|
|
26
|
+
from ..common.result_variables import criterion_variables
|
|
27
|
+
from ..types import _pattern_to_regex
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def classify_dependency(entry: str) -> tuple[str, str]:
|
|
31
|
+
"""Classify a Config.dependencies entry as loadModel vs loadFile.
|
|
32
|
+
|
|
33
|
+
Returns (``"loadModel"``, name) for bare library names (no path
|
|
34
|
+
separators, doesn't look like a file), or (``"loadFile"``,
|
|
35
|
+
absolute_path) for path-like entries (resolves to a package.mo).
|
|
36
|
+
|
|
37
|
+
Bare names reach this function unresolved because Config's dependency
|
|
38
|
+
absolutization skips them — review 2026-07-06 finding 17: absolutizing
|
|
39
|
+
"Modelica" produced ``loadFile()`` on a nonexistent path and suppressed
|
|
40
|
+
the MSL auto-injection.
|
|
41
|
+
"""
|
|
42
|
+
# Path-like if it contains a separator, ends in .mo, or starts with a
|
|
43
|
+
# relative-path/home prefix (mirrors config._dependency_looks_like_path).
|
|
44
|
+
looks_like_path = (
|
|
45
|
+
"/" in entry
|
|
46
|
+
or "\\" in entry
|
|
47
|
+
or entry.endswith(".mo")
|
|
48
|
+
or entry.startswith(".")
|
|
49
|
+
or entry.startswith("~")
|
|
50
|
+
)
|
|
51
|
+
if not looks_like_path:
|
|
52
|
+
return ("loadModel", entry)
|
|
53
|
+
|
|
54
|
+
# The file it is loaded by: itself, or its directory's package.mo.
|
|
55
|
+
return ("loadFile", str(dependency_file(entry)).replace("\\", "/"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_variable_filter(
|
|
59
|
+
patterns: Iterable[str],
|
|
60
|
+
diagnostic_vars: Iterable[str],
|
|
61
|
+
extra_names: Iterable[str] = (),
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Build OM's ``variableFilter`` regex for the given tracked-variable set.
|
|
64
|
+
|
|
65
|
+
OM's ``variableFilter`` is a regex over variable names. We escape each
|
|
66
|
+
name literally and join with ``|``. ``time`` is always included; so are
|
|
67
|
+
the diagnostic variables and any ``extra_names`` the caller passes (the
|
|
68
|
+
UnitTests component's ``<instance>.x[1..N]`` for unit_tests-sourced
|
|
69
|
+
tests). Returning a regex (not ``.*``) keeps the .mat small — OM dumps
|
|
70
|
+
all parameters/aliases/derivatives by default.
|
|
71
|
+
"""
|
|
72
|
+
alternatives: list[str] = []
|
|
73
|
+
# Always include time
|
|
74
|
+
alternatives.append(re.escape("time"))
|
|
75
|
+
# Tracked variables: expand globs (*, ?) to regex via the framework's
|
|
76
|
+
# existing glob-to-regex helper, so a pattern like ``pipe.T*`` becomes
|
|
77
|
+
# ``pipe\.T.*`` and the filter actually matches the runtime variable
|
|
78
|
+
# names (post-simulation ``resolve_variable_patterns`` still narrows
|
|
79
|
+
# the set for the pass/fail comparison).
|
|
80
|
+
for pat in patterns:
|
|
81
|
+
alternatives.append(_pattern_to_regex(pat).pattern)
|
|
82
|
+
# Extra literal names (no globs). build_simulate_args uses this for the
|
|
83
|
+
# UnitTests component's variables (``<instance>.x[i]``), which the spec
|
|
84
|
+
# doesn't list, and for the variables a criterion names.
|
|
85
|
+
for n in extra_names:
|
|
86
|
+
alternatives.append(re.escape(n))
|
|
87
|
+
# Diagnostic variables (literal names — no globs)
|
|
88
|
+
for dv in diagnostic_vars:
|
|
89
|
+
alternatives.append(re.escape(dv))
|
|
90
|
+
# Dedupe while preserving order
|
|
91
|
+
seen: set[str] = set()
|
|
92
|
+
unique: list[str] = []
|
|
93
|
+
for a in alternatives:
|
|
94
|
+
if a not in seen:
|
|
95
|
+
seen.add(a)
|
|
96
|
+
unique.append(a)
|
|
97
|
+
# OM's variableFilter is a POSIX ERE applied with partial-match semantics
|
|
98
|
+
# by default — an unanchored alternation would over-match (e.g. "x" would
|
|
99
|
+
# hit "phi" or "x_derivative"). Anchor with ^(...)$ to force whole-name
|
|
100
|
+
# match.
|
|
101
|
+
return "^(" + "|".join(unique) + ")$"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_sim_kwarg(key: str, value) -> str:
|
|
105
|
+
"""Render one key=value for OM's simulate() call."""
|
|
106
|
+
if isinstance(value, str):
|
|
107
|
+
# OM escape: double-quote and escape backslashes + quotes
|
|
108
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
109
|
+
return f'{key}="{escaped}"'
|
|
110
|
+
if isinstance(value, bool):
|
|
111
|
+
return f"{key}={'true' if value else 'false'}"
|
|
112
|
+
if isinstance(value, float):
|
|
113
|
+
# Avoid scientific notation for integer-valued floats, use repr otherwise.
|
|
114
|
+
if value == int(value):
|
|
115
|
+
return f"{key}={value!r}"
|
|
116
|
+
return f"{key}={value!r}"
|
|
117
|
+
return f"{key}={value!r}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_simulate_args(
|
|
121
|
+
test: TestModel,
|
|
122
|
+
diagnostic_vars: Iterable[str],
|
|
123
|
+
*,
|
|
124
|
+
mat_sync: bool = False,
|
|
125
|
+
) -> list[str]:
|
|
126
|
+
"""Build the ``key=value`` strings for an OM ``simulate(...)`` call.
|
|
127
|
+
|
|
128
|
+
Pure string assembly; used both by :func:`build_simulate_mos` (batch
|
|
129
|
+
path) and by the persistent-worker backend, which passes the same args
|
|
130
|
+
to ``session.sendExpression("simulate(<model>, <args>)")``.
|
|
131
|
+
|
|
132
|
+
OM's ``simulate()`` has ``numberOfIntervals`` but not ``outputInterval``
|
|
133
|
+
(Dymola's ``simulateModel`` has the latter), so we convert
|
|
134
|
+
``output_interval`` → ``numberOfIntervals`` when that's what the test
|
|
135
|
+
specified. Solver names are lowercased here so a framework-wide
|
|
136
|
+
Dymola-style ``"Dassl"`` survives the crossing.
|
|
137
|
+
|
|
138
|
+
``mat_sync`` adds ``simflags="-mat_sync=1"``: the simulation executable
|
|
139
|
+
then writes each output row to the result file as it is produced, where
|
|
140
|
+
it otherwise leaves them in a buffer until a block is full. A run that
|
|
141
|
+
is watched (``Config.live_results``) asks for it; no other run does, so
|
|
142
|
+
with it off the arguments are exactly what they were before it existed.
|
|
143
|
+
"""
|
|
144
|
+
diagnostic_vars = list(diagnostic_vars) # read twice below
|
|
145
|
+
args: list[str] = []
|
|
146
|
+
args.append(f"stopTime={float(test.stop_time)!r}")
|
|
147
|
+
if test.number_of_intervals is not None:
|
|
148
|
+
args.append(f"numberOfIntervals={int(test.number_of_intervals)}")
|
|
149
|
+
elif test.output_interval is not None and test.output_interval > 0:
|
|
150
|
+
n_intervals = max(
|
|
151
|
+
1, int(round(float(test.stop_time) / float(test.output_interval)))
|
|
152
|
+
)
|
|
153
|
+
args.append(f"numberOfIntervals={n_intervals}")
|
|
154
|
+
args.append(f"tolerance={float(test.tolerance)!r}")
|
|
155
|
+
args.append(_format_sim_kwarg("method", (test.method or "dassl").lower()))
|
|
156
|
+
args.append('outputFormat="mat"')
|
|
157
|
+
args.append('fileNamePrefix="result"')
|
|
158
|
+
var_filter = build_variable_filter(
|
|
159
|
+
test.variable_patterns,
|
|
160
|
+
diagnostic_vars,
|
|
161
|
+
# The UnitTests component's variables are in no spec pattern: both
|
|
162
|
+
# OM backends would otherwise filter them out of the .mat. Naming
|
|
163
|
+
# them here is what keeps the two paths saying the same thing. The
|
|
164
|
+
# variables a criterion names follow them (tracked because scored).
|
|
165
|
+
extra_names=[
|
|
166
|
+
*(v.name for v in unit_test_variables(test)),
|
|
167
|
+
*criterion_variables(test, diagnostic_vars),
|
|
168
|
+
],
|
|
169
|
+
)
|
|
170
|
+
args.append(_format_sim_kwarg("variableFilter", var_filter))
|
|
171
|
+
if mat_sync:
|
|
172
|
+
args.append(_format_sim_kwarg("simflags", "-mat_sync=1"))
|
|
173
|
+
return args
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_simulate_mos(
|
|
177
|
+
*,
|
|
178
|
+
test: TestModel,
|
|
179
|
+
test_dir: Path,
|
|
180
|
+
library_package_mo: Path,
|
|
181
|
+
dependencies: list[str],
|
|
182
|
+
backend_setup: list[str],
|
|
183
|
+
diagnostic_vars: list[str],
|
|
184
|
+
std_version: str = "latest",
|
|
185
|
+
mat_sync: bool = False,
|
|
186
|
+
) -> str:
|
|
187
|
+
"""Assemble the full per-test .mos script.
|
|
188
|
+
|
|
189
|
+
See the module docstring for the shape. All path arguments are emitted
|
|
190
|
+
with forward slashes (OM accepts them on Windows too, and it sidesteps
|
|
191
|
+
Modelica string-escaping of backslashes).
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def fwd(path: Path) -> str:
|
|
195
|
+
return str(path).replace("\\", "/")
|
|
196
|
+
|
|
197
|
+
lines: list[str] = []
|
|
198
|
+
lines.append(f'setCommandLineOptions("--std={std_version}");')
|
|
199
|
+
|
|
200
|
+
for dep in dependencies:
|
|
201
|
+
kind, arg = classify_dependency(dep)
|
|
202
|
+
if kind == "loadModel":
|
|
203
|
+
lines.append(f"loadModel({arg});")
|
|
204
|
+
else:
|
|
205
|
+
lines.append(f'loadFile("{arg}");')
|
|
206
|
+
lines.append("getErrorString();")
|
|
207
|
+
|
|
208
|
+
# Main library
|
|
209
|
+
lines.append(f'loadFile("{fwd(library_package_mo)}");')
|
|
210
|
+
lines.append("getErrorString();")
|
|
211
|
+
|
|
212
|
+
# Setup commands (backend-specific; user owns these)
|
|
213
|
+
for cmd in backend_setup:
|
|
214
|
+
c = cmd.strip()
|
|
215
|
+
if not c.endswith(";"):
|
|
216
|
+
c = c + ";"
|
|
217
|
+
lines.append(c)
|
|
218
|
+
|
|
219
|
+
lines.append(f'cd("{fwd(test_dir)}");')
|
|
220
|
+
|
|
221
|
+
sim_args = build_simulate_args(test, diagnostic_vars, mat_sync=mat_sync)
|
|
222
|
+
# Bare simulate(...) — omc REPL-echoes the SimulationResult record to
|
|
223
|
+
# stdout, which log_parser parses. No ``res := ...`` assignment and no
|
|
224
|
+
# follow-up prints (see module docstring).
|
|
225
|
+
lines.append(f"simulate({test.model_id}, {', '.join(sim_args)});")
|
|
226
|
+
lines.append("getErrorString();")
|
|
227
|
+
|
|
228
|
+
return "\n".join(lines) + "\n"
|