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.
Files changed (185) hide show
  1. dstf/__init__.py +41 -0
  2. dstf/__main__.py +10 -0
  3. dstf/authoring/__init__.py +13 -0
  4. dstf/authoring/builtin/__init__.py +32 -0
  5. dstf/authoring/builtin/accept.py +205 -0
  6. dstf/authoring/builtin/edit_criteria.py +479 -0
  7. dstf/authoring/builtin/edit_family.py +228 -0
  8. dstf/authoring/builtin/edit_on_plot.py +1354 -0
  9. dstf/authoring/builtin/edit_tree.py +1399 -0
  10. dstf/authoring/builtin/enqueue.py +144 -0
  11. dstf/authoring/capabilities.py +463 -0
  12. dstf/authoring/errors.py +49 -0
  13. dstf/authoring/family_rows.py +526 -0
  14. dstf/authoring/forms.py +631 -0
  15. dstf/authoring/layout.py +104 -0
  16. dstf/authoring/live_rows.py +337 -0
  17. dstf/authoring/live_runs.py +385 -0
  18. dstf/authoring/needle.py +127 -0
  19. dstf/authoring/picker_rows.py +92 -0
  20. dstf/authoring/queue_rows.py +354 -0
  21. dstf/authoring/session.py +3016 -0
  22. dstf/authoring/shell/__init__.py +9 -0
  23. dstf/authoring/shell/actions.py +702 -0
  24. dstf/authoring/shell/app.py +53 -0
  25. dstf/authoring/shell/drawer_controls.py +190 -0
  26. dstf/authoring/shell/family_fold.py +468 -0
  27. dstf/authoring/shell/figure_hook.py +249 -0
  28. dstf/authoring/shell/grips.js +1000 -0
  29. dstf/authoring/shell/header.py +783 -0
  30. dstf/authoring/shell/live_view.py +93 -0
  31. dstf/authoring/shell/picker.py +267 -0
  32. dstf/authoring/shell/placing.py +122 -0
  33. dstf/authoring/shell/plot_card.py +1171 -0
  34. dstf/authoring/shell/queue_panel.py +433 -0
  35. dstf/authoring/shell/rail.py +593 -0
  36. dstf/authoring/shell/shell.py +528 -0
  37. dstf/authoring/shell/stats_card.py +501 -0
  38. dstf/authoring/shell/strip.py +204 -0
  39. dstf/authoring/shell/style.py +721 -0
  40. dstf/authoring/shell/suite.py +277 -0
  41. dstf/authoring/shell/suite_grid.js +414 -0
  42. dstf/authoring/shell/suite_grid.py +373 -0
  43. dstf/authoring/shell/tab_theme.py +110 -0
  44. dstf/authoring/shell/tabstate.py +146 -0
  45. dstf/authoring/shell/test_list.py +179 -0
  46. dstf/authoring/shell/test_page.py +1404 -0
  47. dstf/authoring/shell/tree_card.py +1171 -0
  48. dstf/authoring/shell/variables_drawer.py +589 -0
  49. dstf/authoring/shell/widgets.py +188 -0
  50. dstf/authoring/situation.py +287 -0
  51. dstf/authoring/spec_document.py +215 -0
  52. dstf/authoring/spec_preview.py +83 -0
  53. dstf/authoring/suite_rows.py +99 -0
  54. dstf/authoring/tree_move.py +516 -0
  55. dstf/authoring/tree_pointer.py +33 -0
  56. dstf/authoring/tree_text.py +61 -0
  57. dstf/authoring/typed_number.js +64 -0
  58. dstf/authoring/typed_number.py +76 -0
  59. dstf/authoring/unscorable.py +141 -0
  60. dstf/authoring/variable_rows.py +216 -0
  61. dstf/backends/__init__.py +283 -0
  62. dstf/backends/base.py +1117 -0
  63. dstf/backends/common/__init__.py +1 -0
  64. dstf/backends/common/json_driver.py +484 -0
  65. dstf/backends/common/live_mat.py +369 -0
  66. dstf/backends/common/mat_reader.py +632 -0
  67. dstf/backends/common/proc_kill.py +238 -0
  68. dstf/backends/common/proc_output.py +20 -0
  69. dstf/backends/common/result_files.py +109 -0
  70. dstf/backends/common/result_variables.py +329 -0
  71. dstf/backends/common/statistics.py +39 -0
  72. dstf/backends/cross_backend.py +193 -0
  73. dstf/backends/dymola/__init__.py +5 -0
  74. dstf/backends/dymola/backend.py +776 -0
  75. dstf/backends/dymola/interface_loader.py +241 -0
  76. dstf/backends/dymola/log_parser.py +396 -0
  77. dstf/backends/dymola/persistent_backend.py +949 -0
  78. dstf/backends/fmpy/__init__.py +11 -0
  79. dstf/backends/fmpy/backend.py +443 -0
  80. dstf/backends/julia/__init__.py +27 -0
  81. dstf/backends/julia/backend.py +156 -0
  82. dstf/backends/julia/persistent_backend.py +541 -0
  83. dstf/backends/julia/run_persistent.jl +167 -0
  84. dstf/backends/julia/run_test.jl +146 -0
  85. dstf/backends/openmodelica/__init__.py +21 -0
  86. dstf/backends/openmodelica/backend.py +368 -0
  87. dstf/backends/openmodelica/init_xml.py +76 -0
  88. dstf/backends/openmodelica/log_parser.py +119 -0
  89. dstf/backends/openmodelica/mos_generator.py +228 -0
  90. dstf/backends/openmodelica/persistent_backend.py +1045 -0
  91. dstf/backends/openmodelica/session_loader.py +63 -0
  92. dstf/backends/persistent.py +1811 -0
  93. dstf/backends/pool.py +602 -0
  94. dstf/backends/progress.py +395 -0
  95. dstf/backends/python/__init__.py +11 -0
  96. dstf/backends/python/backend.py +141 -0
  97. dstf/backends/python/run_test.py +141 -0
  98. dstf/backends/run_metadata.py +103 -0
  99. dstf/backends/types.py +310 -0
  100. dstf/cli/__init__.py +12 -0
  101. dstf/cli/author_cmd.py +61 -0
  102. dstf/cli/baseline_cmds.py +341 -0
  103. dstf/cli/common.py +231 -0
  104. dstf/cli/diag_cmds.py +350 -0
  105. dstf/cli/main.py +561 -0
  106. dstf/cli/review.py +398 -0
  107. dstf/cli/run_cmds.py +589 -0
  108. dstf/cli/spec_cmds.py +123 -0
  109. dstf/comparison/__init__.py +13 -0
  110. dstf/comparison/algorithms.py +2127 -0
  111. dstf/comparison/builtins.py +291 -0
  112. dstf/comparison/comparator.py +1189 -0
  113. dstf/comparison/geometry.py +164 -0
  114. dstf/comparison/handles.py +1194 -0
  115. dstf/comparison/metric_tree.py +332 -0
  116. dstf/comparison/mode_schema.py +176 -0
  117. dstf/comparison/modes.py +788 -0
  118. dstf/comparison/registry.py +230 -0
  119. dstf/comparison/seeds.py +281 -0
  120. dstf/comparison/sides.py +158 -0
  121. dstf/comparison/snap.py +62 -0
  122. dstf/comparison/strict_keys.py +246 -0
  123. dstf/comparison/summaries.py +354 -0
  124. dstf/comparison/tree_eval.py +868 -0
  125. dstf/comparison/tree_spec.py +678 -0
  126. dstf/comparison/types.py +194 -0
  127. dstf/comparison/validator.py +232 -0
  128. dstf/config.py +815 -0
  129. dstf/discovery/__init__.py +0 -0
  130. dstf/discovery/json_recognizer.py +702 -0
  131. dstf/discovery/library_state.py +248 -0
  132. dstf/discovery/mo_parser.py +794 -0
  133. dstf/discovery/patch_apply.py +308 -0
  134. dstf/discovery/patch_envelope.py +63 -0
  135. dstf/discovery/plots.py +318 -0
  136. dstf/discovery/recognizer.py +103 -0
  137. dstf/discovery/scan_cache.py +154 -0
  138. dstf/discovery/spec_parser.py +538 -0
  139. dstf/discovery/spec_shape.py +122 -0
  140. dstf/discovery/spec_update.py +493 -0
  141. dstf/discovery/test_registry.py +488 -0
  142. dstf/discovery/unit_tests.py +95 -0
  143. dstf/figures/__init__.py +14 -0
  144. dstf/figures/assemble.py +48 -0
  145. dstf/figures/decimate.py +150 -0
  146. dstf/figures/families.py +289 -0
  147. dstf/figures/palette.py +170 -0
  148. dstf/figures/plotly.py +1413 -0
  149. dstf/figures/view.py +233 -0
  150. dstf/pipeline.py +435 -0
  151. dstf/plugins.py +104 -0
  152. dstf/reporting/__init__.py +0 -0
  153. dstf/reporting/console_report.py +187 -0
  154. dstf/reporting/context_builder.py +979 -0
  155. dstf/reporting/dashboard_render.py +534 -0
  156. dstf/reporting/junit_report.py +192 -0
  157. dstf/reporting/overlay_loader.py +708 -0
  158. dstf/reporting/report_suite.py +1059 -0
  159. dstf/reporting/rows.py +458 -0
  160. dstf/reporting/schema_export.py +413 -0
  161. dstf/reporting/stats_rows.py +321 -0
  162. dstf/reporting/status.py +388 -0
  163. dstf/reporting/templates/axes.js +61 -0
  164. dstf/reporting/templates/dashboard.html +284 -0
  165. dstf/reporting/templates/dashboard.js +604 -0
  166. dstf/reporting/templates/filters.js +100 -0
  167. dstf/reporting/templates/interactive.html +420 -0
  168. dstf/reporting/templates/report.js +116 -0
  169. dstf/reporting/templates/vendor/plotly.min.js +8 -0
  170. dstf/reporting/theme.py +324 -0
  171. dstf/reporting/tree_rows.py +73 -0
  172. dstf/reporting/verdicts.py +124 -0
  173. dstf/storage/__init__.py +0 -0
  174. dstf/storage/accept.py +216 -0
  175. dstf/storage/baseline.py +43 -0
  176. dstf/storage/batch_manifest.py +204 -0
  177. dstf/storage/ref_index.py +295 -0
  178. dstf/storage/reference_store.py +661 -0
  179. dstf/storage/roles.py +576 -0
  180. dstf/storage/run_stats.py +85 -0
  181. dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
  182. dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
  183. dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
  184. dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
  185. 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
@@ -0,0 +1,5 @@
1
+ """Dymola backend."""
2
+
3
+ from .backend import DymolaBackend, DymolaConfig
4
+
5
+ __all__ = ["DymolaConfig", "DymolaBackend"]