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,214 @@
1
+ """Grid plot composition for `viz` Plot objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from plotly.subplots import make_subplots
10
+
11
+ from diffract.viz.plots.base.plot import Plot
12
+ from diffract.viz.styling import Theme, apply_theme
13
+
14
+ from .coloraxis import (
15
+ ColoraxisRegistry,
16
+ apply_coloraxis_configs,
17
+ distribute_colorbars,
18
+ extract_coloraxis_share_keys,
19
+ extract_legacy_coloraxis_name,
20
+ remap_coloraxis,
21
+ )
22
+ from .layout import add_figure_to_subplot
23
+ from .spec import VALID_SESSION_FILTER_KEYS, VALUE_FILTER_SENTINEL, SubplotSpec
24
+
25
+ if TYPE_CHECKING:
26
+ import plotly.graph_objects as go
27
+
28
+ from diffract.session import Session
29
+
30
+
31
+ @dataclass(kw_only=True)
32
+ class GridPlot:
33
+ """Compose multiple plots into a grid layout."""
34
+
35
+ subplots: list[SubplotSpec]
36
+ make_subplots_kwargs: dict[str, Any]
37
+
38
+ def render(self, session: Session, theme: Theme | None = None) -> go.Figure:
39
+ """Render all subplot specs into a single Plotly grid figure."""
40
+ _validate_subplots(self.subplots)
41
+
42
+ total_rows = max((spec.row for spec in self.subplots), default=1)
43
+ total_cols = max((spec.col for spec in self.subplots), default=1)
44
+ sorted_specs = sorted(self.subplots, key=lambda spec: (spec.row, spec.col))
45
+ titles = _build_subplot_titles(sorted_specs, total_rows, total_cols)
46
+
47
+ kwargs = dict(self.make_subplots_kwargs or {})
48
+ for forbidden_key in ("rows", "cols", "subplot_titles"):
49
+ kwargs.pop(forbidden_key, None)
50
+
51
+ grid = make_subplots(
52
+ rows=total_rows,
53
+ cols=total_cols,
54
+ subplot_titles=titles,
55
+ **kwargs,
56
+ )
57
+
58
+ seen_legend_names: set[str] = set()
59
+ coloraxis_registry = ColoraxisRegistry()
60
+
61
+ for spec in sorted_specs:
62
+ render_session = session
63
+ resolved_session_filter = _resolve_session_filter(spec)
64
+ if resolved_session_filter:
65
+ render_session = _apply_session_filter(session, resolved_session_filter)
66
+
67
+ child_figure = _render_subplot_plot(
68
+ plot=spec.plot,
69
+ session=render_session,
70
+ theme=theme,
71
+ extra_value_filter=spec.value_filter,
72
+ )
73
+
74
+ remap_coloraxis(
75
+ child_figure,
76
+ coloraxis_registry,
77
+ explicit_share_keys=extract_coloraxis_share_keys(spec.plot),
78
+ legacy_share_name=extract_legacy_coloraxis_name(spec.plot),
79
+ )
80
+
81
+ add_figure_to_subplot(
82
+ grid,
83
+ child_figure,
84
+ row=spec.row,
85
+ col=spec.col,
86
+ transfer_layout=True,
87
+ seen_legend_names=seen_legend_names,
88
+ )
89
+
90
+ apply_coloraxis_configs(grid, coloraxis_registry)
91
+
92
+ if theme is not None:
93
+ apply_theme(grid, theme)
94
+
95
+ distribute_colorbars(grid)
96
+
97
+ return grid
98
+
99
+
100
+ def _build_subplot_titles(specs: list[SubplotSpec], rows: int, cols: int) -> list[str]:
101
+ titles = [""] * (rows * cols)
102
+ for spec in specs:
103
+ idx = (spec.row - 1) * cols + (spec.col - 1)
104
+ existing = titles[idx]
105
+ if existing and spec.title and existing != spec.title:
106
+ raise ValueError(
107
+ "Conflicting subplot titles for the same cell "
108
+ f"(row={spec.row}, col={spec.col}): '{existing}' vs '{spec.title}'."
109
+ )
110
+ if not existing and spec.title:
111
+ titles[idx] = spec.title
112
+ return titles
113
+
114
+
115
+ def _validate_subplots(subplots: list[SubplotSpec]) -> None:
116
+ for spec in subplots:
117
+ if spec.row < 1 or spec.col < 1:
118
+ raise ValueError(
119
+ "Subplot row/col must be 1-indexed positive integers; "
120
+ f"got row={spec.row}, col={spec.col}."
121
+ )
122
+
123
+ filter_dict = _resolve_session_filter(spec)
124
+ if filter_dict:
125
+ invalid_keys = sorted(set(filter_dict) - VALID_SESSION_FILTER_KEYS)
126
+ if invalid_keys:
127
+ valid_keys = sorted(VALID_SESSION_FILTER_KEYS)
128
+ raise ValueError(
129
+ f"Invalid filter keys {invalid_keys} in subplot filter. "
130
+ f"Valid keys are: {valid_keys}"
131
+ )
132
+
133
+
134
+ def _resolve_session_filter(spec: SubplotSpec) -> dict[str, Any] | None:
135
+ if spec.session_filter is None:
136
+ return spec.filter
137
+ if spec.filter is None:
138
+ return spec.session_filter
139
+
140
+ merged = dict(spec.filter)
141
+ merged.update(spec.session_filter)
142
+ return merged
143
+
144
+
145
+ def _render_subplot_plot(
146
+ *,
147
+ plot: Plot,
148
+ session: Session,
149
+ theme: Theme | None,
150
+ extra_value_filter: dict[str, tuple[str, Any]] | None,
151
+ ) -> go.Figure:
152
+ original_value_filter = getattr(plot, "value_filter", VALUE_FILTER_SENTINEL)
153
+
154
+ if original_value_filter is not VALUE_FILTER_SENTINEL:
155
+ merged = _merge_value_filters(original_value_filter, extra_value_filter)
156
+ plot.value_filter = merged
157
+
158
+ try:
159
+ return _render_plot_with_optional_theme(plot, session, theme)
160
+ finally:
161
+ if original_value_filter is not VALUE_FILTER_SENTINEL:
162
+ plot.value_filter = original_value_filter
163
+
164
+
165
+ def _render_plot_with_optional_theme(
166
+ plot: Plot,
167
+ session: Session,
168
+ theme: Theme | None,
169
+ ) -> go.Figure:
170
+ if theme is None:
171
+ return plot.render(session)
172
+
173
+ if _plot_supports_theme_kwarg(plot):
174
+ return plot.render(session, theme=theme)
175
+ return plot.render(session)
176
+
177
+
178
+ def _plot_supports_theme_kwarg(plot: Plot) -> bool:
179
+ try:
180
+ signature = inspect.signature(plot.render)
181
+ except (TypeError, ValueError):
182
+ return True
183
+
184
+ for parameter in signature.parameters.values():
185
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
186
+ return True
187
+ if parameter.name == "theme":
188
+ return True
189
+
190
+ return False
191
+
192
+
193
+ def _merge_value_filters(
194
+ base: dict[str, tuple[str, Any]] | None,
195
+ extra: dict[str, tuple[str, Any]] | None,
196
+ ) -> dict[str, tuple[str, Any]] | None:
197
+ if base is None and extra is None:
198
+ return None
199
+
200
+ merged: dict[str, tuple[str, Any]] = {}
201
+ if base:
202
+ merged.update(base)
203
+ if extra:
204
+ merged.update(extra)
205
+ return merged
206
+
207
+
208
+ def _apply_session_filter(session: Session, filter_dict: dict[str, Any]) -> Session:
209
+ return session.filter(
210
+ param_ids=filter_dict.get("param_ids"),
211
+ param_names=filter_dict.get("param_names"),
212
+ param_types=filter_dict.get("param_types"),
213
+ model_ids=filter_dict.get("model_ids"),
214
+ )
@@ -0,0 +1,370 @@
1
+ """Layout transfer helpers for subplot composition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ import plotly.graph_objects as go
10
+
11
+ _AXIS_REF_RE = re.compile(r"^(?P<axis>[xy])(?P<id>\d+)?(?P<domain>\s+domain)?$")
12
+
13
+
14
+ def add_figure_to_subplot(
15
+ fig_parent: go.Figure,
16
+ child_fig: go.Figure,
17
+ *,
18
+ row: int,
19
+ col: int,
20
+ transfer_layout: bool,
21
+ seen_legend_names: set[str] | None = None,
22
+ ) -> None:
23
+ """Add traces from a child figure into a specific subplot cell."""
24
+ if seen_legend_names is None:
25
+ seen_legend_names = set()
26
+
27
+ axis_refs = _get_subplot_axis_refs(fig_parent, row=row, col=col)
28
+ overlay_xaxis_map: dict[str, str] = {}
29
+ if axis_refs is not None:
30
+ x_ref, y_ref = axis_refs
31
+ overlay_xaxis_map = _register_overlay_xaxes(
32
+ fig_parent,
33
+ child_fig,
34
+ x_ref=x_ref,
35
+ y_ref=y_ref,
36
+ )
37
+
38
+ for trace in child_fig.data:
39
+ source_xaxis = getattr(trace, "xaxis", None)
40
+ source_yaxis = getattr(trace, "yaxis", None)
41
+
42
+ name = getattr(trace, "name", None)
43
+ if name and name in seen_legend_names:
44
+ trace.showlegend = False
45
+ if not getattr(trace, "legendgroup", None):
46
+ trace.legendgroup = name
47
+ elif name:
48
+ seen_legend_names.add(name)
49
+ if not getattr(trace, "legendgroup", None):
50
+ trace.legendgroup = name
51
+
52
+ fig_parent.add_trace(trace, row=row, col=col)
53
+
54
+ if axis_refs is None:
55
+ continue
56
+
57
+ mapped_xaxis = _remap_trace_axis_reference(
58
+ source_xaxis,
59
+ axis="x",
60
+ primary_axis=x_ref,
61
+ overlay_map=overlay_xaxis_map,
62
+ )
63
+ mapped_yaxis = _remap_trace_axis_reference(
64
+ source_yaxis,
65
+ axis="y",
66
+ primary_axis=y_ref,
67
+ )
68
+
69
+ fig_parent.data[-1].update(xaxis=mapped_xaxis, yaxis=mapped_yaxis)
70
+
71
+ if transfer_layout:
72
+ transfer_layout_to_subplot(fig_parent, child_fig, row=row, col=col)
73
+
74
+
75
+ def transfer_layout_to_subplot(
76
+ fig_parent: go.Figure,
77
+ child_fig: go.Figure,
78
+ *,
79
+ row: int,
80
+ col: int,
81
+ ) -> None:
82
+ """Transfer axis/layout overlays from child to the target subplot cell."""
83
+ axis_refs = _get_subplot_axis_refs(fig_parent, row=row, col=col)
84
+ if axis_refs is None:
85
+ return
86
+ x_ref, y_ref = axis_refs
87
+
88
+ _transfer_axis_props(fig_parent, child_fig, row=row, col=col)
89
+ _transfer_shapes(fig_parent, child_fig, x_ref=x_ref, y_ref=y_ref)
90
+ _transfer_annotations(fig_parent, child_fig, x_ref=x_ref, y_ref=y_ref)
91
+ _transfer_images(fig_parent, child_fig, x_ref=x_ref, y_ref=y_ref)
92
+
93
+
94
+ def _transfer_axis_props(
95
+ fig_parent: go.Figure,
96
+ child_fig: go.Figure,
97
+ *,
98
+ row: int,
99
+ col: int,
100
+ ) -> None:
101
+ axis_props = [
102
+ "tickmode",
103
+ "tickvals",
104
+ "ticktext",
105
+ "tickangle",
106
+ "tickformat",
107
+ "tickprefix",
108
+ "ticksuffix",
109
+ "showticklabels",
110
+ "ticks",
111
+ "tickfont",
112
+ "tickcolor",
113
+ "ticklen",
114
+ "tickwidth",
115
+ "showgrid",
116
+ "gridcolor",
117
+ "gridwidth",
118
+ "zeroline",
119
+ "zerolinecolor",
120
+ "zerolinewidth",
121
+ "type",
122
+ "range",
123
+ "autorange",
124
+ "categoryorder",
125
+ "categoryarray",
126
+ "title",
127
+ "titlefont",
128
+ "showline",
129
+ "linecolor",
130
+ "linewidth",
131
+ "mirror",
132
+ "dtick",
133
+ "tick0",
134
+ "rangemode",
135
+ "fixedrange",
136
+ "automargin",
137
+ ]
138
+
139
+ for axis_type, child_axis in (
140
+ ("x", child_fig.layout.xaxis),
141
+ ("y", child_fig.layout.yaxis),
142
+ ):
143
+ if child_axis is None:
144
+ continue
145
+
146
+ props = {
147
+ key: getattr(child_axis, key)
148
+ for key in axis_props
149
+ if getattr(child_axis, key, None) is not None
150
+ }
151
+ if not props:
152
+ continue
153
+
154
+ if axis_type == "x":
155
+ fig_parent.update_xaxes(row=row, col=col, **props)
156
+ else:
157
+ fig_parent.update_yaxes(row=row, col=col, **props)
158
+
159
+
160
+ def _transfer_shapes(
161
+ fig_parent: go.Figure,
162
+ child_fig: go.Figure,
163
+ *,
164
+ x_ref: str,
165
+ y_ref: str,
166
+ ) -> None:
167
+ if not child_fig.layout.shapes:
168
+ return
169
+
170
+ new_shapes: list[dict[str, Any]] = []
171
+ for shape in child_fig.layout.shapes:
172
+ shape_dict = shape.to_plotly_json()
173
+ _remap_xy_refs(shape_dict, x_ref=x_ref, y_ref=y_ref)
174
+ new_shapes.append(shape_dict)
175
+
176
+ current_shapes = list(fig_parent.layout.shapes or [])
177
+ fig_parent.update_layout(shapes=current_shapes + new_shapes)
178
+
179
+
180
+ def _transfer_annotations(
181
+ fig_parent: go.Figure,
182
+ child_fig: go.Figure,
183
+ *,
184
+ x_ref: str,
185
+ y_ref: str,
186
+ ) -> None:
187
+ if not child_fig.layout.annotations:
188
+ return
189
+
190
+ new_annotations: list[dict[str, Any]] = []
191
+ for annotation in child_fig.layout.annotations:
192
+ annotation_dict = annotation.to_plotly_json()
193
+ _remap_xy_refs(annotation_dict, x_ref=x_ref, y_ref=y_ref)
194
+ new_annotations.append(annotation_dict)
195
+
196
+ current_annotations = list(fig_parent.layout.annotations or [])
197
+ fig_parent.update_layout(annotations=current_annotations + new_annotations)
198
+
199
+
200
+ def _transfer_images(
201
+ fig_parent: go.Figure,
202
+ child_fig: go.Figure,
203
+ *,
204
+ x_ref: str,
205
+ y_ref: str,
206
+ ) -> None:
207
+ if not child_fig.layout.images:
208
+ return
209
+
210
+ new_images: list[dict[str, Any]] = []
211
+ for image in child_fig.layout.images:
212
+ image_dict = image.to_plotly_json()
213
+ _remap_xy_refs(image_dict, x_ref=x_ref, y_ref=y_ref)
214
+ new_images.append(image_dict)
215
+
216
+ current_images = list(fig_parent.layout.images or [])
217
+ fig_parent.update_layout(images=current_images + new_images)
218
+
219
+
220
+ def _remap_xy_refs(item: dict[str, Any], *, x_ref: str, y_ref: str) -> None:
221
+ x_value = item.get("xref")
222
+ y_value = item.get("yref")
223
+ ax_value = item.get("axref")
224
+ ay_value = item.get("ayref")
225
+
226
+ remapped_x = _remap_axis_reference(x_value, axis="x", target_axis=x_ref)
227
+ remapped_y = _remap_axis_reference(y_value, axis="y", target_axis=y_ref)
228
+ remapped_ax = _remap_axis_reference(ax_value, axis="x", target_axis=x_ref)
229
+ remapped_ay = _remap_axis_reference(ay_value, axis="y", target_axis=y_ref)
230
+
231
+ if remapped_x is not None:
232
+ item["xref"] = remapped_x
233
+ if remapped_y is not None:
234
+ item["yref"] = remapped_y
235
+ if remapped_ax is not None:
236
+ item["axref"] = remapped_ax
237
+ if remapped_ay is not None:
238
+ item["ayref"] = remapped_ay
239
+
240
+
241
+ def _remap_axis_reference(
242
+ value: Any,
243
+ *,
244
+ axis: str,
245
+ target_axis: str,
246
+ ) -> str | None:
247
+ if not isinstance(value, str):
248
+ return None
249
+
250
+ match = _AXIS_REF_RE.fullmatch(value.strip())
251
+ if match is None or match.group("axis") != axis:
252
+ return value
253
+
254
+ domain = match.group("domain") or ""
255
+ return f"{target_axis}{domain}"
256
+
257
+
258
+ def _get_subplot_axis_refs(
259
+ fig_parent: go.Figure,
260
+ *,
261
+ row: int,
262
+ col: int,
263
+ ) -> tuple[str, str] | None:
264
+ """Get parent subplot axis references (for example ``('x3', 'y3')``)."""
265
+ grid_refs = fig_parent._grid_ref[row - 1][col - 1]
266
+ if not grid_refs:
267
+ return None
268
+
269
+ grid_ref = grid_refs[0]
270
+ remap_ref = getattr(grid_ref, "trace_kwargs", None)
271
+ if not isinstance(remap_ref, dict):
272
+ return None
273
+
274
+ x_ref = remap_ref.get("xaxis")
275
+ y_ref = remap_ref.get("yaxis")
276
+ if not isinstance(x_ref, str) or not isinstance(y_ref, str):
277
+ return None
278
+
279
+ return x_ref, y_ref
280
+
281
+
282
+ def _register_overlay_xaxes(
283
+ fig_parent: go.Figure,
284
+ child_fig: go.Figure,
285
+ *,
286
+ x_ref: str,
287
+ y_ref: str,
288
+ ) -> dict[str, str]:
289
+ """Create parent overlay x-axes for child overlay axes (x2, x3, ...)."""
290
+ layout_dict = child_fig.layout.to_plotly_json()
291
+ overlay_map: dict[str, str] = {}
292
+
293
+ for layout_key, axis_cfg in layout_dict.items():
294
+ if not layout_key.startswith("xaxis") or layout_key == "xaxis":
295
+ continue
296
+ if not isinstance(axis_cfg, dict):
297
+ continue
298
+
299
+ suffix = layout_key[5:]
300
+ child_axis = "x" if not suffix else f"x{suffix}"
301
+ overlaying = axis_cfg.get("overlaying")
302
+ if overlaying not in {"x", "x1"}:
303
+ continue
304
+
305
+ parent_axis = _next_axis_ref(fig_parent, axis="x")
306
+ parent_layout_key = _axis_layout_key(parent_axis)
307
+ parent_cfg = dict(axis_cfg)
308
+ parent_cfg["overlaying"] = x_ref
309
+ parent_cfg["anchor"] = y_ref
310
+ parent_cfg.pop("domain", None)
311
+
312
+ fig_parent.update_layout(**{parent_layout_key: parent_cfg})
313
+ overlay_map[child_axis] = parent_axis
314
+
315
+ return overlay_map
316
+
317
+
318
+ def _remap_trace_axis_reference(
319
+ value: Any,
320
+ *,
321
+ axis: str,
322
+ primary_axis: str,
323
+ overlay_map: dict[str, str] | None = None,
324
+ ) -> str:
325
+ """Map a child trace axis reference to parent subplot axis reference."""
326
+ if not isinstance(value, str):
327
+ return primary_axis
328
+
329
+ match = _AXIS_REF_RE.fullmatch(value.strip())
330
+ if match is None or match.group("axis") != axis:
331
+ return primary_axis
332
+
333
+ axis_id = match.group("id") or ""
334
+ child_axis = f"{axis}{axis_id}"
335
+
336
+ if overlay_map is not None and child_axis in overlay_map:
337
+ return overlay_map[child_axis]
338
+
339
+ if child_axis in {axis, f"{axis}1"}:
340
+ return primary_axis
341
+
342
+ return child_axis
343
+
344
+
345
+ def _next_axis_ref(fig_parent: go.Figure, *, axis: str) -> str:
346
+ """Get next available axis reference in parent layout."""
347
+ max_id = 0
348
+ layout_dict = fig_parent.layout.to_plotly_json()
349
+ axis_prefix = f"{axis}axis"
350
+
351
+ for layout_key in layout_dict:
352
+ if layout_key == axis_prefix:
353
+ max_id = max(max_id, 1)
354
+ continue
355
+ if not layout_key.startswith(axis_prefix):
356
+ continue
357
+
358
+ suffix = layout_key[len(axis_prefix) :]
359
+ if suffix.isdigit():
360
+ max_id = max(max_id, int(suffix))
361
+
362
+ next_id = max_id + 1
363
+ return axis if next_id == 1 else f"{axis}{next_id}"
364
+
365
+
366
+ def _axis_layout_key(axis_ref: str) -> str:
367
+ """Convert axis ref like ``x2`` to layout key like ``xaxis2``."""
368
+ axis = axis_ref[0]
369
+ suffix = axis_ref[1:]
370
+ return f"{axis}axis{suffix}"
@@ -0,0 +1,39 @@
1
+ """Subplot specifications and shared constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from diffract.viz.plots.base.plot import Plot
9
+
10
+ VALID_SESSION_FILTER_KEYS = frozenset(
11
+ {"param_ids", "param_names", "param_types", "model_ids"}
12
+ )
13
+ VALUE_FILTER_SENTINEL = object()
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class SubplotSpec:
18
+ """Specification for a single subplot in a `GridPlot`.
19
+
20
+ Args:
21
+ row: Row position (1-indexed).
22
+ col: Column position (1-indexed).
23
+ title: Title for this subplot.
24
+ plot: Plot object to render.
25
+ session_filter: Optional dict forwarded to `Session.filter(...)`.
26
+ Valid keys: param_ids, param_names, param_types, model_ids.
27
+ value_filter: Optional value filter merged into `plot.value_filter` for
28
+ this subplot only: {field: (op, threshold)}.
29
+ filter: Backward-compatible alias for `session_filter`.
30
+ """
31
+
32
+ row: int
33
+ col: int
34
+ title: str
35
+ plot: Plot
36
+
37
+ session_filter: dict[str, Any] | None = None
38
+ value_filter: dict[str, tuple[str, Any]] | None = None
39
+ filter: dict[str, Any] | None = None