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
dstf/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Dynamic Systems Testing Framework — regression & unit testing."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ # The DISTRIBUTION name, which is not the import name: you `import dstf`,
6
+ # but you install `dynamic-systems-testing-framework`. They differ because
7
+ # the short name was already taken on PyPI by an unrelated project. Every
8
+ # importlib.metadata lookup must use this constant — a stale literal does not
9
+ # raise, it silently yields the unknown-version fallback below, so
10
+ # tests/test_distribution_name.py pins this to pyproject.toml's [project] name.
11
+ _DIST_NAME = "dynamic-systems-testing-framework"
12
+
13
+ # Version source-of-truth lives in pyproject.toml; importlib.metadata
14
+ # reads the installed package's metadata so `dstf --version` and any
15
+ # downstream tooling stay in sync without manual duplication.
16
+ try:
17
+ from importlib.metadata import version as _pkg_version
18
+
19
+ __version__ = _pkg_version(_DIST_NAME)
20
+ except Exception: # noqa: BLE001 — fall back during editable / unbuilt installs
21
+ __version__ = "0.0.0+unknown"
22
+
23
+ if TYPE_CHECKING:
24
+ from .plugins import load_plugins
25
+
26
+ __all__ = ["__version__", "load_plugins"]
27
+
28
+
29
+ def __getattr__(name: str):
30
+ """Lazily expose :func:`dstf.plugins.load_plugins` at package level.
31
+
32
+ `import dstf` must stay near-free — the CLI imports it before argparse
33
+ has even run. Module-level `from .plugins import load_plugins` would
34
+ drag in the plug-in machinery on every import, so it resolves on first
35
+ attribute access instead (PEP 562).
36
+ """
37
+ if name == "load_plugins":
38
+ from .plugins import load_plugins
39
+
40
+ return load_plugins
41
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
dstf/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Entry point for `python -m dstf`.
2
+
3
+ Delegates to the same `main_entry` used by the `dstf` console
4
+ script so both invocation forms share one code path.
5
+ """
6
+
7
+ from .cli import main_entry
8
+
9
+ if __name__ == "__main__":
10
+ main_entry()
@@ -0,0 +1,13 @@
1
+ """The authoring tier's core — the Session, its spec document and the
2
+ capability contract (two-tier design, §4.5–4.6). Pure Python: no UI
3
+ framework is imported here; sub-project 4's shell sits on top.
4
+
5
+ Importing the package registers the capabilities it ships (the shell then
6
+ lists whatever is registered); the capability modules reach NiceGUI only
7
+ inside their ``panel``, so this import works without the ``[author]``
8
+ extra.
9
+ """
10
+
11
+ from .builtin import register_builtins
12
+
13
+ register_builtins()
@@ -0,0 +1,32 @@
1
+ """The capabilities that ship with the tool. They register through the same
2
+ call a plug-in makes; the shell learns of them the same way."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from ..capabilities import register_capability, registered_capabilities
7
+
8
+
9
+ def register_builtins() -> None:
10
+ """Idempotent, like ``comparison.builtins.register_builtins``."""
11
+ names = {c.name for c in registered_capabilities()}
12
+ from .accept import Accept
13
+ from .edit_criteria import EditCriteria
14
+ from .edit_family import EditFamily
15
+ from .edit_on_plot import EditOnPlot
16
+ from .edit_tree import EditTree
17
+ from .enqueue import EnqueueTests
18
+
19
+ # Before Edit criteria: a folded row's editor reads which elements,
20
+ # then what the criterion is (a host stacks its panels in this order).
21
+ if EditFamily.name not in names:
22
+ register_capability(EditFamily())
23
+ if EditCriteria.name not in names:
24
+ register_capability(EditCriteria())
25
+ if EditTree.name not in names:
26
+ register_capability(EditTree())
27
+ if Accept.name not in names:
28
+ register_capability(Accept())
29
+ if EnqueueTests.name not in names:
30
+ register_capability(EnqueueTests())
31
+ if EditOnPlot.name not in names:
32
+ register_capability(EditOnPlot())
@@ -0,0 +1,205 @@
1
+ """Accept (§4.6 table, §4.7): SUITE and TEST placements. The selected tests'
2
+ current results become baselines through
3
+ ``storage.accept.accept_run_results`` — the one function the CLI's two
4
+ ``--accept`` paths call — so the tool cannot accept a result the CLI would
5
+ refuse.
6
+
7
+ The confirm list says, per selected model, what accepting will do with it,
8
+ including the aborted-run refusal and the comparison's own (a base tolerance
9
+ that is no number, in the comparison's sentence), so it never promises what the shared
10
+ function declines — with one exception it cannot see from here: a trajectory
11
+ carrying NaN/Inf is refused by ``store_reference`` as it writes, and shows up
12
+ in the write's own line instead. The button's own availability is read off
13
+ those same rows, so an enabled Accept always has at least one baseline to
14
+ write. The write reports one line (§4.6 amendment 1): what it stored, every
15
+ refusal with its reason, and the shared function's warning for a baseline
16
+ file that is large (D153).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from ...backends.pool import ItemState
24
+ from ...reporting.status import classify, refusal
25
+ from ...storage.accept import accept_run_results
26
+ from ...storage.batch_manifest import unfinished_run_models
27
+ from ..capabilities import Emphasis, Panel, Placement, StoreWrite, plural
28
+ from ..situation import Situation
29
+
30
+ if TYPE_CHECKING:
31
+ from pathlib import Path
32
+
33
+ from ...storage.reference_store import ReferenceStore
34
+ from ..session import SessionView
35
+
36
+ # NO module-level `nicegui` import: `import dstf.authoring` runs
37
+ # register_builtins(), and the Session must import without the extra.
38
+
39
+ #: The confirm row's verdict on a model — every word but the last is a
40
+ #: reason the shared accept function stores no baseline for it; the last is
41
+ #: what it stores. ``WILL_STORE`` is the one truth the button's count, its
42
+ #: availability and this forecast share.
43
+ REFUSED = "re-run before accepting"
44
+ #: A run that did not finish left a partial result (D151): it is shown on
45
+ #: the test's page, and is no baseline.
46
+ PARTIAL = "a partial result is never a baseline: queue it again"
47
+ NO_RESULT = "no simulation result"
48
+ SIM_FAILED = "simulation failed"
49
+ SIMULATE_ONLY = "simulate-only: no baseline"
50
+ NO_VARIABLES = "no tracked variables in result"
51
+ WILL_STORE = "will store"
52
+
53
+ #: The states of a test the simulation queue holds.
54
+ _HELD = (ItemState.WAITING, ItemState.RUNNING)
55
+
56
+
57
+ def _rows(s: SessionView) -> list[tuple[str, str, str]]:
58
+ """``(model_id, status, what accept will do)`` for every selected model,
59
+ in project order."""
60
+ project = s.project
61
+ if project is None:
62
+ return []
63
+ selection = s.selection.tests
64
+ work_dir = project.config.work_dir
65
+ assert work_dir is not None # resolved in Config.__post_init__
66
+ unfinished = unfinished_run_models(work_dir)
67
+ rows: list[tuple[str, str, str]] = []
68
+ for test in project.tests:
69
+ model_id = test.model_id
70
+ if model_id not in selection:
71
+ continue
72
+ comp = s.comparisons.get(model_id)
73
+ status = classify(comp) if comp is not None else ""
74
+ result = project.results.get(model_id)
75
+ if s.partial(model_id) is not None:
76
+ action = PARTIAL # what its page draws is not a result
77
+ elif model_id in unfinished:
78
+ action = REFUSED # the guard runs before the result is looked at
79
+ elif result is None:
80
+ action = NO_RESULT
81
+ elif not result.success:
82
+ action = SIM_FAILED
83
+ elif comp is not None and (refused := refusal(comp)) is not None:
84
+ # A test the comparison refused: the row says the comparison's
85
+ # own sentence, which `accept_run_results` skips it with.
86
+ action = refused
87
+ elif test.is_simulate_only:
88
+ # `accept_results` neither stores nor skips these, by design.
89
+ action = SIMULATE_ONLY
90
+ elif not result.variables:
91
+ # `store_reference` returns False for a variable-less result and
92
+ # `accept_results` skips it with exactly this reason.
93
+ action = NO_VARIABLES
94
+ else:
95
+ action = WILL_STORE
96
+ rows.append((model_id, status, action))
97
+ return rows
98
+
99
+
100
+ def _report(
101
+ stored: int,
102
+ skipped: list[tuple[str, str]],
103
+ warnings: list[str],
104
+ notes: list[str],
105
+ *,
106
+ reference_dir: Path,
107
+ ) -> str:
108
+ """The one line the queue panel shows. A partial accept has no other way
109
+ to say which tests it refused and why — or that a baseline file it
110
+ wrote is large, or which tests it passed over as simulate-only, in the
111
+ sentences the CLI prints."""
112
+ parts = [f"Accept: stored {stored} baseline(s) to {reference_dir}"]
113
+ parts += [f"; skipped {model_id}: {reason}" for model_id, reason in skipped]
114
+ parts += [f"; {line}" for line in notes]
115
+ parts += [f"; {line}" for line in warnings]
116
+ return "".join(parts)
117
+
118
+
119
+ class Accept:
120
+ """The selection's current results as baselines, comparison settings
121
+ travelling with them as the CLI's accept does."""
122
+
123
+ name = "accept"
124
+ label = "Accept"
125
+ placements = frozenset({Placement.SUITE, Placement.TEST})
126
+ #: drawn as this icon where a bar draws icons (the Suite toolbar)
127
+ icon = "done_all"
128
+ #: the Test page's strip: the next action for a result that differs
129
+ #: from its baseline or has none; nothing new to accept otherwise
130
+ emphasis = {
131
+ Situation.FAIL: Emphasis.NEXT,
132
+ Situation.NO_REF: Emphasis.NEXT,
133
+ Situation.PASS: Emphasis.MENU,
134
+ Situation.EDITS: Emphasis.MENU,
135
+ # a criterion's variable to record first: accepted now, the
136
+ # baseline would not hold it
137
+ Situation.EDITS_NEW: Emphasis.MENU,
138
+ Situation.NEW_VARIABLES: Emphasis.MENU,
139
+ Situation.SIM_FAIL: Emphasis.MENU,
140
+ Situation.NO_RESULT: Emphasis.MENU,
141
+ Situation.QUEUED: Emphasis.MENU,
142
+ Situation.RUNNING: Emphasis.MENU,
143
+ Situation.CANCELLED: Emphasis.MENU,
144
+ Situation.START_FAILED: Emphasis.MENU,
145
+ }
146
+
147
+ def available(self, s: SessionView) -> bool | str:
148
+ project = s.project
149
+ if project is None:
150
+ return "no project open"
151
+ if not s.selection.tests:
152
+ return "Accept needs a selection"
153
+ selection = s.selection.tests
154
+ for test in project.tests:
155
+ # The Session's refusal (`apply`), said before the click: the
156
+ # result in memory is about to be replaced by its run's.
157
+ state = s.queue_state(test.model_id)
158
+ if test.model_id in selection and state in _HELD:
159
+ doing = "running" if state is ItemState.RUNNING else "queued"
160
+ return f"{test.model_id} is {doing}: accept it when its run has ended"
161
+ # The button's answer comes from the rows the panel draws, so an
162
+ # enabled button never reads `Accept 0`: a pending, simulate-only or
163
+ # variable-less result is a row the write will not store, and the
164
+ # button carries that row's reason.
165
+ rows = _rows(s)
166
+ if any(action == WILL_STORE for _, _, action in rows):
167
+ return True
168
+ if not rows:
169
+ return "no successful result in the selection"
170
+ return rows[0][2]
171
+
172
+ def caption(self, s: SessionView) -> str:
173
+ count = sum(1 for _, _, action in _rows(s) if action == WILL_STORE)
174
+ return f"Accept {plural(count, 'result')} as baselines"
175
+
176
+ def panel(self, s: SessionView, ui_panel: Panel) -> None:
177
+ from nicegui import ui
178
+
179
+ rows = _rows(s)
180
+ for model_id, status, action in rows:
181
+ ui.label(f"{model_id} · {status} · {action}").classes("text-xs")
182
+ ui.button(self.caption(s), on_click=lambda: ui_panel.submit()).props(
183
+ "dense no-caps"
184
+ ).mark("accept-submit")
185
+
186
+ def act(self, s: SessionView, **inputs: Any) -> StoreWrite:
187
+ project = s.project
188
+ if project is None:
189
+ raise ValueError("no project open")
190
+ selection = set(s.selection.tests)
191
+ # Everything the write needs is read HERE: `run` is handed a store
192
+ # by the Session and holds no view.
193
+ tests = [t for t in project.tests if t.model_id in selection]
194
+ results = dict(project.results)
195
+ work_dir = project.config.work_dir
196
+ assert work_dir is not None # resolved in Config.__post_init__
197
+ reference_dir = project.config.reference_dir
198
+
199
+ def run(store: ReferenceStore) -> str:
200
+ return _report(
201
+ *accept_run_results(store, tests, results, work_dir),
202
+ reference_dir=reference_dir,
203
+ )
204
+
205
+ return StoreWrite(models=frozenset(selection), run=run)