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,33 @@
|
|
|
1
|
+
"""A pointer into a raw metrics tree (``/metrics``, ``/metrics/children/0``,
|
|
2
|
+
...), resolved to the node it names. UI-free: the builtins read a node
|
|
3
|
+
through it to build their ops, and the Test page reads one to tell whether
|
|
4
|
+
a form row's node moved and whether a criterion it is asked to reveal
|
|
5
|
+
exists. The shell imports no builtin, so the one reader lives here."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def node_at(tree: dict, path: str) -> dict:
|
|
13
|
+
"""The raw node at ``path``, a pointer whose first token names the tree
|
|
14
|
+
itself (``/metrics``, ``/metrics/children/0``, ...)."""
|
|
15
|
+
node: Any = tree
|
|
16
|
+
for token in [t for t in path.split("/") if t][1:]:
|
|
17
|
+
# A pointer alternates `children` (a key) and an index (into that
|
|
18
|
+
# list): consume each token as what it is.
|
|
19
|
+
node = node[int(token)] if token.isdigit() else node[token]
|
|
20
|
+
return node
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def node_or_none(tree: dict | None, path: str) -> dict | None:
|
|
24
|
+
"""The node at ``path``, or ``None`` when the pointer names none of
|
|
25
|
+
``tree`` — a stale selection, or one naming a raw key (``/metrics/
|
|
26
|
+
combinator`` resolves to a word, which is no node)."""
|
|
27
|
+
if tree is None:
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
node = node_at(tree, path)
|
|
31
|
+
except (KeyError, IndexError, TypeError, ValueError):
|
|
32
|
+
return None
|
|
33
|
+
return node if isinstance(node, dict) else None
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""What a tree node READS as. UI-free, so the Test page's rows and the
|
|
2
|
+
Edit tree capability's form titles say the same thing: a form's title
|
|
3
|
+
names its node the way the node's row does. Every function takes a
|
|
4
|
+
`tree_view` node (``kind``, ``label``, ``path``), never a raw spec node."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from ..reporting.tree_rows import leaves_of
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def nodes_of(view: dict | None) -> list[dict]:
|
|
12
|
+
"""Every node of a `tree_view`, combinators included, in tree order."""
|
|
13
|
+
if view is None:
|
|
14
|
+
return []
|
|
15
|
+
return [view] + [
|
|
16
|
+
node for child in view.get("children") or [] for node in nodes_of(child)
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def leaf_name(leaf: dict) -> str:
|
|
21
|
+
"""``metric · label`` — what a criterion's row READS. The label is the
|
|
22
|
+
expression the model wrote for the variable (`nrmse · pipe.T[1]`), which
|
|
23
|
+
the tree view carries beside the `variable` a patch op targets; a
|
|
24
|
+
variable with no expression reads as its name, as it always did."""
|
|
25
|
+
return f"{leaf.get('metric')} · {leaf.get('label') or leaf.get('variable')}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def node_text(node: dict) -> str:
|
|
29
|
+
if node.get("kind") == "leaf":
|
|
30
|
+
return leaf_name(node)
|
|
31
|
+
n = len(leaves_of(node))
|
|
32
|
+
text = f"{node.get('combinator')} · {n} {'criterion' if n == 1 else 'criteria'}"
|
|
33
|
+
if node.get("combinator") == "k-of-n" and node.get("k") is not None:
|
|
34
|
+
text += f" · k={node['k']}"
|
|
35
|
+
if node.get("combinator") == "weighted" and node.get("threshold") is not None:
|
|
36
|
+
text += f" · threshold {node['threshold']:g}"
|
|
37
|
+
return text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def count_text(node: dict) -> str:
|
|
41
|
+
"""A combinator's row text while its kind is read from the dropdown
|
|
42
|
+
beside it: `node_text` without the kind — ``3 criteria``,
|
|
43
|
+
``k=2 · 3 criteria``, ``threshold 0.5 · 3 criteria``."""
|
|
44
|
+
n = len(leaves_of(node))
|
|
45
|
+
parts = [f"{n} {'criterion' if n == 1 else 'criteria'}"]
|
|
46
|
+
if node.get("combinator") == "k-of-n" and node.get("k") is not None:
|
|
47
|
+
parts.insert(0, f"k={node['k']}")
|
|
48
|
+
if node.get("combinator") == "weighted" and node.get("threshold") is not None:
|
|
49
|
+
parts.insert(0, f"threshold {node['threshold']:g}")
|
|
50
|
+
return " · ".join(parts)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parent_kinds(view: dict | None) -> dict[str, str | None]:
|
|
54
|
+
"""Each node's path -> its parent's combinator (None for the root):
|
|
55
|
+
what a leaf's `+` reads, since the parent takes the child it adds."""
|
|
56
|
+
kinds: dict[str, str | None] = {}
|
|
57
|
+
for node in nodes_of(view):
|
|
58
|
+
kinds.setdefault(str(node.get("path")), None)
|
|
59
|
+
for child in node.get("children") or []:
|
|
60
|
+
kinds[str(child.get("path"))] = node.get("combinator")
|
|
61
|
+
return kinds
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// A number field the server cannot overwrite while it is typed in: what the
|
|
2
|
+
// field shows is what was typed, until it loses focus; a server value is
|
|
3
|
+
// taken only while the field is not being typed in. (The passthrough is
|
|
4
|
+
// NiceGUI's own, elements/input.js.)
|
|
5
|
+
//
|
|
6
|
+
// And a PREVIEW (D149): a value the server shows in the field in place of
|
|
7
|
+
// what it holds, staging nothing — a handle held on the plot, the field
|
|
8
|
+
// following it. `previewed` is that value (null: none) and `revision` the
|
|
9
|
+
// number of the drag it is of. It is shown whatever the field's typing
|
|
10
|
+
// state, except in a field typed in SINCE that drag began: what a person
|
|
11
|
+
// types is never painted over.
|
|
12
|
+
//
|
|
13
|
+
// The server sets it by calling `preview(value, revision)` on this
|
|
14
|
+
// component — never by a prop. A prop write resends the element, and
|
|
15
|
+
// NiceGUI renders the page's whole tree again for any element it is sent:
|
|
16
|
+
// with a 30-row table open that was a 50 to 77 ms task in the browser for
|
|
17
|
+
// every move of a held handle. A method call changes this component's own
|
|
18
|
+
// state, and it alone is drawn again.
|
|
19
|
+
export default {
|
|
20
|
+
template: `
|
|
21
|
+
<q-input ref="qRef" v-bind="$attrs" :model-value="display" @update:model-value="typed" @blur="typing = false" @wheel="wheel">
|
|
22
|
+
<template v-for="(_, slot) in $slots" v-slot:[slot]="slotProps">
|
|
23
|
+
<slot :name="slot" v-bind="slotProps || {}" />
|
|
24
|
+
</template>
|
|
25
|
+
</q-input>
|
|
26
|
+
`,
|
|
27
|
+
inheritAttrs: false,
|
|
28
|
+
props: { modelValue: null },
|
|
29
|
+
emits: ["update:modelValue"],
|
|
30
|
+
data() {
|
|
31
|
+
return { shown: this.modelValue, typing: false, typedIn: null, previewed: null, revision: 0 };
|
|
32
|
+
},
|
|
33
|
+
computed: {
|
|
34
|
+
display() {
|
|
35
|
+
const previewing = this.previewed !== null && this.previewed !== undefined;
|
|
36
|
+
return previewing && this.typedIn !== this.revision ? this.previewed : this.shown;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
watch: {
|
|
40
|
+
modelValue(value) {
|
|
41
|
+
if (!this.typing) this.shown = value;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
methods: {
|
|
45
|
+
preview(value, revision) {
|
|
46
|
+
this.previewed = value;
|
|
47
|
+
this.revision = revision;
|
|
48
|
+
},
|
|
49
|
+
typed(value) {
|
|
50
|
+
this.typing = true;
|
|
51
|
+
this.typedIn = this.revision;
|
|
52
|
+
this.shown = value;
|
|
53
|
+
this.$emit("update:modelValue", value);
|
|
54
|
+
},
|
|
55
|
+
// A focused `type=number` input steps by 1 per wheel notch and keeps the
|
|
56
|
+
// page from scrolling: scrolling on with the pointer still over the
|
|
57
|
+
// field just typed in turned 0.3 into 3.3, staged. Left before the
|
|
58
|
+
// browser acts on the wheel, the input steps nothing and the page
|
|
59
|
+
// scrolls. (The arrow keys' step is untouched.)
|
|
60
|
+
wheel(event) {
|
|
61
|
+
if (event.target === document.activeElement) event.target.blur();
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""The tool's number field: one that keeps what is typed in it.
|
|
2
|
+
|
|
3
|
+
NiceGUI resends a whole element — its ``model-value`` with it — on any write
|
|
4
|
+
of one of its props (a hint, a validation mark, ``update()``), and leaves the
|
|
5
|
+
value out only when it equals the raw string the browser last sent
|
|
6
|
+
(``"0.0" != "0"``, ``None != ""``). A stage that rescores takes longer than
|
|
7
|
+
the gap between two keys, so the answer to one keystroke landed under the
|
|
8
|
+
keys that followed: `0.25` typed over 0.3 read — and staged — `0.025`.
|
|
9
|
+
|
|
10
|
+
Imports ``nicegui`` at module level, as ``forms`` does: neither is on the
|
|
11
|
+
path of ``import dstf.authoring``, and a capability imports it inside the
|
|
12
|
+
function that draws.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from nicegui import ui
|
|
20
|
+
from nicegui.events import GenericEventArguments
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TypedNumber(ui.number, component="typed_number.js"):
|
|
24
|
+
"""``ui.number`` whose field shows what was typed until it is left: a
|
|
25
|
+
value from the server is taken only while the field is not being typed
|
|
26
|
+
in (``typed_number.js``). Every number field of the tool is one. The
|
|
27
|
+
mouse wheel over a focused one scrolls the page and steps nothing: the
|
|
28
|
+
field is left first (a browser steps a focused number input by 1 per
|
|
29
|
+
notch).
|
|
30
|
+
|
|
31
|
+
A field's own VALUE is still never set from the server while it is
|
|
32
|
+
typed in: Discard and a released drag rebuild the editor, and a linked
|
|
33
|
+
twin's mirror lands in the field that is not typed in. What the server
|
|
34
|
+
does show in a field that may hold the focus is a PREVIEW
|
|
35
|
+
(:meth:`preview`, D149): beside the value, never in its place.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
# `ui.number` names `q-input` itself, over the registered component.
|
|
41
|
+
component = type(self).component
|
|
42
|
+
assert component is not None # the class statement registered it
|
|
43
|
+
self.tag = component.tag
|
|
44
|
+
#: The preview the browser was last told to show, and of which drag.
|
|
45
|
+
self.previewed: float | None = None
|
|
46
|
+
self.preview_revision = 0
|
|
47
|
+
|
|
48
|
+
def preview(self, value: float | None, revision: int = 0) -> None:
|
|
49
|
+
"""Show ``value`` in the field in place of what it holds — a handle
|
|
50
|
+
held on the plot, the field following it (D149) — or, with None,
|
|
51
|
+
what it holds again. The field's value does not change, so nothing
|
|
52
|
+
is handed to ``on_change`` and nothing is staged. ``revision`` is
|
|
53
|
+
the number of the drag the preview is of: the component shows a
|
|
54
|
+
preview whatever its typing state, but not in a field typed in
|
|
55
|
+
since THAT drag began (``typed_number.js``).
|
|
56
|
+
|
|
57
|
+
A METHOD CALL on the component, not a prop: a prop write resends
|
|
58
|
+
the element, and the browser then renders the page's whole tree
|
|
59
|
+
again — a long task per move of the handle on an editor of thirty
|
|
60
|
+
rows. ``previewed`` and ``preview_revision`` are what was last
|
|
61
|
+
sent."""
|
|
62
|
+
same = self.previewed == value
|
|
63
|
+
if same and (value is None or self.preview_revision == revision):
|
|
64
|
+
return
|
|
65
|
+
self.previewed, self.preview_revision = value, revision
|
|
66
|
+
self.run_method("preview", value, revision)
|
|
67
|
+
|
|
68
|
+
def _event_args_to_value(self, e: GenericEventArguments) -> float | None:
|
|
69
|
+
"""What was typed, as the number it is — and a negative zero as
|
|
70
|
+
zero. `-0` is on the way to every negative number and is what a
|
|
71
|
+
refused `-0.5` backspaces to; a float of it is −0.0, which was
|
|
72
|
+
staged as it came and written `-0.0` by Save. Here, the one place a
|
|
73
|
+
typed text becomes a number for every field of the tool: a table's
|
|
74
|
+
cell and a tolerance's side take their value from this."""
|
|
75
|
+
value = super()._event_args_to_value(e)
|
|
76
|
+
return 0.0 if value == 0 else value
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""What an editor refuses to stage because it can never score: a negative
|
|
2
|
+
tolerance — a number, or either side of a pair; a value's or a time's; the
|
|
3
|
+
criterion's or a table row's — a tube's constant width whose sides sum
|
|
4
|
+
below zero (a negative number, or a pair that leaves nothing between its
|
|
5
|
+
bounds) with no positive minimum width under it, and bounds out of order.
|
|
6
|
+
And one value that scores but says nothing: a tube's minimum width below
|
|
7
|
+
zero, a floor under no width.
|
|
8
|
+
|
|
9
|
+
The EDITOR's rule, raised from a capability's ``act`` before anything is
|
|
10
|
+
staged, the way a window that ends before it starts is refused. It is not
|
|
11
|
+
the scorer's and not the spec parser's: a spec written by hand that holds
|
|
12
|
+
such a value is read, staged and scored exactly as it always was."""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from typing import TypeGuard
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Refused(ValueError):
|
|
21
|
+
"""A value an editor will not stage, said as its reason. A person's own
|
|
22
|
+
— a negative tolerance on the way to a good one, bounds out of order,
|
|
23
|
+
a required cell left blank — so it is shown and never logged
|
|
24
|
+
(`errors.USER_ERRORS`): as a plain `ValueError`, which a bug's also
|
|
25
|
+
is, each keystroke of one logged a traceback."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: The keys that hold a tolerance, on a criterion and in a row of its tables.
|
|
29
|
+
TOLERANCES = ("tolerance", "time_tolerance")
|
|
30
|
+
|
|
31
|
+
#: The keys that hold a tube's constant width (``TubeConfig``).
|
|
32
|
+
WIDTHS = ("tube_abs", "tube_rel")
|
|
33
|
+
|
|
34
|
+
#: The key that holds a tube's minimum width: ONE number, a floor under
|
|
35
|
+
#: both sides.
|
|
36
|
+
MIN_WIDTH = "tube_min_width"
|
|
37
|
+
|
|
38
|
+
#: The metrics whose two bounds may not be EQUAL either: the scorer refuses
|
|
39
|
+
#: a scalar leaf with ``min_value >= max_value`` when it is built
|
|
40
|
+
#: (``modes._validate_scalar_config``), as its drag editor does
|
|
41
|
+
#: (``handles.edit_scalar_handle``). A range with equal bounds scores.
|
|
42
|
+
STRICT_BOUNDS = frozenset({"scalar"})
|
|
43
|
+
|
|
44
|
+
_SIDES = ("lower", "upper")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_number(value: object) -> TypeGuard[int | float]:
|
|
48
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _num(value: float) -> str:
|
|
52
|
+
"""A number in a refusal, to 12 significant digits, its minus as U+2212
|
|
53
|
+
(the report's sign). The sign only: an exponent's ``-`` stays."""
|
|
54
|
+
text = f"{float(value):.12g}"
|
|
55
|
+
return f"−{text[1:]}" if text.startswith("-") else text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def refuse_negative(
|
|
59
|
+
key: str,
|
|
60
|
+
value: object,
|
|
61
|
+
side_names: tuple[str, str] | None = None,
|
|
62
|
+
where: str = "",
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Raise :class:`Refused` when the tolerance ``value`` held under ``key``
|
|
65
|
+
is negative: ``tolerance must not be negative; got −0.5`` for a number,
|
|
66
|
+
``tolerance (lower side) must not be negative; got −0.5`` for a side of
|
|
67
|
+
a pair — named as its fields are where they have words for it
|
|
68
|
+
(``side_names``: a time tolerance's ``early`` / ``late``), ``where``
|
|
69
|
+
before it (``"row 1: "``). A zero is a tolerance. Anything that is no
|
|
70
|
+
number or pair of numbers is not this rule's to refuse."""
|
|
71
|
+
if isinstance(value, dict):
|
|
72
|
+
named = dict(zip(_SIDES, side_names or _SIDES))
|
|
73
|
+
sides = [(f"{key} ({named[s]} side)", value.get(s)) for s in _SIDES]
|
|
74
|
+
else:
|
|
75
|
+
sides = [(key, value)]
|
|
76
|
+
for what, side in sides:
|
|
77
|
+
if _is_number(side) and side < 0:
|
|
78
|
+
raise Refused(f"{where}{what} must not be negative; got {_num(side)}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def refuse_negative_width(key: str, value: object, floor: object) -> None:
|
|
82
|
+
"""Raise :class:`Refused` when the tube width ``value`` held under ``key``
|
|
83
|
+
puts the tube's upper bound under its lower one, so that nothing is
|
|
84
|
+
ever inside it (it was staged, and read "0.0% inside tube"): a negative
|
|
85
|
+
NUMBER — both sides — ``tube_abs must not be negative; got −0.3``, and
|
|
86
|
+
a PAIR of numbers whose sides sum below zero, ``tube_abs sides must not
|
|
87
|
+
sum below zero; got lower −0.1, upper −0.5``. The sum, not either
|
|
88
|
+
side: a pair with one side negative and a sum above zero is a band
|
|
89
|
+
shifted off its reference, which a run can lie in, and one whose sides
|
|
90
|
+
sum to exactly zero is a band of no width — a zero is a tolerance.
|
|
91
|
+
|
|
92
|
+
Only where ``floor``, the leaf's minimum width, is no positive number.
|
|
93
|
+
A positive one widens BOTH sides to itself
|
|
94
|
+
(``algorithms._tube_bounds``), so no width over it leaves nothing
|
|
95
|
+
inside: ``{"lower": -0.1, "upper": -0.5}`` over a floor of 0.2 is a
|
|
96
|
+
tube 0.2 either side of its reference. Such a leaf, written by hand,
|
|
97
|
+
was refused every edit — a window end, and the Min width that would
|
|
98
|
+
repair one with no floor — with the width's sentence. A floor that is
|
|
99
|
+
absent, zero, negative or no number floors nothing, and the width is
|
|
100
|
+
refused as it was: the floor cleared from such a leaf too."""
|
|
101
|
+
if _is_number(floor) and floor > 0:
|
|
102
|
+
return
|
|
103
|
+
if _is_number(value):
|
|
104
|
+
refuse_negative(key, value)
|
|
105
|
+
return
|
|
106
|
+
if not isinstance(value, dict):
|
|
107
|
+
return
|
|
108
|
+
lower, upper = value.get("lower"), value.get("upper")
|
|
109
|
+
if _is_number(lower) and _is_number(upper) and lower + upper < 0:
|
|
110
|
+
raise Refused(
|
|
111
|
+
f"{key} sides must not sum below zero; "
|
|
112
|
+
f"got lower {_num(lower)}, upper {_num(upper)}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def refuse_negative_floor(key: str, value: object) -> None:
|
|
117
|
+
"""Raise :class:`Refused` when the tube's minimum width ``value`` held
|
|
118
|
+
under ``key`` is a negative number: ``tube_min_width must not be
|
|
119
|
+
negative; got −0.3``. A floor below zero is under every width: it
|
|
120
|
+
floors nothing, and the criterion scores as if the field were empty —
|
|
121
|
+
which is what the field should then be. Only a NUMBER is this rule's:
|
|
122
|
+
the floor takes one, and anything else is the leaf's config error,
|
|
123
|
+
said by its row (``comparison.sides.parse_number``)."""
|
|
124
|
+
if _is_number(value):
|
|
125
|
+
refuse_negative(key, value)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def refuse_bounds(metric: str, params: Mapping[str, object]) -> None:
|
|
129
|
+
"""Raise :class:`Refused` when ``params`` holds both bounds out of order:
|
|
130
|
+
``min_value 9 exceeds max_value 2`` — and, for a metric of
|
|
131
|
+
:data:`STRICT_BOUNDS`, when they are equal."""
|
|
132
|
+
lo, hi = params.get("min_value"), params.get("max_value")
|
|
133
|
+
if not _is_number(lo) or not _is_number(hi):
|
|
134
|
+
return
|
|
135
|
+
if lo > hi:
|
|
136
|
+
raise Refused(f"min_value {_num(lo)} exceeds max_value {_num(hi)}")
|
|
137
|
+
if lo == hi and metric in STRICT_BOUNDS:
|
|
138
|
+
raise Refused(
|
|
139
|
+
f"min_value {_num(lo)} equals max_value {_num(hi)}; "
|
|
140
|
+
f"a {metric} leaf needs min_value < max_value"
|
|
141
|
+
)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""The Variables drawer's rows (the roadmap's 3c; DESIGN.md "The Variables
|
|
2
|
+
drawer"), as data: the page draws them and knows none of the rules.
|
|
3
|
+
|
|
4
|
+
Every variable of the last result, in groups, with arrays folded into one
|
|
5
|
+
row the way the plots fold them (`discovery.plots.derive_plots`: by
|
|
6
|
+
RESULT-FILE NAME, a UnitTests element never), filtered, and each row's
|
|
7
|
+
state against the plots: in the selected plot's context,
|
|
8
|
+
its subject, some plot's subject. Three groupings (the
|
|
9
|
+
drawer's group menu): by component — the tracked ones first, then by the
|
|
10
|
+
first dot component of the NAME, inside any ``der(...)`` ("(top level)"
|
|
11
|
+
for a name with none), the diagnostics last; flat, one group; and tracked
|
|
12
|
+
first, the rest in one group after them. Two orders: result-file order, or
|
|
13
|
+
A→Z by what a row reads as. Three reads (the names menu): the expression,
|
|
14
|
+
the result-file name, or both."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable, Collection, Mapping, Sequence
|
|
19
|
+
from dataclasses import dataclass, replace
|
|
20
|
+
|
|
21
|
+
from ..discovery.plots import ResolvedPlot
|
|
22
|
+
from ..figures.families import family_label, group_families, mask_quoted
|
|
23
|
+
|
|
24
|
+
TOP_LEVEL = "(top level)"
|
|
25
|
+
#: The one group of the flat grouping, and the second of "tracked first".
|
|
26
|
+
ALL, OTHER = "All variables", "Other variables"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class VarRow:
|
|
31
|
+
#: the family key (``pipe.T[]``; a lone variable's too when it has one,
|
|
32
|
+
#: ``$DER.x[]``), else the name. Not unique: the tracked ``pipe.T[1:2]``
|
|
33
|
+
#: keys ``pipe.T[]`` as the untracked rest of the array does — ``id`` is.
|
|
34
|
+
key: str
|
|
35
|
+
#: ``<group rank>/<group title>/<key>``, unique within the drawer. The
|
|
36
|
+
#: rank is in it because a title is not unique: the tracked
|
|
37
|
+
#: ``Tracked.a[1:2]`` and the untracked ``Tracked.a[3:4]`` beside it are
|
|
38
|
+
#: both "Tracked" and both key ``Tracked.a[]``. A tracked family in a
|
|
39
|
+
#: group of untracked ones too (Flat) keys ``tracked:<key>``: under Flat
|
|
40
|
+
#: both ``pipe.T[]`` rows are in "All variables".
|
|
41
|
+
id: str
|
|
42
|
+
members: tuple[str, ...] #: names, by index; one for a lone variable
|
|
43
|
+
text: str #: what it reads as: the family label, or the label
|
|
44
|
+
#: what it reads as by result-file names: the family label
|
|
45
|
+
#: (``pipe.T[1:2]``), or the name
|
|
46
|
+
file_text: str
|
|
47
|
+
name: str | None #: the muted name beside ``text``, when they differ
|
|
48
|
+
#: members the selected plot draws as CONTEXT — what its check adds or
|
|
49
|
+
#: takes off
|
|
50
|
+
on_selected: int
|
|
51
|
+
elsewhere: tuple[int, ...] #: other plots showing any member, 0-based
|
|
52
|
+
#: whether a member is the selected plot's SUBJECT: its check is on and
|
|
53
|
+
#: grey, since a plot cannot lose its subject
|
|
54
|
+
is_subject: bool
|
|
55
|
+
#: the plot (0-based) a member is the subject of, or None: a variable
|
|
56
|
+
#: with a plot of its own has its "+ new plot" grey
|
|
57
|
+
subject_of: int | None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class VarGroup:
|
|
62
|
+
#: 0 tracked, 1 a prefix group, 2 top level, 3 diagnostics: a variable
|
|
63
|
+
#: named ``Tracked.a`` is in a prefix group titled "Tracked", never in
|
|
64
|
+
#: the tracked one
|
|
65
|
+
rank: int
|
|
66
|
+
title: str
|
|
67
|
+
rows: tuple[VarRow, ...]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _group(name: str, tracked: set[str], diagnostics: set[str]) -> tuple[int, str]:
|
|
71
|
+
if name in tracked:
|
|
72
|
+
return 0, "Tracked"
|
|
73
|
+
if name in diagnostics:
|
|
74
|
+
return 3, "Diagnostics"
|
|
75
|
+
# a derivative is in the group of what it differentiates:
|
|
76
|
+
# `der(pipe.T[1])` is in "pipe", never "der(pipe"
|
|
77
|
+
while name.startswith("der("):
|
|
78
|
+
name = name[4:]
|
|
79
|
+
# found in the mask: `'a.b'.c` is in "'a.b'", never "'a"
|
|
80
|
+
masked = mask_quoted(name)
|
|
81
|
+
dot = masked.find(".")
|
|
82
|
+
if dot == -1:
|
|
83
|
+
return 2, TOP_LEVEL
|
|
84
|
+
# the component, not its index: `port[2, 1].C[1]` is in "port"
|
|
85
|
+
bracket = masked.find("[", 0, dot)
|
|
86
|
+
return 1, name[: dot if bracket == -1 else bracket]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def variable_groups(
|
|
90
|
+
*,
|
|
91
|
+
names: Sequence[str],
|
|
92
|
+
tracked: Sequence[str],
|
|
93
|
+
diagnostics: Collection[str],
|
|
94
|
+
labels: Mapping[str, str],
|
|
95
|
+
plots: Sequence[ResolvedPlot],
|
|
96
|
+
selected: int | None,
|
|
97
|
+
keep: Callable[[str], bool],
|
|
98
|
+
unit_tests: Collection[str] = (),
|
|
99
|
+
group: str = "component",
|
|
100
|
+
order: str = "file",
|
|
101
|
+
read: str = "both",
|
|
102
|
+
) -> list[VarGroup]:
|
|
103
|
+
"""``names`` in the drawer's groups, each row's state read off
|
|
104
|
+
``plots`` and the ``selected`` one (None: no plot); ``keep`` is the
|
|
105
|
+
filter, asked of the name AND the label. Absent from either map, a name
|
|
106
|
+
reads as itself.
|
|
107
|
+
|
|
108
|
+
The rows fold as the plots do (the owner's ruling O3,
|
|
109
|
+
:func:`~dstf.discovery.plots.derive_plots`): by RESULT-FILE NAME, never
|
|
110
|
+
by label, and each of the ``unit_tests`` names is a row of its own
|
|
111
|
+
whatever its expression reads — three UnitTests variables whose
|
|
112
|
+
expressions are ``pipe.T[1:3]`` are three plots, and one row over them
|
|
113
|
+
named a plot there is not. A row READS ``labels`` (the plain label),
|
|
114
|
+
with the name muted beside it where they differ.
|
|
115
|
+
|
|
116
|
+
``group`` (the drawer's group menu): ``component`` — tracked, then by
|
|
117
|
+
component, top level, diagnostics; ``flat`` — one group, "All
|
|
118
|
+
variables"; ``tracked`` — "Tracked", then "Other variables" with
|
|
119
|
+
everything else, diagnostics included. Families fold in every one, the
|
|
120
|
+
tracked names apart from the rest: under Flat the tracked
|
|
121
|
+
``pipe.T[1:2]`` and the untracked rest of the array fold to the same
|
|
122
|
+
key, and one row over both hid which of them the test tracks.
|
|
123
|
+
``order``: ``file`` keeps a group's rows in first-appearance
|
|
124
|
+
(result-file) order; ``name`` sorts them by what they are shown as —
|
|
125
|
+
``file_text`` when ``read`` (the names menu) is ``file``, else
|
|
126
|
+
``text``."""
|
|
127
|
+
tracked_set, diag, apart = set(tracked), set(diagnostics), set(unit_tests)
|
|
128
|
+
kept = [n for n in names if keep(n) or (n in labels and keep(labels[n]))]
|
|
129
|
+
by_group: dict[tuple[int, str], list[str]] = {}
|
|
130
|
+
for name in kept:
|
|
131
|
+
if group == "flat":
|
|
132
|
+
where = (1, ALL)
|
|
133
|
+
elif group == "tracked":
|
|
134
|
+
where = (0, "Tracked") if name in tracked_set else (1, OTHER)
|
|
135
|
+
else:
|
|
136
|
+
where = _group(name, tracked_set, diag)
|
|
137
|
+
by_group.setdefault(where, []).append(name)
|
|
138
|
+
# the plots indexed once: scanning every plot per name cost 395 ms over
|
|
139
|
+
# 6,093 names and 300 plots
|
|
140
|
+
index: dict[str, list[int]] = {}
|
|
141
|
+
subject_of: dict[str, int] = {}
|
|
142
|
+
for i, plot in enumerate(plots):
|
|
143
|
+
for name in plot.subject:
|
|
144
|
+
subject_of.setdefault(name, i)
|
|
145
|
+
for name in plot.series:
|
|
146
|
+
at = index.setdefault(name, [])
|
|
147
|
+
if not at or at[-1] != i:
|
|
148
|
+
at.append(i)
|
|
149
|
+
# the page's cards can lag the Session's plots by one sync
|
|
150
|
+
chosen = (
|
|
151
|
+
plots[selected] if selected is not None and 0 <= selected < len(plots) else None
|
|
152
|
+
)
|
|
153
|
+
context = set(chosen.context) if chosen is not None else set()
|
|
154
|
+
subject = set(chosen.subject) if chosen is not None else set()
|
|
155
|
+
showing = {n: tuple(index.get(n, ())) for n in kept}
|
|
156
|
+
position = {n: i for i, n in enumerate(kept)}
|
|
157
|
+
out: list[VarGroup] = []
|
|
158
|
+
for rank, title in sorted(by_group, key=lambda g: (g[0], g[1].casefold(), g[1])):
|
|
159
|
+
in_group = by_group[rank, title]
|
|
160
|
+
# Folded apart, then merged back in result-file order (a family's
|
|
161
|
+
# first appearance, `group_families`' own order).
|
|
162
|
+
families = sorted(
|
|
163
|
+
[(n, [n]) for n in in_group if n in apart]
|
|
164
|
+
+ group_families(
|
|
165
|
+
[n for n in in_group if n in tracked_set and n not in apart]
|
|
166
|
+
)
|
|
167
|
+
+ group_families(
|
|
168
|
+
[n for n in in_group if n not in tracked_set and n not in apart]
|
|
169
|
+
),
|
|
170
|
+
key=lambda f: min(position[m] for m in f[1]),
|
|
171
|
+
)
|
|
172
|
+
rows = []
|
|
173
|
+
for key, members in families:
|
|
174
|
+
text = family_label(key, members, labels)
|
|
175
|
+
file_text = family_label(key, members)
|
|
176
|
+
lone = members[0] if len(members) == 1 else None
|
|
177
|
+
on = [m for m in members if m in context]
|
|
178
|
+
elsewhere = sorted(
|
|
179
|
+
{i for m in members for i in showing[m] if i != selected}
|
|
180
|
+
)
|
|
181
|
+
rows.append(
|
|
182
|
+
VarRow(
|
|
183
|
+
key=key,
|
|
184
|
+
id=f"{rank}/{title}/"
|
|
185
|
+
+ ("tracked:" if rank != 0 and members[0] in tracked_set else "")
|
|
186
|
+
+ key,
|
|
187
|
+
members=tuple(members),
|
|
188
|
+
text=text,
|
|
189
|
+
file_text=file_text,
|
|
190
|
+
name=lone if lone is not None and text != lone else None,
|
|
191
|
+
on_selected=len(on),
|
|
192
|
+
elsewhere=tuple(elsewhere),
|
|
193
|
+
is_subject=any(m in subject for m in members),
|
|
194
|
+
subject_of=next(
|
|
195
|
+
(subject_of[m] for m in members if m in subject_of), None
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
if order == "name":
|
|
200
|
+
shown = (lambda r: r.file_text) if read == "file" else (lambda r: r.text)
|
|
201
|
+
rows.sort(key=lambda r: (shown(r).casefold(), shown(r)))
|
|
202
|
+
out.append(VarGroup(rank, title, tuple(rows)))
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def cap(groups: list[VarGroup], limit: int) -> tuple[list[VarGroup], int, int]:
|
|
207
|
+
"""At most ``limit`` rows, groups in order: ``(groups, shown, total)``.
|
|
208
|
+
A result of thousands of names drew thousands of rows otherwise."""
|
|
209
|
+
total = sum(len(g.rows) for g in groups)
|
|
210
|
+
out, left = [], limit
|
|
211
|
+
for g in groups:
|
|
212
|
+
if left <= 0:
|
|
213
|
+
break
|
|
214
|
+
out.append(replace(g, rows=g.rows[:left]))
|
|
215
|
+
left -= len(out[-1].rows)
|
|
216
|
+
return out, min(total, limit), total
|