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,291 @@
|
|
|
1
|
+
"""Registration of the built-in metrics and combinators.
|
|
2
|
+
|
|
3
|
+
Imported for its side effect by the package ``__init__`` — the same shape
|
|
4
|
+
``backends`` uses for its backends. Each ``build`` here reproduces the
|
|
5
|
+
per-mode construction that used to live in ``modes.resolve_mode``'s
|
|
6
|
+
if/elif chain, so it is the single place that knows how an override dict
|
|
7
|
+
turns into a configured mode.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ..backends.types import DatasetType
|
|
13
|
+
from .handles import (
|
|
14
|
+
edit_events_handle,
|
|
15
|
+
edit_peaks_handle,
|
|
16
|
+
edit_points_handle,
|
|
17
|
+
edit_range_handle,
|
|
18
|
+
edit_scalar_handle,
|
|
19
|
+
edit_tube_handle,
|
|
20
|
+
place_events_row,
|
|
21
|
+
place_peaks_row,
|
|
22
|
+
place_points_row,
|
|
23
|
+
place_tube_row,
|
|
24
|
+
tube_row_noun,
|
|
25
|
+
)
|
|
26
|
+
from .metric_tree import (
|
|
27
|
+
AndCombinator,
|
|
28
|
+
KOfNCombinator,
|
|
29
|
+
OrCombinator,
|
|
30
|
+
WarnCombinator,
|
|
31
|
+
WeightedCombinator,
|
|
32
|
+
)
|
|
33
|
+
from .modes import (
|
|
34
|
+
_TUBE_KEYS,
|
|
35
|
+
DominantFrequencyConfig,
|
|
36
|
+
DominantFrequencyMode,
|
|
37
|
+
EventTimingConfig,
|
|
38
|
+
EventTimingMode,
|
|
39
|
+
NrmseConfig,
|
|
40
|
+
NrmseMode,
|
|
41
|
+
PointsConfig,
|
|
42
|
+
PointsMode,
|
|
43
|
+
RangeConfig,
|
|
44
|
+
RangeMode,
|
|
45
|
+
ScalarConfig,
|
|
46
|
+
ScalarMode,
|
|
47
|
+
TubeConfig,
|
|
48
|
+
TubeMode,
|
|
49
|
+
)
|
|
50
|
+
from .registry import (
|
|
51
|
+
DEFAULT_METRIC,
|
|
52
|
+
CombinatorDescriptor,
|
|
53
|
+
MetricDescriptor,
|
|
54
|
+
metric,
|
|
55
|
+
register_combinator,
|
|
56
|
+
register_metric,
|
|
57
|
+
)
|
|
58
|
+
from .seeds import seed_dominant_frequency, seed_range, seed_scalar, seed_tube
|
|
59
|
+
from .sides import Sides, parse_number, parse_sides
|
|
60
|
+
from .summaries import (
|
|
61
|
+
summarize_dominant_frequency,
|
|
62
|
+
summarize_event_timing,
|
|
63
|
+
summarize_nrmse,
|
|
64
|
+
summarize_points,
|
|
65
|
+
summarize_range,
|
|
66
|
+
summarize_scalar,
|
|
67
|
+
summarize_tube,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _build_nrmse(override: dict, tolerance: Sides) -> NrmseMode:
|
|
72
|
+
if not tolerance.equal:
|
|
73
|
+
raise ValueError("nrmse has one ceiling: tolerance must be a number")
|
|
74
|
+
return NrmseMode(NrmseConfig(tolerance=tolerance.upper))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_tube(override: dict, tolerance: Sides) -> TubeMode:
|
|
78
|
+
tube_kwargs = {k: override[k] for k in _TUBE_KEYS if k in override}
|
|
79
|
+
for key in ("tube_abs", "tube_rel"):
|
|
80
|
+
if key not in tube_kwargs:
|
|
81
|
+
continue
|
|
82
|
+
if tube_kwargs[key] is None:
|
|
83
|
+
# ``null`` is "no width", as it was when a width was a number
|
|
84
|
+
# (a falsy one was left out): absent, not a config error.
|
|
85
|
+
del tube_kwargs[key]
|
|
86
|
+
else:
|
|
87
|
+
tube_kwargs[key] = parse_sides(tube_kwargs[key], key)
|
|
88
|
+
# The floor has no sides: one number, read here so that anything else
|
|
89
|
+
# is a ``ValueError`` where ``resolve_mode``'s callers catch it — a
|
|
90
|
+
# pair or a string went on to the scorer, which compared it with zero
|
|
91
|
+
# and raised a ``TypeError`` nothing caught. ``null`` is "no floor".
|
|
92
|
+
if tube_kwargs.get("tube_min_width") is None:
|
|
93
|
+
tube_kwargs.pop("tube_min_width", None)
|
|
94
|
+
else:
|
|
95
|
+
tube_kwargs["tube_min_width"] = parse_number(
|
|
96
|
+
tube_kwargs["tube_min_width"], "tube_min_width"
|
|
97
|
+
)
|
|
98
|
+
return TubeMode(TubeConfig(**tube_kwargs))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_points(override: dict, tolerance: Sides) -> PointsMode:
|
|
102
|
+
return PointsMode(
|
|
103
|
+
PointsConfig(
|
|
104
|
+
points=override.get("points"),
|
|
105
|
+
tolerance=tolerance,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _bound(override: dict, key: str) -> float | None:
|
|
111
|
+
"""A ``min_value`` / ``max_value`` as it is written: one number, or
|
|
112
|
+
none (absent, or ``null``). Checked here, as a tube's floor is, so that
|
|
113
|
+
anything else is a ``ValueError`` where ``resolve_mode``'s callers catch
|
|
114
|
+
it: a string, or an integer too large for a float, went on to the
|
|
115
|
+
scorer, which subtracted it from the samples and raised what nothing
|
|
116
|
+
caught."""
|
|
117
|
+
value = override.get(key)
|
|
118
|
+
if value is not None:
|
|
119
|
+
parse_number(value, key)
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_range(override: dict, tolerance: Sides) -> RangeMode:
|
|
124
|
+
# ``min_value`` / ``max_value`` only — the names the ``RangeConfig``
|
|
125
|
+
# dataclass, the auto-derived UI and MetricTree leaf params all use.
|
|
126
|
+
return RangeMode(
|
|
127
|
+
RangeConfig(
|
|
128
|
+
min_value=_bound(override, "min_value"),
|
|
129
|
+
max_value=_bound(override, "max_value"),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_event_timing(override: dict, tolerance: Sides) -> EventTimingMode:
|
|
135
|
+
return EventTimingMode(
|
|
136
|
+
EventTimingConfig(
|
|
137
|
+
time_tolerance=parse_sides(
|
|
138
|
+
override.get("time_tolerance", 1e-3), "time_tolerance"
|
|
139
|
+
),
|
|
140
|
+
count_must_match=override.get("count_must_match", True),
|
|
141
|
+
events=override.get("events"),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _build_dominant_frequency(
|
|
147
|
+
override: dict, tolerance: Sides
|
|
148
|
+
) -> DominantFrequencyMode:
|
|
149
|
+
return DominantFrequencyMode(
|
|
150
|
+
DominantFrequencyConfig(
|
|
151
|
+
peaks=override.get("peaks"),
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _build_scalar(override: dict, tolerance: Sides) -> ScalarMode:
|
|
157
|
+
return ScalarMode(
|
|
158
|
+
ScalarConfig(
|
|
159
|
+
quantity=override.get("quantity"),
|
|
160
|
+
tolerance=tolerance,
|
|
161
|
+
tolerance_mode=override.get("tolerance_mode", "abs"),
|
|
162
|
+
min_value=_bound(override, "min_value"),
|
|
163
|
+
max_value=_bound(override, "max_value"),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def register_builtins() -> None:
|
|
169
|
+
"""Register the seven built-in metrics and five combinators.
|
|
170
|
+
|
|
171
|
+
Registration order is the canonical presentation order (schema export
|
|
172
|
+
``$defs``, the reporter's mode-schema map, the "valid: ..." list in
|
|
173
|
+
:func:`~.modes.resolve_mode`'s error message).
|
|
174
|
+
|
|
175
|
+
Idempotent: ``importlib.reload(dstf.comparison)`` re-executes the
|
|
176
|
+
package init, and a second registration pass must not trip the
|
|
177
|
+
duplicate-name guard (B1 review finding 3).
|
|
178
|
+
"""
|
|
179
|
+
if metric(DEFAULT_METRIC) is not None:
|
|
180
|
+
return
|
|
181
|
+
register_metric(
|
|
182
|
+
MetricDescriptor(
|
|
183
|
+
name="nrmse",
|
|
184
|
+
config_cls=NrmseConfig,
|
|
185
|
+
build=_build_nrmse,
|
|
186
|
+
score_direction="lower",
|
|
187
|
+
summarize=summarize_nrmse,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
register_metric(
|
|
191
|
+
MetricDescriptor(
|
|
192
|
+
name="tube",
|
|
193
|
+
config_cls=TubeConfig,
|
|
194
|
+
build=_build_tube,
|
|
195
|
+
# Tube's score is the fraction of samples inside the envelope.
|
|
196
|
+
score_direction="higher",
|
|
197
|
+
summarize=summarize_tube,
|
|
198
|
+
edit_handle=edit_tube_handle,
|
|
199
|
+
seed=seed_tube,
|
|
200
|
+
place_row=place_tube_row,
|
|
201
|
+
row_noun=tube_row_noun,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
register_metric(
|
|
205
|
+
MetricDescriptor(
|
|
206
|
+
name="points",
|
|
207
|
+
config_cls=PointsConfig,
|
|
208
|
+
build=_build_points,
|
|
209
|
+
score_direction="lower",
|
|
210
|
+
summarize=summarize_points,
|
|
211
|
+
edit_handle=edit_points_handle,
|
|
212
|
+
place_row=place_points_row,
|
|
213
|
+
row_noun=lambda _leaf: "a point",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
register_metric(
|
|
217
|
+
MetricDescriptor(
|
|
218
|
+
name="range",
|
|
219
|
+
config_cls=RangeConfig,
|
|
220
|
+
build=_build_range,
|
|
221
|
+
score_direction="lower",
|
|
222
|
+
summarize=summarize_range,
|
|
223
|
+
edit_handle=edit_range_handle,
|
|
224
|
+
drawn_by_default=False,
|
|
225
|
+
seed=seed_range,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
register_metric(
|
|
229
|
+
MetricDescriptor(
|
|
230
|
+
name="event-timing",
|
|
231
|
+
config_cls=EventTimingConfig,
|
|
232
|
+
build=_build_event_timing,
|
|
233
|
+
score_direction="lower",
|
|
234
|
+
summarize=summarize_event_timing,
|
|
235
|
+
edit_handle=edit_events_handle,
|
|
236
|
+
place_row=place_events_row,
|
|
237
|
+
row_noun=lambda _leaf: "an event",
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
register_metric(
|
|
241
|
+
MetricDescriptor(
|
|
242
|
+
name="dominant-frequency",
|
|
243
|
+
config_cls=DominantFrequencyConfig,
|
|
244
|
+
build=_build_dominant_frequency,
|
|
245
|
+
score_direction="lower",
|
|
246
|
+
summarize=summarize_dominant_frequency,
|
|
247
|
+
edit_handle=edit_peaks_handle,
|
|
248
|
+
seed=seed_dominant_frequency,
|
|
249
|
+
place_row=place_peaks_row,
|
|
250
|
+
row_noun=lambda _leaf: "a peak",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
register_metric(
|
|
254
|
+
MetricDescriptor(
|
|
255
|
+
name="scalar",
|
|
256
|
+
config_cls=ScalarConfig,
|
|
257
|
+
build=_build_scalar,
|
|
258
|
+
# The one metric that scores a number rather than a trajectory:
|
|
259
|
+
# it works on a scalars-only dataset as well as on a time series
|
|
260
|
+
# it reduces itself.
|
|
261
|
+
accepted_datasets=frozenset({DatasetType.TIME_SERIES, DatasetType.SCALARS}),
|
|
262
|
+
score_direction="lower",
|
|
263
|
+
# The only builtin whose ``quantity`` names its data source.
|
|
264
|
+
accepts_quantity=True,
|
|
265
|
+
summarize=summarize_scalar,
|
|
266
|
+
edit_handle=edit_scalar_handle,
|
|
267
|
+
drawn_by_default=False,
|
|
268
|
+
seed=seed_scalar,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
register_combinator(
|
|
273
|
+
CombinatorDescriptor(name="and", build=lambda spec: AndCombinator())
|
|
274
|
+
)
|
|
275
|
+
register_combinator(
|
|
276
|
+
CombinatorDescriptor(name="or", build=lambda spec: OrCombinator())
|
|
277
|
+
)
|
|
278
|
+
register_combinator(
|
|
279
|
+
CombinatorDescriptor(name="k-of-n", build=lambda spec: KOfNCombinator(spec.k))
|
|
280
|
+
)
|
|
281
|
+
register_combinator(
|
|
282
|
+
CombinatorDescriptor(name="warn", build=lambda spec: WarnCombinator())
|
|
283
|
+
)
|
|
284
|
+
register_combinator(
|
|
285
|
+
CombinatorDescriptor(
|
|
286
|
+
name="weighted",
|
|
287
|
+
build=lambda spec: WeightedCombinator(
|
|
288
|
+
spec.weights, spec.threshold, spec.direction
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
)
|