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,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
|