diffract-core 0.2.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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
"""Factory helpers for building bound subplot grids."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from diffract.viz.plots.base.plot import Plot
|
|
11
|
+
from diffract.viz.styling import Theme
|
|
12
|
+
|
|
13
|
+
from .grid import GridPlot
|
|
14
|
+
from .spec import VALID_SESSION_FILTER_KEYS, SubplotSpec
|
|
15
|
+
|
|
16
|
+
BindTarget = Literal["plot", "session_filter", "value_filter"]
|
|
17
|
+
ValueFilterOperator = Literal[">", "<", ">=", "<=", "==", "!="]
|
|
18
|
+
CellWhere = Literal["all", "first_row", "last_row", "first_col", "last_col"]
|
|
19
|
+
|
|
20
|
+
VALID_BIND_TARGETS = frozenset({"plot", "session_filter", "value_filter"})
|
|
21
|
+
VALID_VALUE_FILTER_OPERATORS = frozenset({">", "<", ">=", "<=", "==", "!="})
|
|
22
|
+
VALID_CELL_WHERE = frozenset({"all", "first_row", "last_row", "first_col", "last_col"})
|
|
23
|
+
_FILTER_CONDITION_ITEM_COUNT = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(kw_only=True)
|
|
27
|
+
class GridAxisBind:
|
|
28
|
+
"""Axis binding definition for grid generation.
|
|
29
|
+
|
|
30
|
+
A bind maps a sequence of values onto either rows or columns in a generated
|
|
31
|
+
`GridPlot`, overriding one target key per subplot cell.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
target: Target domain to override.
|
|
35
|
+
- "plot": set an attribute on the per-cell plot copy.
|
|
36
|
+
- "session_filter": set a key in `SubplotSpec.session_filter`.
|
|
37
|
+
- "value_filter": set a key in `SubplotSpec.value_filter`.
|
|
38
|
+
key: Target key within the selected domain.
|
|
39
|
+
values: Ordered values to place along the axis.
|
|
40
|
+
labels: Optional display labels for titles. If omitted, labels are
|
|
41
|
+
derived from values via `str(value)`.
|
|
42
|
+
op: Optional operator for `value_filter` binds. If provided, each entry
|
|
43
|
+
in `values` is treated as a threshold and converted to
|
|
44
|
+
`(op, threshold)`. If omitted, each value must already be
|
|
45
|
+
`(operator, threshold)`.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
target: BindTarget
|
|
49
|
+
key: str
|
|
50
|
+
values: Sequence[Any]
|
|
51
|
+
labels: Sequence[str] | None = None
|
|
52
|
+
op: ValueFilterOperator | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(kw_only=True)
|
|
56
|
+
class CellSelector:
|
|
57
|
+
"""Select cells where a rule should be applied.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
where: Predefined positional selector.
|
|
61
|
+
row: Optional explicit row index (1-based).
|
|
62
|
+
col: Optional explicit column index (1-based).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
where: CellWhere = "all"
|
|
66
|
+
row: int | None = None
|
|
67
|
+
col: int | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(kw_only=True)
|
|
71
|
+
class GridCellRule:
|
|
72
|
+
"""Conditional overrides applied to selected grid cells.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
selector: Which cells to target.
|
|
76
|
+
plot: Direct plot-attribute overrides for matching cells.
|
|
77
|
+
plot_format: Format-string plot overrides. Values are formatted with
|
|
78
|
+
placeholders from cell context:
|
|
79
|
+
row, col, n_rows, n_cols,
|
|
80
|
+
row_label, col_label, row_value, col_value, row_key, col_key,
|
|
81
|
+
is_first_row, is_last_row, is_first_col, is_last_col.
|
|
82
|
+
session_filter: Extra per-cell session filter values.
|
|
83
|
+
value_filter: Extra per-cell value filter conditions.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
selector: CellSelector = field(default_factory=CellSelector)
|
|
87
|
+
plot: dict[str, Any] | None = None
|
|
88
|
+
plot_format: dict[str, str] | None = None
|
|
89
|
+
session_filter: dict[str, Any] | None = None
|
|
90
|
+
value_filter: dict[str, tuple[str, Any] | list[Any]] | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class _AxisPoint:
|
|
95
|
+
value: Any
|
|
96
|
+
label: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class _CellContext:
|
|
101
|
+
row: int
|
|
102
|
+
col: int
|
|
103
|
+
n_rows: int
|
|
104
|
+
n_cols: int
|
|
105
|
+
row_bind: GridAxisBind | None
|
|
106
|
+
col_bind: GridAxisBind | None
|
|
107
|
+
row_point: _AxisPoint
|
|
108
|
+
col_point: _AxisPoint
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_bound_grid(
|
|
112
|
+
*,
|
|
113
|
+
plot_template: Plot,
|
|
114
|
+
row: GridAxisBind | None = None,
|
|
115
|
+
col: GridAxisBind | None = None,
|
|
116
|
+
cell_rules: Sequence[GridCellRule] | None = None,
|
|
117
|
+
title_template: str | None = None,
|
|
118
|
+
make_subplots_kwargs: dict[str, Any] | None = None,
|
|
119
|
+
theme: Theme | None = None,
|
|
120
|
+
base_session_filter: dict[str, Any] | None = None,
|
|
121
|
+
base_value_filter: dict[str, tuple[str, Any]] | None = None,
|
|
122
|
+
) -> GridPlot:
|
|
123
|
+
"""Build a `GridPlot` by binding row/column values onto subplot templates.
|
|
124
|
+
|
|
125
|
+
At least one axis bind (`row` or `col`) is required. For each generated cell:
|
|
126
|
+
1. The plot template is deep-copied.
|
|
127
|
+
2. Row bind (if provided) is applied.
|
|
128
|
+
3. Column bind (if provided) is applied.
|
|
129
|
+
4. Matching `cell_rules` are applied in order (last write wins).
|
|
130
|
+
5. A `SubplotSpec` is created.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
plot_template: Base plot object copied for each subplot cell.
|
|
134
|
+
row: Optional row-axis bind.
|
|
135
|
+
col: Optional column-axis bind.
|
|
136
|
+
cell_rules: Optional positional rules for per-cell overrides.
|
|
137
|
+
title_template: Optional format string for subplot titles.
|
|
138
|
+
Available placeholders:
|
|
139
|
+
- row_label, col_label
|
|
140
|
+
- row_value, col_value
|
|
141
|
+
- row_key, col_key
|
|
142
|
+
- row, col
|
|
143
|
+
If omitted, titles default to:
|
|
144
|
+
- "{row_label} | {col_label}" when both binds exist,
|
|
145
|
+
- "{row_label}" or "{col_label}" for single-axis grids.
|
|
146
|
+
make_subplots_kwargs: Extra kwargs passed into `GridPlot`.
|
|
147
|
+
theme: Optional theme for the resulting `GridPlot`.
|
|
148
|
+
base_session_filter: Optional session filter merged into each subplot.
|
|
149
|
+
base_value_filter: Optional value filter merged into each subplot.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A `GridPlot` with auto-generated `SubplotSpec` entries.
|
|
153
|
+
"""
|
|
154
|
+
_validate_axes(row=row, col=col)
|
|
155
|
+
_validate_cell_rules(cell_rules=cell_rules, plot_template=plot_template)
|
|
156
|
+
|
|
157
|
+
row_points = _materialize_axis_points(row)
|
|
158
|
+
col_points = _materialize_axis_points(col)
|
|
159
|
+
n_rows = len(row_points)
|
|
160
|
+
n_cols = len(col_points)
|
|
161
|
+
|
|
162
|
+
subplots: list[SubplotSpec] = []
|
|
163
|
+
for row_idx, row_point in enumerate(row_points, start=1):
|
|
164
|
+
for col_idx, col_point in enumerate(col_points, start=1):
|
|
165
|
+
plot = deepcopy(plot_template)
|
|
166
|
+
session_filter = dict(base_session_filter or {})
|
|
167
|
+
value_filter = dict(base_value_filter or {})
|
|
168
|
+
|
|
169
|
+
_apply_axis_bind(
|
|
170
|
+
bind=row,
|
|
171
|
+
point=row_point,
|
|
172
|
+
plot=plot,
|
|
173
|
+
session_filter=session_filter,
|
|
174
|
+
value_filter=value_filter,
|
|
175
|
+
)
|
|
176
|
+
_apply_axis_bind(
|
|
177
|
+
bind=col,
|
|
178
|
+
point=col_point,
|
|
179
|
+
plot=plot,
|
|
180
|
+
session_filter=session_filter,
|
|
181
|
+
value_filter=value_filter,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
context = _CellContext(
|
|
185
|
+
row=row_idx,
|
|
186
|
+
col=col_idx,
|
|
187
|
+
n_rows=n_rows,
|
|
188
|
+
n_cols=n_cols,
|
|
189
|
+
row_bind=row,
|
|
190
|
+
col_bind=col,
|
|
191
|
+
row_point=row_point,
|
|
192
|
+
col_point=col_point,
|
|
193
|
+
)
|
|
194
|
+
_apply_cell_rules(
|
|
195
|
+
rules=cell_rules,
|
|
196
|
+
context=context,
|
|
197
|
+
plot=plot,
|
|
198
|
+
session_filter=session_filter,
|
|
199
|
+
value_filter=value_filter,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
title = _build_title(
|
|
203
|
+
row=row_idx,
|
|
204
|
+
col=col_idx,
|
|
205
|
+
row_bind=row,
|
|
206
|
+
col_bind=col,
|
|
207
|
+
row_point=row_point,
|
|
208
|
+
col_point=col_point,
|
|
209
|
+
title_template=title_template,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
subplots.append(
|
|
213
|
+
SubplotSpec(
|
|
214
|
+
row=row_idx,
|
|
215
|
+
col=col_idx,
|
|
216
|
+
title=title,
|
|
217
|
+
plot=plot,
|
|
218
|
+
session_filter=session_filter or None,
|
|
219
|
+
value_filter=value_filter or None,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return GridPlot(
|
|
224
|
+
subplots=subplots,
|
|
225
|
+
make_subplots_kwargs=dict(make_subplots_kwargs or {}),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _validate_axes(*, row: GridAxisBind | None, col: GridAxisBind | None) -> None:
|
|
230
|
+
if row is None and col is None:
|
|
231
|
+
raise ValueError("At least one bind must be provided: row=... or col=....")
|
|
232
|
+
|
|
233
|
+
_validate_bind(row, axis_name="row")
|
|
234
|
+
_validate_bind(col, axis_name="col")
|
|
235
|
+
|
|
236
|
+
if row is not None and col is not None:
|
|
237
|
+
same_target = row.target == col.target
|
|
238
|
+
same_key = row.key == col.key
|
|
239
|
+
if same_target and same_key:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
"Row and column binds target the same key "
|
|
242
|
+
f"('{row.target}.{row.key}'). Use different keys per axis."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _validate_bind(bind: GridAxisBind | None, *, axis_name: str) -> None:
|
|
247
|
+
if bind is None:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if bind.target not in VALID_BIND_TARGETS:
|
|
251
|
+
valid_targets = sorted(VALID_BIND_TARGETS)
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"Invalid target '{bind.target}' in {axis_name} bind. "
|
|
254
|
+
f"Valid targets: {valid_targets}."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if not bind.key:
|
|
258
|
+
raise ValueError(f"{axis_name} bind key must be non-empty.")
|
|
259
|
+
|
|
260
|
+
if _is_scalar_like(bind.values):
|
|
261
|
+
raise TypeError(
|
|
262
|
+
f"{axis_name} bind values must be a sequence, got scalar "
|
|
263
|
+
f"{type(bind.values).__name__}."
|
|
264
|
+
)
|
|
265
|
+
if len(bind.values) == 0:
|
|
266
|
+
raise ValueError(f"{axis_name} bind values must not be empty.")
|
|
267
|
+
|
|
268
|
+
if bind.labels is not None:
|
|
269
|
+
if _is_scalar_like(bind.labels):
|
|
270
|
+
raise TypeError(
|
|
271
|
+
f"{axis_name} bind labels must be a sequence of strings, got scalar "
|
|
272
|
+
f"{type(bind.labels).__name__}."
|
|
273
|
+
)
|
|
274
|
+
if len(bind.labels) != len(bind.values):
|
|
275
|
+
raise ValueError(
|
|
276
|
+
f"{axis_name} bind labels length must match values length: "
|
|
277
|
+
f"{len(bind.labels)} != {len(bind.values)}."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if bind.target == "session_filter":
|
|
281
|
+
if bind.key not in VALID_SESSION_FILTER_KEYS:
|
|
282
|
+
valid_keys = sorted(VALID_SESSION_FILTER_KEYS)
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"Invalid session_filter key '{bind.key}' for {axis_name} bind. "
|
|
285
|
+
f"Valid keys: {valid_keys}."
|
|
286
|
+
)
|
|
287
|
+
if bind.op is not None:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"{axis_name} bind uses op='{bind.op}' with target='session_filter'. "
|
|
290
|
+
"Operator is supported only for target='value_filter'."
|
|
291
|
+
)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
if bind.target == "plot":
|
|
295
|
+
if bind.op is not None:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"{axis_name} bind uses op='{bind.op}' with target='plot'. "
|
|
298
|
+
"Operator is supported only for target='value_filter'."
|
|
299
|
+
)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
if bind.target == "value_filter":
|
|
303
|
+
if bind.op is not None and bind.op not in VALID_VALUE_FILTER_OPERATORS:
|
|
304
|
+
valid_ops = sorted(VALID_VALUE_FILTER_OPERATORS)
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"Invalid value_filter operator '{bind.op}' in {axis_name} bind. "
|
|
307
|
+
f"Valid operators: {valid_ops}."
|
|
308
|
+
)
|
|
309
|
+
for value in bind.values:
|
|
310
|
+
if bind.op is not None:
|
|
311
|
+
continue
|
|
312
|
+
_validate_value_filter_condition(value, axis_name=axis_name, key=bind.key)
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _validate_cell_rules(
|
|
317
|
+
*,
|
|
318
|
+
cell_rules: Sequence[GridCellRule] | None,
|
|
319
|
+
plot_template: Plot,
|
|
320
|
+
) -> None:
|
|
321
|
+
if cell_rules is None:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if _is_scalar_like(cell_rules):
|
|
325
|
+
raise TypeError(
|
|
326
|
+
"cell_rules must be a sequence of GridCellRule, "
|
|
327
|
+
f"got scalar {type(cell_rules).__name__}."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
for idx, rule in enumerate(cell_rules, start=1):
|
|
331
|
+
if not isinstance(rule, GridCellRule):
|
|
332
|
+
raise TypeError(
|
|
333
|
+
f"cell_rules[{idx}] must be GridCellRule, got {type(rule).__name__}."
|
|
334
|
+
)
|
|
335
|
+
_validate_cell_selector(rule.selector, rule_idx=idx)
|
|
336
|
+
_validate_rule_plot_overrides(rule, plot_template=plot_template, rule_idx=idx)
|
|
337
|
+
_validate_rule_session_filter(rule, rule_idx=idx)
|
|
338
|
+
_validate_rule_value_filter(rule, rule_idx=idx)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _validate_cell_selector(selector: CellSelector, *, rule_idx: int) -> None:
|
|
342
|
+
if selector.where not in VALID_CELL_WHERE:
|
|
343
|
+
valid_where = sorted(VALID_CELL_WHERE)
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"Invalid cell_rules[{rule_idx}].selector.where='{selector.where}'. "
|
|
346
|
+
f"Valid values: {valid_where}."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
for axis_name, value in (("row", selector.row), ("col", selector.col)):
|
|
350
|
+
if value is None:
|
|
351
|
+
continue
|
|
352
|
+
if not _is_positive_int(value):
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"cell_rules[{rule_idx}].selector.{axis_name} must be positive "
|
|
355
|
+
f"1-based index, got {value!r}."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _validate_rule_plot_overrides(
|
|
360
|
+
rule: GridCellRule,
|
|
361
|
+
*,
|
|
362
|
+
plot_template: Plot,
|
|
363
|
+
rule_idx: int,
|
|
364
|
+
) -> None:
|
|
365
|
+
if rule.plot is not None and not isinstance(rule.plot, dict):
|
|
366
|
+
raise TypeError(
|
|
367
|
+
f"cell_rules[{rule_idx}].plot must be a dict, "
|
|
368
|
+
f"got {type(rule.plot).__name__}."
|
|
369
|
+
)
|
|
370
|
+
if rule.plot_format is not None and not isinstance(rule.plot_format, dict):
|
|
371
|
+
raise TypeError(
|
|
372
|
+
f"cell_rules[{rule_idx}].plot_format must be a dict, "
|
|
373
|
+
f"got {type(rule.plot_format).__name__}."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
static_keys = set(rule.plot or {})
|
|
377
|
+
formatted_keys = set(rule.plot_format or {})
|
|
378
|
+
duplicate_keys = sorted(static_keys & formatted_keys)
|
|
379
|
+
if duplicate_keys:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f"cell_rules[{rule_idx}] has duplicate keys in plot and plot_format: "
|
|
382
|
+
f"{duplicate_keys}."
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
for key in sorted(static_keys | formatted_keys):
|
|
386
|
+
if not key:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"cell_rules[{rule_idx}] has empty key in plot/plot_format override."
|
|
389
|
+
)
|
|
390
|
+
if not hasattr(plot_template, key):
|
|
391
|
+
available = _bindable_plot_keys(plot_template)
|
|
392
|
+
raise ValueError(
|
|
393
|
+
f"Unknown plot override key '{key}' in cell_rules[{rule_idx}] for "
|
|
394
|
+
f"{type(plot_template).__name__}. Available fields: {available}."
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
for key, template in (rule.plot_format or {}).items():
|
|
398
|
+
if not isinstance(template, str):
|
|
399
|
+
raise TypeError(
|
|
400
|
+
f"cell_rules[{rule_idx}].plot_format['{key}'] must be string "
|
|
401
|
+
f"template, got {type(template).__name__}."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _validate_rule_session_filter(rule: GridCellRule, *, rule_idx: int) -> None:
|
|
406
|
+
if rule.session_filter is None:
|
|
407
|
+
return
|
|
408
|
+
if not isinstance(rule.session_filter, dict):
|
|
409
|
+
raise TypeError(
|
|
410
|
+
f"cell_rules[{rule_idx}].session_filter must be a dict, "
|
|
411
|
+
f"got {type(rule.session_filter).__name__}."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
invalid_keys = sorted(set(rule.session_filter) - VALID_SESSION_FILTER_KEYS)
|
|
415
|
+
if invalid_keys:
|
|
416
|
+
valid_keys = sorted(VALID_SESSION_FILTER_KEYS)
|
|
417
|
+
raise ValueError(
|
|
418
|
+
f"Invalid session_filter keys {invalid_keys} in cell_rules[{rule_idx}]. "
|
|
419
|
+
f"Valid keys: {valid_keys}."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _validate_rule_value_filter(rule: GridCellRule, *, rule_idx: int) -> None:
|
|
424
|
+
if rule.value_filter is None:
|
|
425
|
+
return
|
|
426
|
+
if not isinstance(rule.value_filter, dict):
|
|
427
|
+
raise TypeError(
|
|
428
|
+
f"cell_rules[{rule_idx}].value_filter must be a dict, "
|
|
429
|
+
f"got {type(rule.value_filter).__name__}."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
for key, condition in rule.value_filter.items():
|
|
433
|
+
_validate_value_filter_condition(
|
|
434
|
+
condition,
|
|
435
|
+
axis_name=f"cell_rules[{rule_idx}]",
|
|
436
|
+
key=key,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _materialize_axis_points(bind: GridAxisBind | None) -> list[_AxisPoint]:
|
|
441
|
+
if bind is None:
|
|
442
|
+
return [_AxisPoint(value=None, label="")]
|
|
443
|
+
|
|
444
|
+
labels = bind.labels
|
|
445
|
+
points: list[_AxisPoint] = []
|
|
446
|
+
for idx, value in enumerate(bind.values):
|
|
447
|
+
label = labels[idx] if labels is not None else _default_label(value)
|
|
448
|
+
points.append(_AxisPoint(value=value, label=label))
|
|
449
|
+
return points
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _apply_axis_bind(
|
|
453
|
+
*,
|
|
454
|
+
bind: GridAxisBind | None,
|
|
455
|
+
point: _AxisPoint,
|
|
456
|
+
plot: Plot,
|
|
457
|
+
session_filter: dict[str, Any],
|
|
458
|
+
value_filter: dict[str, tuple[str, Any]],
|
|
459
|
+
) -> None:
|
|
460
|
+
if bind is None:
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if bind.target == "plot":
|
|
464
|
+
_set_plot_attr(plot, key=bind.key, value=point.value)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
if bind.target == "session_filter":
|
|
468
|
+
session_filter[bind.key] = point.value
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
if bind.target == "value_filter":
|
|
472
|
+
value_filter[bind.key] = _coerce_value_filter_condition(
|
|
473
|
+
bind=bind,
|
|
474
|
+
value=point.value,
|
|
475
|
+
)
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
raise ValueError(
|
|
479
|
+
f"Unsupported bind target '{bind.target}' while applying axis bind."
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _apply_cell_rules(
|
|
484
|
+
*,
|
|
485
|
+
rules: Sequence[GridCellRule] | None,
|
|
486
|
+
context: _CellContext,
|
|
487
|
+
plot: Plot,
|
|
488
|
+
session_filter: dict[str, Any],
|
|
489
|
+
value_filter: dict[str, tuple[str, Any]],
|
|
490
|
+
) -> None:
|
|
491
|
+
if not rules:
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
format_context = _build_cell_format_context(context)
|
|
495
|
+
|
|
496
|
+
for rule in rules:
|
|
497
|
+
if not _selector_matches(rule.selector, context):
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if rule.plot:
|
|
501
|
+
for key, raw_value in rule.plot.items():
|
|
502
|
+
_set_plot_attr(plot, key=key, value=raw_value)
|
|
503
|
+
|
|
504
|
+
if rule.plot_format:
|
|
505
|
+
for key, template in rule.plot_format.items():
|
|
506
|
+
rendered = _format_template(
|
|
507
|
+
template=template,
|
|
508
|
+
format_context=format_context,
|
|
509
|
+
template_name=f"plot_format['{key}']",
|
|
510
|
+
)
|
|
511
|
+
_set_plot_attr(plot, key=key, value=rendered)
|
|
512
|
+
|
|
513
|
+
if rule.session_filter:
|
|
514
|
+
session_filter.update(rule.session_filter)
|
|
515
|
+
|
|
516
|
+
if rule.value_filter:
|
|
517
|
+
for key, raw_condition in rule.value_filter.items():
|
|
518
|
+
value_filter[key] = _normalize_value_filter_condition(
|
|
519
|
+
raw_condition,
|
|
520
|
+
axis_name="cell rule",
|
|
521
|
+
key=key,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _selector_matches(selector: CellSelector, context: _CellContext) -> bool:
|
|
526
|
+
if selector.row is not None and selector.row != context.row:
|
|
527
|
+
return False
|
|
528
|
+
if selector.col is not None and selector.col != context.col:
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
where = selector.where
|
|
532
|
+
if where == "all":
|
|
533
|
+
return True
|
|
534
|
+
if where == "first_row":
|
|
535
|
+
return context.row == 1
|
|
536
|
+
if where == "last_row":
|
|
537
|
+
return context.row == context.n_rows
|
|
538
|
+
if where == "first_col":
|
|
539
|
+
return context.col == 1
|
|
540
|
+
if where == "last_col":
|
|
541
|
+
return context.col == context.n_cols
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _build_cell_format_context(context: _CellContext) -> dict[str, Any]:
|
|
546
|
+
return {
|
|
547
|
+
"row": context.row,
|
|
548
|
+
"col": context.col,
|
|
549
|
+
"n_rows": context.n_rows,
|
|
550
|
+
"n_cols": context.n_cols,
|
|
551
|
+
"row_label": context.row_point.label,
|
|
552
|
+
"col_label": context.col_point.label,
|
|
553
|
+
"row_value": context.row_point.value,
|
|
554
|
+
"col_value": context.col_point.value,
|
|
555
|
+
"row_key": context.row_bind.key if context.row_bind is not None else "",
|
|
556
|
+
"col_key": context.col_bind.key if context.col_bind is not None else "",
|
|
557
|
+
"is_first_row": context.row == 1,
|
|
558
|
+
"is_last_row": context.row == context.n_rows,
|
|
559
|
+
"is_first_col": context.col == 1,
|
|
560
|
+
"is_last_col": context.col == context.n_cols,
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _set_plot_attr(plot: Plot, *, key: str, value: Any) -> None:
|
|
565
|
+
if not hasattr(plot, key):
|
|
566
|
+
available = _bindable_plot_keys(plot)
|
|
567
|
+
raise ValueError(
|
|
568
|
+
f"Unknown plot bind key '{key}' for {type(plot).__name__}. "
|
|
569
|
+
f"Available fields: {available}."
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
coerced_value = _coerce_plot_value(plot, key=key, value=value)
|
|
573
|
+
setattr(plot, key, coerced_value)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _coerce_plot_value(plot: Plot, *, key: str, value: Any) -> Any:
|
|
577
|
+
from diffract.viz.data import FieldRef
|
|
578
|
+
|
|
579
|
+
current_value = getattr(plot, key, None)
|
|
580
|
+
if isinstance(current_value, FieldRef) and isinstance(value, str):
|
|
581
|
+
return FieldRef(field=value)
|
|
582
|
+
return value
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _coerce_value_filter_condition(
|
|
586
|
+
*,
|
|
587
|
+
bind: GridAxisBind,
|
|
588
|
+
value: Any,
|
|
589
|
+
) -> tuple[str, Any]:
|
|
590
|
+
if bind.op is not None:
|
|
591
|
+
return (bind.op, value)
|
|
592
|
+
|
|
593
|
+
return _normalize_value_filter_condition(value, axis_name="bind", key=bind.key)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _normalize_value_filter_condition(
|
|
597
|
+
value: Any,
|
|
598
|
+
*,
|
|
599
|
+
axis_name: str,
|
|
600
|
+
key: str,
|
|
601
|
+
) -> tuple[str, Any]:
|
|
602
|
+
if _is_scalar_like(value) or len(value) != _FILTER_CONDITION_ITEM_COUNT:
|
|
603
|
+
raise TypeError(
|
|
604
|
+
f"{axis_name} bind for value_filter key '{key}' expects each value to be "
|
|
605
|
+
"(operator, threshold) pair when op is not provided."
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
operator = value[0]
|
|
609
|
+
if not isinstance(operator, str) or operator not in VALID_VALUE_FILTER_OPERATORS:
|
|
610
|
+
valid_ops = sorted(VALID_VALUE_FILTER_OPERATORS)
|
|
611
|
+
raise ValueError(
|
|
612
|
+
f"Invalid value_filter operator '{operator}' for key '{key}'. "
|
|
613
|
+
f"Valid operators: {valid_ops}."
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
threshold = value[1]
|
|
617
|
+
return (operator, threshold)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _validate_value_filter_condition(
|
|
621
|
+
value: Any,
|
|
622
|
+
*,
|
|
623
|
+
axis_name: str,
|
|
624
|
+
key: str,
|
|
625
|
+
) -> None:
|
|
626
|
+
_normalize_value_filter_condition(value, axis_name=axis_name, key=key)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _build_title(
|
|
630
|
+
*,
|
|
631
|
+
row: int,
|
|
632
|
+
col: int,
|
|
633
|
+
row_bind: GridAxisBind | None,
|
|
634
|
+
col_bind: GridAxisBind | None,
|
|
635
|
+
row_point: _AxisPoint,
|
|
636
|
+
col_point: _AxisPoint,
|
|
637
|
+
title_template: str | None,
|
|
638
|
+
) -> str:
|
|
639
|
+
row_label = row_point.label
|
|
640
|
+
col_label = col_point.label
|
|
641
|
+
|
|
642
|
+
if title_template is None:
|
|
643
|
+
if row_bind is not None and col_bind is not None:
|
|
644
|
+
return f"{row_label} | {col_label}"
|
|
645
|
+
if row_bind is not None:
|
|
646
|
+
return row_label
|
|
647
|
+
if col_bind is not None:
|
|
648
|
+
return col_label
|
|
649
|
+
return ""
|
|
650
|
+
|
|
651
|
+
format_context = {
|
|
652
|
+
"row": row,
|
|
653
|
+
"col": col,
|
|
654
|
+
"row_label": row_label,
|
|
655
|
+
"col_label": col_label,
|
|
656
|
+
"row_value": row_point.value,
|
|
657
|
+
"col_value": col_point.value,
|
|
658
|
+
"row_key": row_bind.key if row_bind is not None else "",
|
|
659
|
+
"col_key": col_bind.key if col_bind is not None else "",
|
|
660
|
+
}
|
|
661
|
+
return _format_template(
|
|
662
|
+
template=title_template,
|
|
663
|
+
format_context=format_context,
|
|
664
|
+
template_name="title_template",
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _format_template(
|
|
669
|
+
*,
|
|
670
|
+
template: str,
|
|
671
|
+
format_context: dict[str, Any],
|
|
672
|
+
template_name: str,
|
|
673
|
+
) -> str:
|
|
674
|
+
try:
|
|
675
|
+
return template.format(**format_context)
|
|
676
|
+
except KeyError as exc:
|
|
677
|
+
key = exc.args[0]
|
|
678
|
+
available = sorted(format_context)
|
|
679
|
+
raise ValueError(
|
|
680
|
+
f"Unknown placeholder '{key}' in {template_name}. "
|
|
681
|
+
f"Available placeholders: {available}."
|
|
682
|
+
) from exc
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _bindable_plot_keys(plot: Plot) -> list[str]:
|
|
686
|
+
if is_dataclass(plot):
|
|
687
|
+
return sorted(dataclass_field.name for dataclass_field in fields(plot))
|
|
688
|
+
return sorted(name for name in dir(plot) if not name.startswith("_"))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _is_scalar_like(value: Any) -> bool:
|
|
692
|
+
return isinstance(value, (str, bytes)) or not isinstance(value, Sequence)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _is_positive_int(value: Any) -> bool:
|
|
696
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 1
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _default_label(value: Any) -> str:
|
|
700
|
+
if isinstance(value, list) and len(value) == 1:
|
|
701
|
+
return str(value[0])
|
|
702
|
+
return str(value)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
__all__ = [
|
|
706
|
+
"BindTarget",
|
|
707
|
+
"CellSelector",
|
|
708
|
+
"CellWhere",
|
|
709
|
+
"GridAxisBind",
|
|
710
|
+
"GridCellRule",
|
|
711
|
+
"ValueFilterOperator",
|
|
712
|
+
"build_bound_grid",
|
|
713
|
+
]
|