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.
Files changed (193) hide show
  1. diffract/__init__.py +52 -0
  2. diffract/configs/fast_speed_without_disk.ini +48 -0
  3. diffract/configs/hybrid.ini +74 -0
  4. diffract/configs/sqlite.ini +62 -0
  5. diffract/containers.py +405 -0
  6. diffract/core/__init__.py +23 -0
  7. diffract/core/cache/__init__.py +24 -0
  8. diffract/core/cache/containers.py +93 -0
  9. diffract/core/cache/interface.py +117 -0
  10. diffract/core/cache/redis_manager.py +464 -0
  11. diffract/core/cache/simple_manager.py +478 -0
  12. diffract/core/compute/__init__.py +46 -0
  13. diffract/core/compute/config.py +48 -0
  14. diffract/core/compute/containers.py +81 -0
  15. diffract/core/compute/decorator.py +125 -0
  16. diffract/core/compute/exceptions.py +31 -0
  17. diffract/core/compute/execution/__init__.py +14 -0
  18. diffract/core/compute/execution/_types.py +70 -0
  19. diffract/core/compute/execution/aggregation.py +74 -0
  20. diffract/core/compute/execution/aggregation_runner.py +362 -0
  21. diffract/core/compute/execution/enums.py +38 -0
  22. diffract/core/compute/execution/executor.py +125 -0
  23. diffract/core/compute/execution/parameter_runner.py +125 -0
  24. diffract/core/compute/execution/restrictions.py +61 -0
  25. diffract/core/compute/execution/strategy.py +118 -0
  26. diffract/core/compute/execution/utils.py +112 -0
  27. diffract/core/compute/extensions/__init__.py +0 -0
  28. diffract/core/compute/extensions/power_law.py +1051 -0
  29. diffract/core/compute/extensions/rmt.py +150 -0
  30. diffract/core/compute/extensions/utils.py +36 -0
  31. diffract/core/compute/field_signature.py +183 -0
  32. diffract/core/compute/kernels/__init__.py +48 -0
  33. diffract/core/compute/kernels/alignment.py +106 -0
  34. diffract/core/compute/kernels/heavy_tailed.py +394 -0
  35. diffract/core/compute/kernels/marchenko_pastur.py +99 -0
  36. diffract/core/compute/kernels/mat_decomposition.py +101 -0
  37. diffract/core/compute/kernels/mat_properties.py +53 -0
  38. diffract/core/compute/kernels/model_quality.py +27 -0
  39. diffract/core/compute/kernels/norms.py +88 -0
  40. diffract/core/compute/kernels/ranks.py +35 -0
  41. diffract/core/compute/kernels/tracy_widom.py +36 -0
  42. diffract/core/compute/metadata.py +67 -0
  43. diffract/core/compute/registry.py +485 -0
  44. diffract/core/constants.py +147 -0
  45. diffract/core/data/__init__.py +32 -0
  46. diffract/core/data/interface.py +304 -0
  47. diffract/core/data/metadata/__init__.py +15 -0
  48. diffract/core/data/metadata/containers.py +60 -0
  49. diffract/core/data/metadata/interface.py +208 -0
  50. diffract/core/data/metadata/sqlite_index.py +604 -0
  51. diffract/core/data/nn/__init__.py +43 -0
  52. diffract/core/data/nn/aggregates/__init__.py +35 -0
  53. diffract/core/data/nn/aggregates/metadata.py +96 -0
  54. diffract/core/data/nn/aggregates/proxy.py +57 -0
  55. diffract/core/data/nn/aggregates/repository.py +87 -0
  56. diffract/core/data/nn/aggregates/view.py +307 -0
  57. diffract/core/data/nn/containers.py +86 -0
  58. diffract/core/data/nn/extractors/__init__.py +49 -0
  59. diffract/core/data/nn/extractors/base.py +300 -0
  60. diffract/core/data/nn/extractors/factory.py +234 -0
  61. diffract/core/data/nn/extractors/flax.py +112 -0
  62. diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
  63. diffract/core/data/nn/extractors/handlers/base.py +110 -0
  64. diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
  65. diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
  66. diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
  67. diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
  68. diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
  69. diffract/core/data/nn/extractors/interface.py +56 -0
  70. diffract/core/data/nn/extractors/numpy.py +94 -0
  71. diffract/core/data/nn/extractors/onnx.py +108 -0
  72. diffract/core/data/nn/extractors/tensorflow.py +99 -0
  73. diffract/core/data/nn/extractors/torch.py +204 -0
  74. diffract/core/data/nn/params/__init__.py +27 -0
  75. diffract/core/data/nn/params/interface.py +402 -0
  76. diffract/core/data/nn/params/metadata.py +110 -0
  77. diffract/core/data/nn/params/proxy.py +93 -0
  78. diffract/core/data/nn/params/repository.py +45 -0
  79. diffract/core/data/nn/params/schema.py +82 -0
  80. diffract/core/data/nn/params/view.py +393 -0
  81. diffract/core/data/proxy.py +246 -0
  82. diffract/core/data/repository.py +227 -0
  83. diffract/core/data/utils.py +33 -0
  84. diffract/core/data/view.py +389 -0
  85. diffract/core/export/__init__.py +27 -0
  86. diffract/core/export/containers.py +47 -0
  87. diffract/core/export/exporters.py +191 -0
  88. diffract/core/export/formatters/__init__.py +23 -0
  89. diffract/core/export/formatters/base.py +68 -0
  90. diffract/core/export/formatters/dict_formatter.py +38 -0
  91. diffract/core/export/formatters/json_formatter.py +58 -0
  92. diffract/core/export/formatters/list_formatter.py +41 -0
  93. diffract/core/export/formatters/pandas_formatter.py +86 -0
  94. diffract/core/export/formatters/polars_formatter.py +86 -0
  95. diffract/core/export/formatters/registry.py +84 -0
  96. diffract/core/export/interface.py +105 -0
  97. diffract/core/parallel/__init__.py +21 -0
  98. diffract/core/parallel/containers.py +57 -0
  99. diffract/core/parallel/execution.py +91 -0
  100. diffract/core/parallel/runtime.py +91 -0
  101. diffract/core/storage/__init__.py +35 -0
  102. diffract/core/storage/base_manager.py +330 -0
  103. diffract/core/storage/containers.py +122 -0
  104. diffract/core/storage/hdf5_manager.py +856 -0
  105. diffract/core/storage/hybrid_manager.py +218 -0
  106. diffract/core/storage/interface.py +222 -0
  107. diffract/core/storage/metadata.py +89 -0
  108. diffract/core/storage/ram_manager.py +159 -0
  109. diffract/core/storage/sqlite_manager.py +1062 -0
  110. diffract/core/storage/zarr_manager.py +992 -0
  111. diffract/core/utils/__init__.py +45 -0
  112. diffract/core/utils/build.py +46 -0
  113. diffract/core/utils/exceptions.py +11 -0
  114. diffract/core/utils/hashing.py +90 -0
  115. diffract/core/utils/imports.py +292 -0
  116. diffract/core/utils/math.py +15 -0
  117. diffract/py.typed +0 -0
  118. diffract/session/__init__.py +24 -0
  119. diffract/session/errors.py +17 -0
  120. diffract/session/field_cache.py +216 -0
  121. diffract/session/namespaces/__init__.py +15 -0
  122. diffract/session/namespaces/compute/__init__.py +219 -0
  123. diffract/session/namespaces/models/__init__.py +282 -0
  124. diffract/session/namespaces/models/parameters/__init__.py +133 -0
  125. diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
  126. diffract/session/namespaces/results/__init__.py +378 -0
  127. diffract/session/namespaces/results/eraser.py +129 -0
  128. diffract/session/namespaces/results/injester.py +249 -0
  129. diffract/session/namespaces/utils/__init__.py +141 -0
  130. diffract/session/namespaces/utils/merger.py +295 -0
  131. diffract/session/namespaces/viz/__init__.py +91 -0
  132. diffract/session/namespaces/viz/_utils.py +14 -0
  133. diffract/session/namespaces/viz/box.py +207 -0
  134. diffract/session/namespaces/viz/grid.py +160 -0
  135. diffract/session/namespaces/viz/heatmap.py +164 -0
  136. diffract/session/namespaces/viz/scatter.py +202 -0
  137. diffract/session/namespaces/viz/sparkline.py +270 -0
  138. diffract/session/namespaces/viz/violin.py +212 -0
  139. diffract/session/session.py +260 -0
  140. diffract/session/utils.py +81 -0
  141. diffract/viz/__init__.py +42 -0
  142. diffract/viz/data/__init__.py +34 -0
  143. diffract/viz/data/detection.py +57 -0
  144. diffract/viz/data/extraction.py +117 -0
  145. diffract/viz/data/filtering.py +57 -0
  146. diffract/viz/data/ordering.py +129 -0
  147. diffract/viz/data/provider.py +99 -0
  148. diffract/viz/data/types.py +98 -0
  149. diffract/viz/plots/__init__.py +37 -0
  150. diffract/viz/plots/base/__init__.py +20 -0
  151. diffract/viz/plots/base/axis.py +219 -0
  152. diffract/viz/plots/base/coloraxis.py +133 -0
  153. diffract/viz/plots/base/configurator.py +18 -0
  154. diffract/viz/plots/base/jitter.py +368 -0
  155. diffract/viz/plots/base/line.py +197 -0
  156. diffract/viz/plots/base/marker.py +278 -0
  157. diffract/viz/plots/base/overlay.py +14 -0
  158. diffract/viz/plots/base/plot.py +110 -0
  159. diffract/viz/plots/base/update.py +50 -0
  160. diffract/viz/plots/boxplot.py +283 -0
  161. diffract/viz/plots/cluster.py +374 -0
  162. diffract/viz/plots/heatmap.py +168 -0
  163. diffract/viz/plots/scatter.py +233 -0
  164. diffract/viz/plots/sparkline.py +405 -0
  165. diffract/viz/plots/subplots/__init__.py +32 -0
  166. diffract/viz/plots/subplots/coloraxis.py +339 -0
  167. diffract/viz/plots/subplots/factory.py +713 -0
  168. diffract/viz/plots/subplots/grid.py +214 -0
  169. diffract/viz/plots/subplots/layout.py +370 -0
  170. diffract/viz/plots/subplots/spec.py +39 -0
  171. diffract/viz/plots/violin.py +298 -0
  172. diffract/viz/renderer.py +513 -0
  173. diffract/viz/styling/__init__.py +78 -0
  174. diffract/viz/styling/palettes/__init__.py +20 -0
  175. diffract/viz/styling/palettes/bundle.py +16 -0
  176. diffract/viz/styling/palettes/color.py +83 -0
  177. diffract/viz/styling/palettes/dashes.py +27 -0
  178. diffract/viz/styling/palettes/symbols.py +42 -0
  179. diffract/viz/styling/resolvers/__init__.py +11 -0
  180. diffract/viz/styling/resolvers/categorical.py +68 -0
  181. diffract/viz/styling/resolvers/color.py +77 -0
  182. diffract/viz/styling/resolvers/numeric.py +108 -0
  183. diffract/viz/styling/sources.py +27 -0
  184. diffract/viz/styling/theme/__init__.py +29 -0
  185. diffract/viz/styling/theme/applier.py +114 -0
  186. diffract/viz/styling/theme/components.py +66 -0
  187. diffract/viz/styling/theme/presets.py +58 -0
  188. diffract/viz/styling/theme/theme.py +36 -0
  189. diffract_core-0.2.1.dist-info/METADATA +530 -0
  190. diffract_core-0.2.1.dist-info/RECORD +193 -0
  191. diffract_core-0.2.1.dist-info/WHEEL +4 -0
  192. diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
  193. 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
+ ]