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,321 @@
1
+ """The Statistics rows both tiers render: the report's Statistics table and
2
+ the tool's Statistics card.
3
+
4
+ One row per statistic the run or its baseline recorded, side by side. A
5
+ row is ``changed`` when the criterion its ``+`` would add would fail — the
6
+ scalar mode's own verdict over the seed
7
+ :func:`~dstf.comparison.seeds.seed_quantity` writes, so the tolerance lives
8
+ in one place. Not the report's old "any difference": a CPU time 3% off is
9
+ not a change the tool would score.
10
+
11
+ The rows keep the report's order and sections: translation, simulation,
12
+ timing, then any other section in the order they appear (the baseline's
13
+ first); keys alphabetical inside a section, ``timing`` its fixed four
14
+ first; the top-level scalars (``CPUtime``, ``EventCounter``, folded in from
15
+ the diagnostics) sorted in among the Simulation keys — or, with no
16
+ ``simulation`` section on either side, a Simulation section of their own
17
+ at the end.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Callable, Iterable, Mapping, Sequence
23
+ from dataclasses import dataclass
24
+ from typing import Any, Literal
25
+
26
+ import numpy as np
27
+
28
+ from ..backends.common.statistics import SUMMARIES, read_summary, sizes_systems
29
+ from ..comparison.modes import ScalarConfig, ScalarMode
30
+ from ..comparison.seeds import quantity_kind, seed_quantity
31
+ from ..comparison.tree_eval import resolve_scalar_path
32
+
33
+ #: The section a leaf's quantity lands in when no row holds it (S4).
34
+ OTHER = "other scored"
35
+ SECTION_TITLES = {
36
+ "translation": "Translation",
37
+ "simulation": "Simulation",
38
+ "timing": "Timing",
39
+ OTHER: "Other scored quantities",
40
+ }
41
+ _PREFERRED = ("translation", "simulation", "timing")
42
+ _TIMING_RANK = {
43
+ k: i
44
+ for i, k in enumerate(("translation_wall", "sim_wall", "other_wall", "total_wall"))
45
+ }
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class StatRow:
50
+ section: str # "translation" | "simulation" | "timing" | other | OTHER
51
+ key: str # the key inside its section ("nonlinear_count", "CPUtime")
52
+ path: str # the quantity path a leaf reads ("translation.nonlinear_count")
53
+ label: str # "Nonlinear count"; a top-level name as is
54
+ kind: Literal["count", "time", "text"]
55
+ run: Any # a number, or a list / string for a `text` row; None: not recorded
56
+ baseline: Any
57
+ changed: bool
58
+ delta: str # "0→3" | "+38%" | "+0.12 s" | "" (S6)
59
+
60
+
61
+ def changed(
62
+ path: str,
63
+ kind: Literal["count", "time"],
64
+ run: float | None,
65
+ baseline: float | None,
66
+ ) -> bool:
67
+ """Would the criterion a ``+`` on this row adds fail? Never when a side
68
+ has no value (S3): there is nothing to compare, and an accept records
69
+ it."""
70
+ if run is None or baseline is None:
71
+ return False
72
+ seed = seed_quantity(path, kind, into="and")
73
+ leaf = seed["children"][0] if "combinator" in seed else seed
74
+ params = {k: v for k, v in leaf.items() if k not in ("metric", "variable")}
75
+ got = ScalarMode(ScalarConfig(**params)).compare(
76
+ np.array([0.0]),
77
+ np.array([float(baseline)]),
78
+ np.array([0.0]),
79
+ np.array([float(run)]),
80
+ )
81
+ return not got.passed
82
+
83
+
84
+ def _is_number(v: object) -> bool:
85
+ return isinstance(v, (int, float)) and not isinstance(v, bool)
86
+
87
+
88
+ def _number(v: object) -> float | None:
89
+ return float(v) if isinstance(v, (int, float)) else None
90
+
91
+
92
+ def _count_text(v: float) -> str:
93
+ return str(int(v)) if float(v).is_integer() else f"{v:g}"
94
+
95
+
96
+ def _rounded(v: float) -> str:
97
+ """3 significant figures KEEPING zeros (``0.800``, ``2.10``): ``0.8``
98
+ beside ``1.12`` read as a coarser number. ``100.`` loses its point, and
99
+ from 999.5 a time is whole seconds (``1234``): ``#.3g`` wrote
100
+ ``1.00e+03``."""
101
+ return f"{v:.0f}" if abs(v) >= 999.5 else f"{v:#.3g}".rstrip(".")
102
+
103
+
104
+ def _value_text(kind: str, value: Any, time: Callable[[float], str]) -> str:
105
+ if value is None:
106
+ return ""
107
+ if kind == "text" or not _is_number(value):
108
+ return _text(value)
109
+ if kind == "time":
110
+ return f"{time(float(value))} s"
111
+ return _count_text(value)
112
+
113
+
114
+ def report_text(kind: str, value: Any) -> str:
115
+ """A row's value as the REPORT prints it, in full: a time with its unit
116
+ (``0.045845 s``, ``1.600425 s``, ``1234567.89 s`` — ``:.15g``, where
117
+ ``:g`` stopped at six figures, ``1.60042 s``), a count whole
118
+ (``1234567``, never ``1.23457e+06``), a list joined; ``""`` for a side
119
+ that recorded none. The report is where a number is read to its last
120
+ digit."""
121
+ return _value_text(kind, value, lambda v: f"{v:.15g}")
122
+
123
+
124
+ def card_text(kind: str, value: Any) -> str:
125
+ """A row's value as the tool's CARD prints it, rounded to be compared
126
+ at a glance: a time to 3 significant figures keeping its zeros
127
+ (``0.800 s``), whole seconds from 999.5 s (``1234 s``); otherwise as
128
+ :func:`report_text`."""
129
+ return _value_text(kind, value, _rounded)
130
+
131
+
132
+ def _delta(kind: str, run: float | None, baseline: float | None) -> str:
133
+ """S6: a count reads ``0→3``, a time ``+38%`` (``+0.12 s`` over a zero
134
+ baseline) — whenever both sides are present and differ, changed or
135
+ not. Under 1% a time keeps one decimal (``+0.3%``), and under 0.05%
136
+ reads ``+<0.1%``: a time that moved never reads ``+0%`` or ``+0.0%``."""
137
+ if run is None or baseline is None or run == baseline:
138
+ return ""
139
+ if kind == "count":
140
+ return f"{_count_text(baseline)}→{_count_text(run)}"
141
+ if baseline == 0:
142
+ return f"{run - baseline:+.2g} s"
143
+ ratio = (run - baseline) / baseline
144
+ if abs(ratio) < 0.0005:
145
+ return f"{'+' if ratio > 0 else '-'}<0.1%"
146
+ return f"{ratio:+.1%}" if abs(ratio) < 0.01 else f"{ratio:+.0%}"
147
+
148
+
149
+ def _text(v: object) -> str:
150
+ """The report's formatting of a list or string: joined; None as empty."""
151
+ if isinstance(v, list):
152
+ return ", ".join(str(x) for x in v)
153
+ if v is None or v == "":
154
+ return ""
155
+ return str(v)
156
+
157
+
158
+ #: A word a label spells in capitals: `cpu_time_integration` reads "CPU
159
+ #: time integration".
160
+ _UPPER = {"cpu": "CPU"}
161
+
162
+
163
+ def _label(key: str, top_level: bool) -> str:
164
+ if top_level:
165
+ return key
166
+ text = " ".join(_UPPER.get(word, word) for word in key.split("_"))
167
+ return text[:1].upper() + text[1:]
168
+
169
+
170
+ def _diagnostic_values(diagnostics: Sequence, name: str) -> np.ndarray | None:
171
+ """The run's samples of the diagnostic ``name``, if it recorded one."""
172
+ for entry in diagnostics:
173
+ if getattr(entry, "name", None) == name:
174
+ values = getattr(entry, "values", None)
175
+ return None if values is None else np.asarray(values, dtype=float)
176
+ return None
177
+
178
+
179
+ def _row(
180
+ section: str,
181
+ key: str,
182
+ path: str,
183
+ run: object,
184
+ baseline: object,
185
+ *,
186
+ top_level: bool = False,
187
+ samples: np.ndarray | None = None,
188
+ ) -> StatRow:
189
+ label = _label(key, top_level)
190
+ if (run is None or _is_number(run)) and (baseline is None or _is_number(baseline)):
191
+ kind = quantity_kind(path, samples)
192
+ r, b = _number(run), _number(baseline)
193
+ return StatRow(
194
+ section,
195
+ key,
196
+ path,
197
+ label,
198
+ kind,
199
+ run,
200
+ baseline,
201
+ changed(path, kind, r, b),
202
+ _delta(kind, r, b),
203
+ )
204
+ # A list or a string: the report's rule — formatted text differs, both
205
+ # sides non-empty. Nothing scores it, so the card never shows it (S1).
206
+ rt, bt = _text(run), _text(baseline)
207
+ return StatRow(
208
+ section,
209
+ key,
210
+ path,
211
+ label,
212
+ "text",
213
+ run,
214
+ baseline,
215
+ bool(rt and bt and rt != bt),
216
+ "",
217
+ )
218
+
219
+
220
+ def _section(stats: Mapping, name: str) -> Mapping:
221
+ got = stats.get(name)
222
+ return got if isinstance(got, Mapping) else {}
223
+
224
+
225
+ def _summary_or_none(category: Mapping, key: str) -> object:
226
+ if key in category:
227
+ return category[key]
228
+ return read_summary(dict(category), key)
229
+
230
+
231
+ def _summary_keys(*categories: Mapping) -> set[str]:
232
+ """``<list>_count`` / ``_total`` / ``_max`` of every system-size list
233
+ either side holds: the zero rule surfaces. A baseline written before
234
+ `summaries` holds ``nonlinear: []`` and no count; beside a run that
235
+ recorded none either, no row said the count stays 0, and no `+` could
236
+ require it."""
237
+ return {
238
+ f"{key}_{field}"
239
+ for category in categories
240
+ for key, value in category.items()
241
+ if sizes_systems(value)
242
+ for field in SUMMARIES
243
+ }
244
+
245
+
246
+ def statistics_rows(
247
+ run: Mapping | None,
248
+ baseline: Mapping | None,
249
+ *,
250
+ run_diagnostics: Sequence = (),
251
+ baseline_diagnostics: Sequence = (),
252
+ extra_paths: Iterable[str] = (),
253
+ ) -> list[StatRow]:
254
+ """Every statistic the run or its baseline recorded, in the report's
255
+ order (the module docstring), then a row per ``extra_paths`` entry no
256
+ row holds (S4: a leaf's quantity always has a home). A system-size
257
+ list on either side gives its three summary rows on both; a summary
258
+ key a side lacks reads through :func:`read_summary` (a baseline's empty
259
+ ``nonlinear`` gives ``nonlinear_count`` 0). Never writes into its
260
+ inputs."""
261
+ run, baseline = run or {}, baseline or {}
262
+ names: list[str] = []
263
+ for src in (baseline, run):
264
+ for k, v in src.items():
265
+ if isinstance(v, Mapping) and k not in names:
266
+ names.append(k)
267
+ top = sorted(
268
+ {
269
+ k
270
+ for src in (baseline, run)
271
+ for k, v in src.items()
272
+ if not isinstance(v, Mapping)
273
+ }
274
+ )
275
+ ordered = [n for n in _PREFERRED if n in names]
276
+ ordered += [n for n in names if n not in _PREFERRED]
277
+
278
+ def top_row(key: str) -> StatRow:
279
+ return _row(
280
+ "simulation",
281
+ key,
282
+ key,
283
+ run.get(key),
284
+ baseline.get(key),
285
+ top_level=True,
286
+ samples=_diagnostic_values(run_diagnostics, key),
287
+ )
288
+
289
+ rows: list[StatRow] = []
290
+ for name in ordered:
291
+ r_cat, b_cat = _section(run, name), _section(baseline, name)
292
+ keys = {*r_cat, *b_cat, *_summary_keys(r_cat, b_cat)}
293
+ keyed: list[tuple[str, str]] = [(k, f"{name}.{k}") for k in keys]
294
+ if name == "simulation":
295
+ keyed += [(k, k) for k in top if k not in r_cat and k not in b_cat]
296
+ keyed.sort()
297
+ if name == "timing":
298
+ keyed.sort(key=lambda kp: _TIMING_RANK.get(kp[0], len(_TIMING_RANK)))
299
+ for key, path in keyed:
300
+ if path == key:
301
+ rows.append(top_row(key))
302
+ continue
303
+ r = _summary_or_none(r_cat, key)
304
+ b = _summary_or_none(b_cat, key)
305
+ rows.append(_row(name, key, path, r, b))
306
+ if "simulation" not in names:
307
+ rows += [top_row(key) for key in top]
308
+
309
+ # A text row holds no leaf: the card never shows one (S1). `X.final`
310
+ # is the row `X` only for a top-level `X` (S2).
311
+ held = {row.path for row in rows if row.kind != "text"}
312
+ for path in dict.fromkeys(extra_paths):
313
+ name, _, field = path.rpartition(".")
314
+ if path in held or (field == "final" and "." not in name and name in held):
315
+ continue
316
+ r = resolve_scalar_path(path, dict(run), list(run_diagnostics))
317
+ b = resolve_scalar_path(path, dict(baseline), list(baseline_diagnostics))
318
+ samples = _diagnostic_values(run_diagnostics, name or path)
319
+ rows.append(_row(OTHER, path, path, r, b, top_level=True, samples=samples))
320
+ held.add(path)
321
+ return rows