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,479 @@
|
|
|
1
|
+
"""Edit criteria (§4.6 table, §4.7): the CRITERION placement; a mode switch,
|
|
2
|
+
the mode's form, the window, the ``against`` selector — and the
|
|
3
|
+
STATISTIC_CRITERION one, a statistics criterion's editor under its
|
|
4
|
+
Statistics row, where the mode, the window and the quantity are no fields —
|
|
5
|
+
and the TREE_CRITERION one, any criterion's editor under its tree row (U5). Each change stages one
|
|
6
|
+
``replace`` of the whole leaf and the test rescores. The Session collapses
|
|
7
|
+
consecutive replaces of one leaf into one pending op, so the pending count
|
|
8
|
+
and the exported envelope carry one edit, never a keystroke log (the
|
|
9
|
+
preview drawer shows the entry's diff either way).
|
|
10
|
+
|
|
11
|
+
At FAMILY_CRITERION (D152) the same fields are a folded row's: the
|
|
12
|
+
criterion every member shares, read off the first, and each change replaces
|
|
13
|
+
EVERY member's leaf — its own variable kept — as one whole-tree op
|
|
14
|
+
(`family_rows.as_one`), which the Session collapses the same way.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import copy
|
|
20
|
+
import dataclasses
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
from ...comparison.mode_schema import Schema, derive_schema
|
|
24
|
+
from ...comparison.registry import metric, metrics
|
|
25
|
+
from ...comparison.seeds import seeded_leaf
|
|
26
|
+
from ...comparison.tree_spec import (
|
|
27
|
+
LeafSpec,
|
|
28
|
+
parse_metric_tree,
|
|
29
|
+
reads_a_statistic,
|
|
30
|
+
spec_to_raw,
|
|
31
|
+
spec_to_view,
|
|
32
|
+
)
|
|
33
|
+
from ..capabilities import Panel, Patch, Placement
|
|
34
|
+
from ..family_rows import NOT_A_FOLD, as_one, edit_notes
|
|
35
|
+
from ..session import no_tree_error
|
|
36
|
+
from ..tree_pointer import node_or_none
|
|
37
|
+
from ..unscorable import (
|
|
38
|
+
MIN_WIDTH,
|
|
39
|
+
TOLERANCES,
|
|
40
|
+
WIDTHS,
|
|
41
|
+
refuse_bounds,
|
|
42
|
+
refuse_negative,
|
|
43
|
+
refuse_negative_floor,
|
|
44
|
+
refuse_negative_width,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from ..session import SessionView
|
|
49
|
+
from ..spec_document import PatchOp
|
|
50
|
+
from .edit_on_plot import _replace, placeable, placed
|
|
51
|
+
from .edit_tree import seed_data
|
|
52
|
+
|
|
53
|
+
# NO module-level `nicegui` or `..forms` import: `import dstf.authoring` runs
|
|
54
|
+
# register_builtins(), and the Session must import without the extra.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _leaf_node(view: dict | None, path: str) -> dict | None:
|
|
58
|
+
"""The node at ``path`` in the report's tree view, or None."""
|
|
59
|
+
if view is None:
|
|
60
|
+
return None
|
|
61
|
+
if view.get("path") == path:
|
|
62
|
+
return view
|
|
63
|
+
for child in view.get("children") or []:
|
|
64
|
+
found = _leaf_node(child, path)
|
|
65
|
+
if found is not None:
|
|
66
|
+
return found
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _authored_leaf(tree: dict | None, path: str) -> dict | None:
|
|
71
|
+
"""The leaf at ``path`` of the tree as AUTHORED (`SessionView.tree`,
|
|
72
|
+
staged edits included), shaped as the tree view's leaf — its metric,
|
|
73
|
+
variable, params, baseline and window, and ``advisory`` when a ``warn``
|
|
74
|
+
sits above it; None where ``path`` names no leaf. Not the view's: a
|
|
75
|
+
test with nothing compared (no baseline) has no view, and the editor
|
|
76
|
+
of the criterion just staged there drew no field."""
|
|
77
|
+
raw = node_or_none(tree, path)
|
|
78
|
+
if raw is None or "metric" not in raw:
|
|
79
|
+
return None
|
|
80
|
+
leaf = spec_to_view(parse_metric_tree(raw), root=path)
|
|
81
|
+
parts = path.split("/children/")
|
|
82
|
+
leaf["advisory"] = any(
|
|
83
|
+
(node_or_none(tree, "/children/".join(parts[:i])) or {}).get("combinator")
|
|
84
|
+
== "warn"
|
|
85
|
+
for i in range(1, len(parts))
|
|
86
|
+
)
|
|
87
|
+
return leaf
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def refuse_unscorable(name: str, params: dict) -> None:
|
|
91
|
+
"""Raise ``ValueError`` for a criterion of metric ``name`` whose
|
|
92
|
+
``params`` can never score (`..unscorable`): a negative tolerance, its
|
|
93
|
+
sides named as the mode's own fields name them, a tube's width whose
|
|
94
|
+
sides sum below zero over no positive minimum width, or bounds out of
|
|
95
|
+
order — and a tube's minimum width below zero, which floors nothing."""
|
|
96
|
+
desc = metric(name)
|
|
97
|
+
sides: dict[str, Any] = {}
|
|
98
|
+
if desc is not None and dataclasses.is_dataclass(desc.config_cls):
|
|
99
|
+
sides = {
|
|
100
|
+
f.name: f.metadata.get("side_labels")
|
|
101
|
+
for f in dataclasses.fields(desc.config_cls)
|
|
102
|
+
}
|
|
103
|
+
for key in TOLERANCES:
|
|
104
|
+
refuse_negative(key, params.get(key), sides.get(key))
|
|
105
|
+
for key in WIDTHS:
|
|
106
|
+
refuse_negative_width(key, params.get(key), params.get(MIN_WIDTH))
|
|
107
|
+
refuse_negative_floor(MIN_WIDTH, params.get(MIN_WIDTH))
|
|
108
|
+
refuse_bounds(name, params)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class EditCriteria:
|
|
112
|
+
"""One leaf's metric, its mode's parameters, its window and the baseline
|
|
113
|
+
it scores against — or, for a folded row, every member's at once."""
|
|
114
|
+
|
|
115
|
+
name = "edit-criteria"
|
|
116
|
+
label = "Edit criteria"
|
|
117
|
+
placements = frozenset(
|
|
118
|
+
{
|
|
119
|
+
Placement.CRITERION,
|
|
120
|
+
Placement.STATISTIC_CRITERION,
|
|
121
|
+
Placement.TREE_CRITERION,
|
|
122
|
+
Placement.FAMILY_CRITERION,
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def caption(self, s: SessionView) -> str:
|
|
127
|
+
return self.label
|
|
128
|
+
|
|
129
|
+
def available(self, s: SessionView) -> bool | str:
|
|
130
|
+
sel = s.selection
|
|
131
|
+
model_id = next(iter(sel.tests)) if len(sel.tests) == 1 else None
|
|
132
|
+
if model_id is None or sel.leaf_path is None:
|
|
133
|
+
return "select one leaf"
|
|
134
|
+
# A compared test whose tree the Session will not materialize: its
|
|
135
|
+
# leaves are on the plots, but every edit is refused — the Session's
|
|
136
|
+
# reason, as the editor's why-line, not the view's leaf as if
|
|
137
|
+
# editable. Said here and not in the panel: Edit on plot is refused
|
|
138
|
+
# for the same reason, and the host says a sentence once.
|
|
139
|
+
_tree, why = s.tree_or_reason(model_id)
|
|
140
|
+
return True if why is None else why
|
|
141
|
+
|
|
142
|
+
def panel(self, s: SessionView, ui_panel: Panel) -> None:
|
|
143
|
+
from nicegui import ui
|
|
144
|
+
|
|
145
|
+
from ..forms import BOTTOM, SidesControl, render_form
|
|
146
|
+
from ..typed_number import TypedNumber
|
|
147
|
+
|
|
148
|
+
sel = s.selection
|
|
149
|
+
model_id = next(iter(sel.tests))
|
|
150
|
+
path = sel.leaf_path or ""
|
|
151
|
+
tree = s.tree(model_id)
|
|
152
|
+
assert tree is not None # `available` said so, on this same view
|
|
153
|
+
leaf = _authored_leaf(tree, path)
|
|
154
|
+
# The panel is rebuilt after edits made elsewhere: Edit tree wrapping
|
|
155
|
+
# the selected leaf leaves the selection's path naming a combinator.
|
|
156
|
+
if leaf is None:
|
|
157
|
+
ui.label("this leaf is not in the scored tree")
|
|
158
|
+
return
|
|
159
|
+
#: A folded row's members as they are read now: the act refuses
|
|
160
|
+
#: the edit if a path no longer holds that member (`act`).
|
|
161
|
+
members = tuple(
|
|
162
|
+
str((node_or_none(tree, member) or {}).get("variable", ""))
|
|
163
|
+
for member in sel.leaf_paths
|
|
164
|
+
)
|
|
165
|
+
# What the leaf was scored with: only a view says — none before a
|
|
166
|
+
# compare (no baseline), and then the fields show no hint.
|
|
167
|
+
scored = _leaf_node(s.tree_view(model_id), path) or {}
|
|
168
|
+
state: dict[str, Any] = {
|
|
169
|
+
"metric": leaf["metric"],
|
|
170
|
+
"variable": leaf["variable"],
|
|
171
|
+
"params": dict(leaf.get("params") or {}),
|
|
172
|
+
"against": leaf.get("against") or "primary",
|
|
173
|
+
"window": dict(leaf.get("window") or {}),
|
|
174
|
+
}
|
|
175
|
+
# A statistics criterion (U3): only `scalar` scores a stored number
|
|
176
|
+
# (no mode select, and `set_metric`'s reseed never runs on one), a
|
|
177
|
+
# stored number has no window, and the Statistics row the editor
|
|
178
|
+
# sits under names the quantity — none of the three is a field.
|
|
179
|
+
statistic = reads_a_statistic(state["metric"], state["params"])
|
|
180
|
+
# No heading: the criterion's own row sits directly above this panel
|
|
181
|
+
# and names it, and a second name there made the editor read as two
|
|
182
|
+
# panels rather than one (brief §3.2).
|
|
183
|
+
# Nor an error line: a leaf the scorer could not score says why in
|
|
184
|
+
# the criteria ROW directly above this panel, which reads out the
|
|
185
|
+
# criterion ("scalar leaf not scored — …"). Drawing it on the form
|
|
186
|
+
# itself (§4.7 "inline on its form") is sub-project 4's.
|
|
187
|
+
|
|
188
|
+
# What the leaf holds — the state its last accepted submit was made
|
|
189
|
+
# from — and what that was when the field being edited was ENTERED
|
|
190
|
+
# (`enter`): where a refused edit puts the leaf back.
|
|
191
|
+
taken: dict[str, Any] = {
|
|
192
|
+
"leaf": copy.deepcopy(state),
|
|
193
|
+
"entered": copy.deepcopy(state),
|
|
194
|
+
"field": None,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def enter(field: str | None) -> None:
|
|
198
|
+
"""A field took the focus (``None``: the row heard it, whichever
|
|
199
|
+
it is) — or ``field`` is the first edited since another was,
|
|
200
|
+
where no focus was heard (a value set from Python)."""
|
|
201
|
+
taken["field"], taken["entered"] = field, copy.deepcopy(taken["leaf"])
|
|
202
|
+
|
|
203
|
+
def submit(values: dict) -> bool:
|
|
204
|
+
# A folded row's members (D152): the act replaces each of them,
|
|
205
|
+
# each named as this panel read it.
|
|
206
|
+
if not ui_panel.submit(
|
|
207
|
+
model_id=model_id,
|
|
208
|
+
path=path,
|
|
209
|
+
paths=sel.leaf_paths,
|
|
210
|
+
variables=members,
|
|
211
|
+
**values,
|
|
212
|
+
):
|
|
213
|
+
return False
|
|
214
|
+
taken["leaf"] = copy.deepcopy(values)
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def emit(field: str) -> None:
|
|
218
|
+
"""Stage the leaf as the fields now say, ``field`` being the
|
|
219
|
+
one just edited. A value typed passes through others on the
|
|
220
|
+
way — `-0.5` over a window end is `-` first, no number, which
|
|
221
|
+
UNSETS the end — and each that the leaf takes is staged; so a
|
|
222
|
+
refused one left the last of those staged, beside a field and
|
|
223
|
+
an error line saying something else. Refused, the leaf goes
|
|
224
|
+
back to what it held when the field was entered: one staged
|
|
225
|
+
state, as if it had not been touched. The field keeps what was
|
|
226
|
+
typed, and the refusal its sentence."""
|
|
227
|
+
if field != taken["field"]:
|
|
228
|
+
enter(field)
|
|
229
|
+
if submit(state) or taken["leaf"] == taken["entered"]:
|
|
230
|
+
return
|
|
231
|
+
submit(taken["entered"])
|
|
232
|
+
# The put-back went through, which hid the refusal: said again,
|
|
233
|
+
# by the submit that earned it.
|
|
234
|
+
submit(state)
|
|
235
|
+
|
|
236
|
+
# A tolerance with one side typed is in no leaf: its control hands
|
|
237
|
+
# nothing over, so the fields show what the spec does not hold. This
|
|
238
|
+
# panel is rebuilt by edits it did not make — a drag on the plot,
|
|
239
|
+
# a cell of the table beside it — and, built from the leaf alone,
|
|
240
|
+
# the blank side came back as its old value, unmarked. So a half
|
|
241
|
+
# pair is written where it outlives the panel (`Panel.kept`), as
|
|
242
|
+
# the tables write theirs: field -> ``{"base": what the leaf holds
|
|
243
|
+
# there, "shown": the half pair}``, read back when the form is
|
|
244
|
+
# built. One whose leaf has since moved under it — that tolerance
|
|
245
|
+
# dragged on the plot — is dropped, and the fields show the spec's.
|
|
246
|
+
kept = ui_panel.kept
|
|
247
|
+
|
|
248
|
+
def remember(name: str, group: SidesControl) -> None:
|
|
249
|
+
"""After every edit of the two-sided field ``name``, once the
|
|
250
|
+
leaf has had what the control handed over."""
|
|
251
|
+
if group.half():
|
|
252
|
+
base = copy.deepcopy(taken["leaf"]["params"].get(name))
|
|
253
|
+
kept[name] = {"base": base, "shown": group.shown()}
|
|
254
|
+
else:
|
|
255
|
+
kept.pop(name, None)
|
|
256
|
+
|
|
257
|
+
#: The form as last built, and the window's two fields: what a
|
|
258
|
+
#: preview is shown in.
|
|
259
|
+
built: dict[str, Any] = {"form": None, "window": {}}
|
|
260
|
+
# A ⌖ only where a click on the plot can set the field as the
|
|
261
|
+
# criterion now is (`edit_on_plot.placeable`, the rule the click
|
|
262
|
+
# itself acts by) — and none beside no plot, or on a statistic.
|
|
263
|
+
can = placeable(s, model_id=model_id, variable=leaf["variable"], path=path)
|
|
264
|
+
|
|
265
|
+
def place(pointer: tuple, label: str, field: Any) -> None:
|
|
266
|
+
if ui_panel.can_place and not statistic and can(pointer):
|
|
267
|
+
with field.add_slot("append"):
|
|
268
|
+
ui_panel.place_button(pointer, label)
|
|
269
|
+
|
|
270
|
+
def build_form() -> None:
|
|
271
|
+
form_slot.clear()
|
|
272
|
+
desc = metric(state["metric"])
|
|
273
|
+
with form_slot:
|
|
274
|
+
if desc is None:
|
|
275
|
+
ui.label(f"unknown metric {state['metric']!r}")
|
|
276
|
+
return
|
|
277
|
+
schema = derive_schema(desc.config_cls, mode=desc.name)
|
|
278
|
+
if statistic:
|
|
279
|
+
schema = Schema(
|
|
280
|
+
schema.mode, [f for f in schema.fields if f.name != "quantity"]
|
|
281
|
+
)
|
|
282
|
+
for name in [
|
|
283
|
+
n for n in kept if kept[n]["base"] != state["params"].get(n)
|
|
284
|
+
]:
|
|
285
|
+
del kept[name] # the leaf moved under it
|
|
286
|
+
built["form"] = render_form(
|
|
287
|
+
schema,
|
|
288
|
+
state["params"],
|
|
289
|
+
on_change=set_params,
|
|
290
|
+
classes="dstf-field",
|
|
291
|
+
shown={name: cell["shown"] for name, cell in kept.items()},
|
|
292
|
+
on_edit=remember,
|
|
293
|
+
place=place,
|
|
294
|
+
# The mode the leaf was scored in: a switched mode's
|
|
295
|
+
# fields were never applied, so nothing is in effect.
|
|
296
|
+
in_effect=(
|
|
297
|
+
scored.get("in_effect")
|
|
298
|
+
if state["metric"] == scored.get("metric")
|
|
299
|
+
else None
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def set_params(values: dict) -> None:
|
|
304
|
+
if statistic:
|
|
305
|
+
# The form's values ARE the params: with its quantity field
|
|
306
|
+
# left out, the path goes back in, or the replace staged a
|
|
307
|
+
# scalar that reads no statistic — which the Session takes.
|
|
308
|
+
values = {**values, "quantity": state["params"]["quantity"]}
|
|
309
|
+
changed = sorted(
|
|
310
|
+
key
|
|
311
|
+
for key in {*values, *state["params"]}
|
|
312
|
+
if values.get(key) != state["params"].get(key)
|
|
313
|
+
)
|
|
314
|
+
state["params"] = values
|
|
315
|
+
emit("params:" + ",".join(changed))
|
|
316
|
+
|
|
317
|
+
def set_metric(name: str) -> None:
|
|
318
|
+
ui_panel.disarm() # another mode's fields are other fields
|
|
319
|
+
state["metric"] = name
|
|
320
|
+
# A new mode starts from its seed, else its defaults: a range
|
|
321
|
+
# switched to with no bound does not resolve. The window stays,
|
|
322
|
+
# so the seed reads the samples it scores.
|
|
323
|
+
data = seed_data(s, model_id, state["variable"], window=state["window"])
|
|
324
|
+
seeded = seeded_leaf(name, state["variable"], data)
|
|
325
|
+
state["params"] = {
|
|
326
|
+
k: v for k, v in seeded.items() if k not in ("metric", "variable")
|
|
327
|
+
}
|
|
328
|
+
# Another mode's fields are other fields, whatever their names:
|
|
329
|
+
# a half pair typed in the old one is not shown in them.
|
|
330
|
+
kept.clear()
|
|
331
|
+
build_form()
|
|
332
|
+
emit("metric")
|
|
333
|
+
|
|
334
|
+
def set_window(key: str, value: float | None) -> None:
|
|
335
|
+
if value is None:
|
|
336
|
+
state["window"].pop(key, None)
|
|
337
|
+
else:
|
|
338
|
+
state["window"][key] = value
|
|
339
|
+
emit(f"window:{key}")
|
|
340
|
+
|
|
341
|
+
def set_against(name: str) -> None:
|
|
342
|
+
state["against"] = name
|
|
343
|
+
emit("against")
|
|
344
|
+
|
|
345
|
+
project = s.project
|
|
346
|
+
names = list(project.baseline_names(model_id)) if project is not None else []
|
|
347
|
+
if not leaf.get("advisory"):
|
|
348
|
+
# D66: a soft_check may be targeted only under a `warn`; offering
|
|
349
|
+
# it here would stage an op the validator refuses every time.
|
|
350
|
+
names = [n for n in names if n == "primary"]
|
|
351
|
+
if state["against"] not in names:
|
|
352
|
+
# A leaf naming a baseline the store no longer has is exactly the
|
|
353
|
+
# leaf one opens this panel to repair: keep its value selectable.
|
|
354
|
+
names.append(state["against"])
|
|
355
|
+
# ONE wrapping row (the board: the plot and its editor fit a screen
|
|
356
|
+
# together), the mode FIRST — it decides what the fields after it
|
|
357
|
+
# are, and drawn below them it read as a footnote to the form it
|
|
358
|
+
# controls. `dstf-form` hands the mode's own fields straight to this
|
|
359
|
+
# row rather than stacking them in a column of its own.
|
|
360
|
+
fields = ui.row().classes("dstf-fields items-end gap-3 wrap w-full")
|
|
361
|
+
# Whichever field it is: what the leaf holds now is its start.
|
|
362
|
+
fields.on("focusin", lambda _e: enter(None), []).mark("edit-criteria:fields")
|
|
363
|
+
with fields:
|
|
364
|
+
if not statistic:
|
|
365
|
+
ui.select(
|
|
366
|
+
[d.name for d in metrics()],
|
|
367
|
+
label="mode",
|
|
368
|
+
value=state["metric"],
|
|
369
|
+
on_change=lambda e: set_metric(e.value),
|
|
370
|
+
).props(f"dense {BOTTOM}").classes("dstf-field")
|
|
371
|
+
form_slot = ui.column().classes("dstf-form")
|
|
372
|
+
if not statistic:
|
|
373
|
+
for end in ("start", "end"):
|
|
374
|
+
field = (
|
|
375
|
+
TypedNumber(
|
|
376
|
+
label=f"window {end}",
|
|
377
|
+
value=state["window"].get(end),
|
|
378
|
+
on_change=lambda e, end=end: set_window(end, e.value),
|
|
379
|
+
)
|
|
380
|
+
.props(f"dense {BOTTOM}")
|
|
381
|
+
.classes("dstf-field")
|
|
382
|
+
)
|
|
383
|
+
place(("window", end), f"window {end}", field)
|
|
384
|
+
built["window"][end] = field
|
|
385
|
+
ui.select(
|
|
386
|
+
names or ["primary"],
|
|
387
|
+
label="against",
|
|
388
|
+
value=state["against"],
|
|
389
|
+
on_change=lambda e: set_against(e.value),
|
|
390
|
+
).props(f"dense {BOTTOM}").classes("dstf-field")
|
|
391
|
+
build_form()
|
|
392
|
+
|
|
393
|
+
def preview(shown: dict | None, revision: int) -> None:
|
|
394
|
+
"""A handle held on the plot (D149): the fields its release
|
|
395
|
+
would change show what it would write, and nothing is staged."""
|
|
396
|
+
would = None
|
|
397
|
+
if shown is not None:
|
|
398
|
+
try:
|
|
399
|
+
would = spec_to_view(parse_metric_tree(shown), root=path)
|
|
400
|
+
except ValueError:
|
|
401
|
+
would = None # no leaf: nothing to show
|
|
402
|
+
form = built["form"]
|
|
403
|
+
if form is not None:
|
|
404
|
+
form.preview(None if would is None else would["params"], revision)
|
|
405
|
+
for end, field in built["window"].items():
|
|
406
|
+
value = None if would is None else (would.get("window") or {}).get(end)
|
|
407
|
+
held = state["window"].get(end)
|
|
408
|
+
field.preview(None if value == held else value, revision)
|
|
409
|
+
|
|
410
|
+
ui_panel.on_preview(preview)
|
|
411
|
+
variable = state["variable"]
|
|
412
|
+
# An armed field of THIS panel, and the plot clicked: the click is
|
|
413
|
+
# read as Edit on plot reads a drag — this panel's fields are the
|
|
414
|
+
# same leaf's — and staged from here, so it is this panel's edit.
|
|
415
|
+
ui_panel.on_place(
|
|
416
|
+
lambda pointer, click: ui_panel.submit(
|
|
417
|
+
kind="place",
|
|
418
|
+
model_id=model_id,
|
|
419
|
+
variable=variable,
|
|
420
|
+
path=path,
|
|
421
|
+
pointer=pointer,
|
|
422
|
+
click=click,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def act(self, s: SessionView, **inputs: Any) -> Patch | None:
|
|
427
|
+
if inputs.get("kind") == "place":
|
|
428
|
+
new = placed(
|
|
429
|
+
s,
|
|
430
|
+
model_id=inputs["model_id"],
|
|
431
|
+
variable=inputs["variable"],
|
|
432
|
+
path=inputs["path"],
|
|
433
|
+
pointer=inputs["pointer"],
|
|
434
|
+
click=inputs["click"],
|
|
435
|
+
)
|
|
436
|
+
return _replace(inputs["model_id"], inputs["path"], new)
|
|
437
|
+
params = dict(inputs["params"])
|
|
438
|
+
# A value that can never score is refused here, before the stage —
|
|
439
|
+
# as the stage refuses a window that ends before it starts, and the
|
|
440
|
+
# panel puts the leaf back the same way (`emit`).
|
|
441
|
+
refuse_unscorable(inputs["metric"], params)
|
|
442
|
+
leaf = LeafSpec(
|
|
443
|
+
metric=inputs["metric"],
|
|
444
|
+
variable=inputs["variable"],
|
|
445
|
+
params=params,
|
|
446
|
+
against=inputs["against"],
|
|
447
|
+
window_start=inputs["window"].get("start"),
|
|
448
|
+
window_end=inputs["window"].get("end"),
|
|
449
|
+
)
|
|
450
|
+
model_id = inputs["model_id"]
|
|
451
|
+
paths = tuple(inputs.get("paths") or ())
|
|
452
|
+
if not paths:
|
|
453
|
+
return Patch(
|
|
454
|
+
ops=[PatchOp(model_id, "replace", inputs["path"], spec_to_raw(leaf))]
|
|
455
|
+
)
|
|
456
|
+
# A folded row (D152): the same criterion on every member, each on
|
|
457
|
+
# its own variable — ONE op, refused or staged whole.
|
|
458
|
+
tree, why = s.tree_or_reason(model_id)
|
|
459
|
+
if tree is None:
|
|
460
|
+
assert why is not None # `tree` is None
|
|
461
|
+
raise no_tree_error(model_id, why)
|
|
462
|
+
# Each path has to hold the member it was read as: an edit staged
|
|
463
|
+
# elsewhere moves the tree under an open row's paths, and "every
|
|
464
|
+
# member" then overwrote whatever criterion stood there instead —
|
|
465
|
+
# another variable's (the executing review's M4).
|
|
466
|
+
variables = tuple(inputs.get("variables") or ())
|
|
467
|
+
if len(variables) != len(paths):
|
|
468
|
+
raise ValueError(NOT_A_FOLD)
|
|
469
|
+
ops, was = [], None
|
|
470
|
+
for member, expected in zip(paths, variables, strict=True):
|
|
471
|
+
node = node_or_none(tree, member)
|
|
472
|
+
if node is None or "metric" not in node or node["variable"] != expected:
|
|
473
|
+
raise ValueError(NOT_A_FOLD)
|
|
474
|
+
was = node if was is None else was
|
|
475
|
+
raw = spec_to_raw(dataclasses.replace(leaf, variable=expected))
|
|
476
|
+
ops.append(PatchOp(model_id, "replace", member, raw))
|
|
477
|
+
assert was is not None # `paths` is not empty
|
|
478
|
+
notes = edit_notes(was, ops[0].value, variables)
|
|
479
|
+
return Patch(ops=[as_one(model_id, tree, ops, notes)])
|