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,39 @@
|
|
|
1
|
+
"""The zero-count rule: a system-size list's summaries, written and read in
|
|
2
|
+
one place."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
SUMMARIES = ("count", "total", "max")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def summaries(key: str, values: list[int]) -> dict[str, int]:
|
|
10
|
+
"""`<key>_count/_total/_max` of a system-size list — 0, 0, 0 for an empty
|
|
11
|
+
one. The parser writes these ALWAYS: code that writes never leaves a
|
|
12
|
+
field out (a reader would have to guess what its absence means)."""
|
|
13
|
+
return {
|
|
14
|
+
f"{key}_count": len(values),
|
|
15
|
+
f"{key}_total": sum(values),
|
|
16
|
+
f"{key}_max": max(values, default=0),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sizes_systems(value: object) -> bool:
|
|
21
|
+
"""Is ``value`` a system-size list — the sizes the parser summarizes
|
|
22
|
+
(``nonlinear: [3, 5, 1]``, or ``[]``)? A list of whole numbers; a list
|
|
23
|
+
of names (``state_names``) is not one."""
|
|
24
|
+
return isinstance(value, list) and all(
|
|
25
|
+
isinstance(v, int) and not isinstance(v, bool) for v in value
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_summary(category: dict, key: str) -> int | None:
|
|
30
|
+
"""A summary a baseline written before `summaries` lacks: 0 where
|
|
31
|
+
`key` is `<list>_count|_total|_max` and `<list>` is present AND EMPTY in
|
|
32
|
+
the same category. Anything else stays missing — a stats path absent
|
|
33
|
+
from a side hard-fails its leaf (tree_eval: a path that silently scored
|
|
34
|
+
0 would be a green test measuring nothing), and only an empty list
|
|
35
|
+
says what its summary is."""
|
|
36
|
+
base, _, field = key.rpartition("_")
|
|
37
|
+
if field not in SUMMARIES or key in category:
|
|
38
|
+
return None
|
|
39
|
+
return 0 if category.get(base) == [] else None
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Cross-backend orchestration (4.B.3). **EXPERIMENTAL** (D65).
|
|
2
|
+
|
|
3
|
+
Helpers that chain backends to produce a **soft_check** baseline (D66).
|
|
4
|
+
Today the only chain is **dymola-via-fmpy**: Dymola exports the model as
|
|
5
|
+
an FMU, FMPy simulates the FMU, and the FMPy result is stored as a
|
|
6
|
+
soft_check (`"dymola-via-fmpy"` by convention) that MetricTree leaves
|
|
7
|
+
can score against via ``"against": "dymola-via-fmpy"`` — always inside
|
|
8
|
+
a ``warn`` combinator, enforced by the validator.
|
|
9
|
+
|
|
10
|
+
EXPERIMENTAL — scope limits (D65):
|
|
11
|
+
- **Semantics**: the chain is only meaningful for *autonomous* tests —
|
|
12
|
+
models with no external inputs, no python driver stepping the FMU, no
|
|
13
|
+
need to choose between co-simulation and model-exchange. For a test
|
|
14
|
+
that is fundamentally "a python script driving an FMU with a scheduled
|
|
15
|
+
input sequence", this chain will produce a baseline whose values do
|
|
16
|
+
NOT reflect what the test actually does — the comparison against the
|
|
17
|
+
primary run becomes meaningless. Opt-in via ``requested_baselines``
|
|
18
|
+
should be reserved for autonomous tests until generalization lands.
|
|
19
|
+
- **Validation**: the Dymola export step requires Windows + Dymola + the
|
|
20
|
+
FMI export license option. Cannot be exercised in CI on Linux WSL —
|
|
21
|
+
tests mock the `export_fmu` step. End-to-end validation on real Dymola
|
|
22
|
+
is deferred to a dedicated phase.
|
|
23
|
+
|
|
24
|
+
Generalization (input schedules, CS/ME choice, start-value overrides,
|
|
25
|
+
python-driver tests) is deferred to a future "FMU-path semantic gap
|
|
26
|
+
closure" phase.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from dataclasses import replace
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from ..config import Config
|
|
36
|
+
from ..discovery.test_registry import TestModel
|
|
37
|
+
from ..storage.reference_store import ReferenceStore
|
|
38
|
+
from .base import Backend
|
|
39
|
+
from .types import Capability, TestResult
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Convention name for the cross-backend baseline. Users reference this via
|
|
45
|
+
# ``{"metric": ..., "against": "dymola-via-fmpy"}`` in a MetricTree leaf.
|
|
46
|
+
CROSS_BACKEND_BASELINE_NAME = "dymola-via-fmpy"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def produce_dymola_via_fmpy_baseline(
|
|
50
|
+
test: TestModel,
|
|
51
|
+
primary_backend: Backend,
|
|
52
|
+
config: Config,
|
|
53
|
+
store: ReferenceStore,
|
|
54
|
+
*,
|
|
55
|
+
fmu_dir: Path | None = None,
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""Run the Dymola → FMPy chain for one test and store the result.
|
|
58
|
+
|
|
59
|
+
Steps:
|
|
60
|
+
1. ``primary_backend.export_fmu(test, fmu_dir)`` produces a .fmu.
|
|
61
|
+
2. Build a clone of the test pointing at the FMU.
|
|
62
|
+
3. Construct an :class:`FmpyBackend` (config tweaked to source_type="fmu").
|
|
63
|
+
4. Run FMPy on the FMU + read back the result.
|
|
64
|
+
5. Persist the FMPy result as a soft_check via
|
|
65
|
+
:meth:`ReferenceStore.add_soft_check` (D66).
|
|
66
|
+
|
|
67
|
+
Returns True on success, False if any step fails (logs the reason).
|
|
68
|
+
Requires the model to already have a primary baseline on disk —
|
|
69
|
+
soft_checks augment primary, they do not replace it.
|
|
70
|
+
"""
|
|
71
|
+
work = fmu_dir or (config.work_dir / f"chain_{test.model_id.replace('.', '_')}")
|
|
72
|
+
|
|
73
|
+
# Step 1 — export the FMU via the primary backend. Gate on the declared
|
|
74
|
+
# capability rather than catching NotImplementedError: "this backend
|
|
75
|
+
# can't export" is a configuration fact knowable before we do any work,
|
|
76
|
+
# and the flag is the framework's single source of truth for it (batch
|
|
77
|
+
# Dymola, for instance, can't — only the persistent variant can).
|
|
78
|
+
if Capability.FMU_EXPORT not in primary_backend.capabilities:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"cross-backend chain: %s does not declare Capability.FMU_EXPORT; "
|
|
81
|
+
"skipping %s",
|
|
82
|
+
type(primary_backend).__name__,
|
|
83
|
+
test.model_id,
|
|
84
|
+
)
|
|
85
|
+
return False
|
|
86
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
try:
|
|
88
|
+
fmu_path = primary_backend.export_fmu(test, work)
|
|
89
|
+
except (NotImplementedError, RuntimeError) as exc:
|
|
90
|
+
logger.warning(
|
|
91
|
+
"cross-backend chain: export_fmu failed for %s: %s",
|
|
92
|
+
test.model_id,
|
|
93
|
+
exc,
|
|
94
|
+
)
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Step 2 — clone the test with source_file pointing at the new FMU.
|
|
98
|
+
chain_test = replace(test, source_file=fmu_path)
|
|
99
|
+
|
|
100
|
+
# Step 3 — construct an FmpyBackend. We don't go through get_backend()
|
|
101
|
+
# because that uses config.backend; we just want a temporary
|
|
102
|
+
# FMPy backend regardless of what the primary is.
|
|
103
|
+
from .fmpy.backend import FmpyBackend
|
|
104
|
+
|
|
105
|
+
fmpy_config = replace(
|
|
106
|
+
config,
|
|
107
|
+
backend_name="FMPy",
|
|
108
|
+
source_type="fmu",
|
|
109
|
+
# Reuse work_dir; FmpyBackend persists result.npz under <work_dir>/<test_key>.
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
fmpy_backend = FmpyBackend(fmpy_config)
|
|
113
|
+
except RuntimeError as exc:
|
|
114
|
+
# The constructor's "fmpy not installed" is a RuntimeError, the same
|
|
115
|
+
# unavailable-backend signal `backends._instantiate` translates.
|
|
116
|
+
logger.warning(
|
|
117
|
+
"cross-backend chain: FMPy not installed (%s); skipping %s",
|
|
118
|
+
exc,
|
|
119
|
+
test.model_id,
|
|
120
|
+
)
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Step 4 — run FMPy on the exported FMU. Use a deterministic test_key
|
|
124
|
+
# that doesn't collide with the primary run's keys.
|
|
125
|
+
test_key = f"chain_{test.model_id.replace('.', '_')}"
|
|
126
|
+
run_result = fmpy_backend.run_single_test(
|
|
127
|
+
chain_test,
|
|
128
|
+
test_key,
|
|
129
|
+
index=1,
|
|
130
|
+
total=1,
|
|
131
|
+
)
|
|
132
|
+
if not run_result.success:
|
|
133
|
+
logger.warning(
|
|
134
|
+
"cross-backend chain: FMPy sim failed for %s: %s",
|
|
135
|
+
test.model_id,
|
|
136
|
+
run_result.error_message,
|
|
137
|
+
)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
fmpy_result: TestResult = fmpy_backend.read_result(chain_test, test_key, run_result)
|
|
141
|
+
if not fmpy_result.success:
|
|
142
|
+
logger.warning(
|
|
143
|
+
"cross-backend chain: FMPy read_result failed for %s: %s",
|
|
144
|
+
test.model_id,
|
|
145
|
+
fmpy_result.error_message,
|
|
146
|
+
)
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
# Step 5 — store FMPy result as a named baseline.
|
|
150
|
+
if not fmpy_result.variables:
|
|
151
|
+
logger.warning(
|
|
152
|
+
"cross-backend chain: FMPy produced no variables for %s; "
|
|
153
|
+
"skipping baseline write",
|
|
154
|
+
test.model_id,
|
|
155
|
+
)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# All FMPy variables share the same time vector; pull from the first.
|
|
159
|
+
time_vec = list(fmpy_result.variables[0].time)
|
|
160
|
+
variables_payload = [
|
|
161
|
+
{
|
|
162
|
+
"index": v.index,
|
|
163
|
+
"name": v.name,
|
|
164
|
+
"expression": v.expression,
|
|
165
|
+
"values": list(v.values),
|
|
166
|
+
}
|
|
167
|
+
for v in fmpy_result.variables
|
|
168
|
+
]
|
|
169
|
+
try:
|
|
170
|
+
store.add_soft_check(
|
|
171
|
+
test.model_id,
|
|
172
|
+
CROSS_BACKEND_BASELINE_NAME,
|
|
173
|
+
time=time_vec,
|
|
174
|
+
variables=variables_payload,
|
|
175
|
+
provenance={
|
|
176
|
+
"source": "cross-backend chain",
|
|
177
|
+
"primary_backend": type(primary_backend).__name__,
|
|
178
|
+
"secondary_backend": "FMPy",
|
|
179
|
+
"fmu_path": str(fmu_path),
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
183
|
+
# review 2026-07-06 (finding 76): add_soft_check also raises
|
|
184
|
+
# ValueError (e.g. name collides with an existing companion) — the
|
|
185
|
+
# documented contract is "failures are logged + skipped", not an
|
|
186
|
+
# abort of the whole run after simulation completed.
|
|
187
|
+
logger.warning(
|
|
188
|
+
"cross-backend chain: cannot store baseline for %s: %s",
|
|
189
|
+
test.model_id,
|
|
190
|
+
exc,
|
|
191
|
+
)
|
|
192
|
+
return False
|
|
193
|
+
return True
|