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,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OrderMode(Enum):
|
|
13
|
+
"""How to order categorical values."""
|
|
14
|
+
|
|
15
|
+
AS_IS = auto()
|
|
16
|
+
LEXICOGRAPHIC = auto()
|
|
17
|
+
NUMERIC = auto()
|
|
18
|
+
MAP = auto()
|
|
19
|
+
CUSTOM = auto()
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_string(cls, name: str) -> OrderMode:
|
|
23
|
+
"""Return the OrderMode matching the given name (case-insensitive)."""
|
|
24
|
+
match name.lower():
|
|
25
|
+
case "as_is":
|
|
26
|
+
return cls.AS_IS
|
|
27
|
+
case "lexicographic":
|
|
28
|
+
return cls.LEXICOGRAPHIC
|
|
29
|
+
case "numeric":
|
|
30
|
+
return cls.NUMERIC
|
|
31
|
+
case "map":
|
|
32
|
+
return cls.MAP
|
|
33
|
+
case "custom":
|
|
34
|
+
return cls.CUSTOM
|
|
35
|
+
case _:
|
|
36
|
+
raise ValueError(f"Unknown order mode: {name!r}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(kw_only=True)
|
|
40
|
+
class Ordering:
|
|
41
|
+
"""Defines how to order values.
|
|
42
|
+
|
|
43
|
+
Returns indices (like np.argsort) so the same ordering
|
|
44
|
+
can be applied to multiple related arrays.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
mode: OrderMode = OrderMode.AS_IS
|
|
48
|
+
descending: bool = False
|
|
49
|
+
|
|
50
|
+
# For MAP mode: function to extract comparison key
|
|
51
|
+
key: Callable[[Any], Any] | None = None
|
|
52
|
+
|
|
53
|
+
# For CUSTOM mode: explicit order of values
|
|
54
|
+
custom_order: Sequence[Any] | None = None
|
|
55
|
+
|
|
56
|
+
def argsort(self, values: Sequence[Any]) -> NDArray[np.intp]:
|
|
57
|
+
"""Return indices that would sort unique values.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
values: Sequence of values to order.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Array of indices into unique values that produces the desired order.
|
|
64
|
+
Use np.unique(..., return_inverse=True) to map back to original data.
|
|
65
|
+
"""
|
|
66
|
+
unique = list(dict.fromkeys(values)) # preserve first occurrence order
|
|
67
|
+
|
|
68
|
+
if self.mode == OrderMode.AS_IS:
|
|
69
|
+
indices = np.arange(len(unique))
|
|
70
|
+
|
|
71
|
+
elif self.mode == OrderMode.LEXICOGRAPHIC:
|
|
72
|
+
indices = np.argsort([str(v) for v in unique])
|
|
73
|
+
|
|
74
|
+
elif self.mode == OrderMode.NUMERIC:
|
|
75
|
+
indices = np.argsort(
|
|
76
|
+
[float(v) if v is not None else float("-inf") for v in unique]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
elif self.mode == OrderMode.MAP:
|
|
80
|
+
if self.key is None:
|
|
81
|
+
indices = np.arange(len(unique))
|
|
82
|
+
else:
|
|
83
|
+
indices = np.argsort([self.key(v) for v in unique])
|
|
84
|
+
|
|
85
|
+
elif True:
|
|
86
|
+
if self.custom_order is None:
|
|
87
|
+
indices = np.arange(len(unique))
|
|
88
|
+
else:
|
|
89
|
+
order_map = {v: i for i, v in enumerate(self.custom_order)}
|
|
90
|
+
max_idx = len(order_map)
|
|
91
|
+
# Values not in custom_order go to the end
|
|
92
|
+
priorities = [order_map.get(v, max_idx) for v in unique]
|
|
93
|
+
indices = np.argsort(priorities)
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
indices = np.arange(len(unique))
|
|
97
|
+
|
|
98
|
+
if self.descending:
|
|
99
|
+
indices = indices[::-1].copy()
|
|
100
|
+
|
|
101
|
+
return indices
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Convenience constructors
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def as_is() -> Ordering:
|
|
108
|
+
"""Keep original order from data."""
|
|
109
|
+
return Ordering(mode=OrderMode.AS_IS)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def lexicographic(descending: bool = False) -> Ordering:
|
|
113
|
+
"""Sort alphabetically by string representation."""
|
|
114
|
+
return Ordering(mode=OrderMode.LEXICOGRAPHIC, descending=descending)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def numeric(descending: bool = False) -> Ordering:
|
|
118
|
+
"""Sort by numeric value."""
|
|
119
|
+
return Ordering(mode=OrderMode.NUMERIC, descending=descending)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def by_key(key: Callable[[Any], Any], descending: bool = False) -> Ordering:
|
|
123
|
+
"""Sort by applying key function to each value."""
|
|
124
|
+
return Ordering(mode=OrderMode.MAP, key=key, descending=descending)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def custom(order: Sequence[Any], descending: bool = False) -> Ordering:
|
|
128
|
+
"""Sort by explicit order. Values not in order go to the end."""
|
|
129
|
+
return Ordering(mode=OrderMode.CUSTOM, custom_order=order, descending=descending)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from .extraction import get_field_data
|
|
6
|
+
from .filtering import apply_filter
|
|
7
|
+
from .types import DataShape, DataType, Entry
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from diffract.session import Session
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataProvider:
|
|
14
|
+
"""Data provider for visualization.
|
|
15
|
+
|
|
16
|
+
Fetches data from session and provides typed field access.
|
|
17
|
+
Always fetches aggregates merged with entries.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session: Session) -> None:
|
|
21
|
+
self._session = session
|
|
22
|
+
self._entries: dict[str, Entry] = {}
|
|
23
|
+
self._cached_fields: set[str] = set()
|
|
24
|
+
|
|
25
|
+
def fetch(
|
|
26
|
+
self,
|
|
27
|
+
fields: list[str],
|
|
28
|
+
*,
|
|
29
|
+
value_filter: dict[str, tuple[str, Any]] | None = None,
|
|
30
|
+
) -> dict[str, Entry]:
|
|
31
|
+
"""Fetch data from session with aggregates merged.
|
|
32
|
+
|
|
33
|
+
Uses unified export API that handles aggregate merge internally.
|
|
34
|
+
Always expands contextual fields into entries.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
fields: Computed field names to fetch.
|
|
38
|
+
value_filter: Optional filter {field: (op, threshold)}.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Entries dict: {uid: Entry}
|
|
42
|
+
"""
|
|
43
|
+
fields_to_fetch = set(fields)
|
|
44
|
+
if value_filter:
|
|
45
|
+
fields_to_fetch.update(value_filter.keys())
|
|
46
|
+
|
|
47
|
+
missing_fields = fields_to_fetch - self._cached_fields
|
|
48
|
+
if missing_fields:
|
|
49
|
+
update = self._session.results.export(
|
|
50
|
+
*sorted(missing_fields),
|
|
51
|
+
sources="all",
|
|
52
|
+
export_format="dict",
|
|
53
|
+
expand_contextual=True,
|
|
54
|
+
)
|
|
55
|
+
self._merge_entries(update)
|
|
56
|
+
self._cached_fields.update(missing_fields)
|
|
57
|
+
|
|
58
|
+
if value_filter:
|
|
59
|
+
return apply_filter(self._entries, value_filter)
|
|
60
|
+
|
|
61
|
+
return self._entries
|
|
62
|
+
|
|
63
|
+
def get_field_data(self, field: str) -> tuple[list[Any], DataType, DataShape]:
|
|
64
|
+
"""Fetch a field and return its values, data type, and data shape.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
field: Computed field name to fetch.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (values, data type, data shape).
|
|
71
|
+
"""
|
|
72
|
+
return get_field_data(self.fetch(fields=[field]), field)
|
|
73
|
+
|
|
74
|
+
def _merge_entries(self, update: dict[str, Any]) -> None:
|
|
75
|
+
for uid, item in update.items():
|
|
76
|
+
fields = item.get("fields")
|
|
77
|
+
metadata = item.get("metadata")
|
|
78
|
+
|
|
79
|
+
if not isinstance(fields, dict):
|
|
80
|
+
raise TypeError(
|
|
81
|
+
f"Expected 'fields' mapping for entry '{uid}', "
|
|
82
|
+
f"got {type(fields).__name__}"
|
|
83
|
+
)
|
|
84
|
+
if not isinstance(metadata, dict):
|
|
85
|
+
raise TypeError(
|
|
86
|
+
f"Expected 'metadata' mapping for entry '{uid}', "
|
|
87
|
+
f"got {type(metadata).__name__}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
merged_fields = dict(fields)
|
|
91
|
+
merged_fields.update(metadata)
|
|
92
|
+
|
|
93
|
+
existing = self._entries.get(uid)
|
|
94
|
+
if existing is not None:
|
|
95
|
+
existing_fields = existing.get("fields", {})
|
|
96
|
+
if isinstance(existing_fields, dict):
|
|
97
|
+
merged_fields = dict(existing_fields) | merged_fields
|
|
98
|
+
|
|
99
|
+
self._entries[uid] = Entry(fields=merged_fields)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
6
|
+
|
|
7
|
+
from .ordering import as_is
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .ordering import Ordering
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Entry(TypedDict, total=False):
|
|
14
|
+
"""Single data entry from session results."""
|
|
15
|
+
|
|
16
|
+
fields: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataType(Enum):
|
|
20
|
+
"""Whether a field's values are numeric or categorical."""
|
|
21
|
+
|
|
22
|
+
NUMERIC = auto()
|
|
23
|
+
CATEGORICAL = auto()
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_string(cls, name: str) -> DataType:
|
|
27
|
+
"""Return the DataType matching the given name (case-insensitive)."""
|
|
28
|
+
match name.lower():
|
|
29
|
+
case "numeric":
|
|
30
|
+
return DataType.NUMERIC
|
|
31
|
+
case "categorical":
|
|
32
|
+
return DataType.CATEGORICAL
|
|
33
|
+
case _:
|
|
34
|
+
raise ValueError(f"Unknown data type: {name!r}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DataShape(Enum):
|
|
38
|
+
"""Whether a field holds a scalar or a vector per entry."""
|
|
39
|
+
|
|
40
|
+
SCALAR = auto()
|
|
41
|
+
VECTOR = auto()
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_string(cls, name: str) -> DataShape:
|
|
45
|
+
"""Return the DataShape matching the given name (case-insensitive)."""
|
|
46
|
+
match name.lower():
|
|
47
|
+
case "scalar":
|
|
48
|
+
return DataShape.SCALAR
|
|
49
|
+
case "vector":
|
|
50
|
+
return DataShape.VECTOR
|
|
51
|
+
case _:
|
|
52
|
+
raise ValueError(f"Unknown data shape: {name!r}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class FieldRef:
|
|
57
|
+
"""Reference to a data field with optional ordering.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
field: Name of the field to reference.
|
|
61
|
+
ordering: How to order values of this field. Defaults to AS_IS (no reordering).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
field: str
|
|
65
|
+
data_type: DataType | None = field(default=None)
|
|
66
|
+
data_shape: DataShape | None = field(default=None)
|
|
67
|
+
ordering: Ordering = field(default_factory=as_is)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class FieldMeta:
|
|
72
|
+
"""Metadata about a field's type and shape."""
|
|
73
|
+
|
|
74
|
+
data_type: DataType
|
|
75
|
+
data_shape: DataShape
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class EntryContext:
|
|
80
|
+
"""Entry fields plus extracted model and parameter context."""
|
|
81
|
+
|
|
82
|
+
fields: dict[str, Any]
|
|
83
|
+
model_id: Any
|
|
84
|
+
parameter_name: Any
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_entry(cls, entry: Entry) -> EntryContext:
|
|
88
|
+
"""Create EntryContext from a session entry."""
|
|
89
|
+
entry_fields = entry.get("fields", {})
|
|
90
|
+
if not isinstance(entry_fields, dict):
|
|
91
|
+
raise TypeError(
|
|
92
|
+
f"Entry 'fields' must be a mapping, got {type(entry_fields).__name__}"
|
|
93
|
+
)
|
|
94
|
+
return cls(
|
|
95
|
+
fields=entry_fields,
|
|
96
|
+
model_id=entry_fields.get("model_id"),
|
|
97
|
+
parameter_name=entry_fields.get("name"),
|
|
98
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Built-in plots for `diffract.viz`.
|
|
2
|
+
|
|
3
|
+
This module mirrors (approximately) the public surface of `diffract.viz.plots`
|
|
4
|
+
for the refactored plotting stack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import plotly
|
|
9
|
+
except ImportError as e:
|
|
10
|
+
_msg = (
|
|
11
|
+
"diffract.viz requires the viz extra. "
|
|
12
|
+
'Install it with: pip install "diffract-core[viz]"'
|
|
13
|
+
)
|
|
14
|
+
raise ImportError(_msg) from e
|
|
15
|
+
|
|
16
|
+
from .base import UpdateFigure
|
|
17
|
+
from .boxplot import BoxPlot
|
|
18
|
+
from .cluster import ClusterBarChart
|
|
19
|
+
from .heatmap import HeatmapPlot
|
|
20
|
+
from .scatter import ScatterPlot
|
|
21
|
+
from .sparkline import SparklinePlot
|
|
22
|
+
from .subplots import GridPlot, SubplotSpec
|
|
23
|
+
from .violin import ViolinPlot
|
|
24
|
+
|
|
25
|
+
Sparkline = SparklinePlot
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BoxPlot",
|
|
29
|
+
"ClusterBarChart",
|
|
30
|
+
"GridPlot",
|
|
31
|
+
"HeatmapPlot",
|
|
32
|
+
"ScatterPlot",
|
|
33
|
+
"Sparkline",
|
|
34
|
+
"SparklinePlot",
|
|
35
|
+
"SubplotSpec",
|
|
36
|
+
"ViolinPlot",
|
|
37
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Base plot components and mixins."""
|
|
2
|
+
|
|
3
|
+
from .axis import AxisType, SupportsAxis
|
|
4
|
+
from .coloraxis import SupportsColoraxis
|
|
5
|
+
from .configurator import Configurator
|
|
6
|
+
from .jitter import SupportsJitter, density_scaled_jitter
|
|
7
|
+
from .marker import SupportsMarker
|
|
8
|
+
from .plot import Plot
|
|
9
|
+
from .update import UpdateFigure
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AxisType",
|
|
13
|
+
"Configurator",
|
|
14
|
+
"Plot",
|
|
15
|
+
"SupportsAxis",
|
|
16
|
+
"SupportsColoraxis",
|
|
17
|
+
"SupportsJitter",
|
|
18
|
+
"SupportsMarker",
|
|
19
|
+
"density_scaled_jitter",
|
|
20
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import field, make_dataclass
|
|
4
|
+
from enum import Flag, auto
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import plotly.graph_objects as go
|
|
8
|
+
|
|
9
|
+
from .configurator import Configurator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AxisType(Flag):
|
|
13
|
+
"""Bit flags describing which axis modes an axis supports."""
|
|
14
|
+
|
|
15
|
+
NUMERIC = auto()
|
|
16
|
+
CATEGORICAL = auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
AxisMode = Literal["numeric", "categorical"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def SupportsAxis(axis_name: str, axis_type: AxisType) -> type[Configurator]: # noqa: N802 factory returns a class
|
|
23
|
+
"""Factory that creates axis configurator mixins."""
|
|
24
|
+
has_numeric = AxisType.NUMERIC in axis_type
|
|
25
|
+
has_categorical = AxisType.CATEGORICAL in axis_type
|
|
26
|
+
has_both = has_numeric and has_categorical
|
|
27
|
+
|
|
28
|
+
fields = _common_fields(axis_name)
|
|
29
|
+
|
|
30
|
+
if has_numeric:
|
|
31
|
+
fields += _numeric_fields(axis_name)
|
|
32
|
+
if has_categorical:
|
|
33
|
+
fields += _categorical_fields(axis_name)
|
|
34
|
+
if has_both:
|
|
35
|
+
fields += _mode_fields(axis_name)
|
|
36
|
+
|
|
37
|
+
def configure(self: Any, fig: go.Figure) -> None:
|
|
38
|
+
kwargs: dict[str, Any] = {}
|
|
39
|
+
|
|
40
|
+
_configure_common(self, axis_name, kwargs)
|
|
41
|
+
|
|
42
|
+
if has_both:
|
|
43
|
+
mode = getattr(self, f"{axis_name}_axis_mode")
|
|
44
|
+
if mode is None:
|
|
45
|
+
mode = getattr(self, f"_{axis_name}_resolved_mode", "categorical")
|
|
46
|
+
if mode == "numeric":
|
|
47
|
+
_configure_numeric(self, axis_name, kwargs)
|
|
48
|
+
else:
|
|
49
|
+
_configure_categorical(self, axis_name, kwargs)
|
|
50
|
+
elif has_numeric:
|
|
51
|
+
_configure_numeric(self, axis_name, kwargs)
|
|
52
|
+
elif has_categorical:
|
|
53
|
+
_configure_categorical(self, axis_name, kwargs)
|
|
54
|
+
|
|
55
|
+
fig.update_layout(**{f"{axis_name}axis": kwargs})
|
|
56
|
+
|
|
57
|
+
return make_dataclass(
|
|
58
|
+
f"SupportsAxis_{axis_name}",
|
|
59
|
+
fields=fields,
|
|
60
|
+
bases=(Configurator,),
|
|
61
|
+
namespace={
|
|
62
|
+
"axis_name": axis_name,
|
|
63
|
+
"axis_type": axis_type,
|
|
64
|
+
"configure": configure,
|
|
65
|
+
},
|
|
66
|
+
kw_only=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- Field definitions ---
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _common_fields(axis_name: str) -> list[tuple[str, type, Any]]:
|
|
74
|
+
return [
|
|
75
|
+
(f"{axis_name}_title", str | None, field(default=None)),
|
|
76
|
+
(f"{axis_name}_showticklabels", bool, field(default=True)),
|
|
77
|
+
(f"{axis_name}_tickangle", int | None, field(default=None)),
|
|
78
|
+
(f"{axis_name}_tickfont_size", int | None, field(default=None)),
|
|
79
|
+
(f"{axis_name}_tickfont_family", str | None, field(default=None)),
|
|
80
|
+
(f"{axis_name}_tickfont_color", str | None, field(default=None)),
|
|
81
|
+
(f"{axis_name}_showgrid", bool, field(default=True)),
|
|
82
|
+
(f"{axis_name}_gridcolor", str | None, field(default=None)),
|
|
83
|
+
(f"{axis_name}_showline", bool, field(default=True)),
|
|
84
|
+
(f"{axis_name}_linecolor", str | None, field(default=None)),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _numeric_fields(axis_name: str) -> list[tuple[str, type, Any]]:
|
|
89
|
+
return [
|
|
90
|
+
(f"{axis_name}_range", tuple[float, float] | None, field(default=None)),
|
|
91
|
+
(f"{axis_name}_dtick", float | str | None, field(default=None)),
|
|
92
|
+
(f"{axis_name}_tick0", float | None, field(default=None)),
|
|
93
|
+
(f"{axis_name}_tickformat", str | None, field(default=None)),
|
|
94
|
+
(f"{axis_name}_tickmode", str | None, field(default=None)),
|
|
95
|
+
(f"{axis_name}_tickvals", list[float] | None, field(default=None)),
|
|
96
|
+
(f"{axis_name}_ticktext", list[str] | None, field(default=None)),
|
|
97
|
+
(f"{axis_name}_zeroline", bool, field(default=False)),
|
|
98
|
+
(f"{axis_name}_zerolinecolor", str | None, field(default=None)),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _categorical_fields(axis_name: str) -> list[tuple[str, type, Any]]:
|
|
103
|
+
return [
|
|
104
|
+
(f"{axis_name}_categoryorder", str | None, field(default=None)),
|
|
105
|
+
(f"{axis_name}_categoryarray", list[str] | None, field(default=None)),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _mode_fields(axis_name: str) -> list[tuple[str, type, Any]]:
|
|
110
|
+
# None means "infer from the data"; the plot stores the outcome in
|
|
111
|
+
# _{axis}_resolved_mode before configuration runs.
|
|
112
|
+
return [
|
|
113
|
+
(f"{axis_name}_axis_mode", AxisMode | None, field(default=None)),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --- Configure helpers ---
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _configure_common(self: Any, axis_name: str, kwargs: dict[str, Any]) -> None:
|
|
121
|
+
def _get(suffix: str) -> Any:
|
|
122
|
+
return getattr(self, f"{axis_name}_{suffix}")
|
|
123
|
+
|
|
124
|
+
# Get theme axes style for defaults
|
|
125
|
+
theme = getattr(self, "_theme", None)
|
|
126
|
+
axes_style = theme.axes if theme else None
|
|
127
|
+
|
|
128
|
+
title = _get("title")
|
|
129
|
+
if title is not None:
|
|
130
|
+
kwargs["title"] = title
|
|
131
|
+
|
|
132
|
+
kwargs["showticklabels"] = _get("showticklabels")
|
|
133
|
+
|
|
134
|
+
# showgrid: explicit value or theme default
|
|
135
|
+
show_grid = _get("showgrid")
|
|
136
|
+
if show_grid is not None:
|
|
137
|
+
kwargs["showgrid"] = show_grid
|
|
138
|
+
elif axes_style:
|
|
139
|
+
kwargs["showgrid"] = axes_style.show_grid
|
|
140
|
+
|
|
141
|
+
# showline: explicit value or theme default
|
|
142
|
+
show_line = _get("showline")
|
|
143
|
+
if show_line is not None:
|
|
144
|
+
kwargs["showline"] = show_line
|
|
145
|
+
elif axes_style:
|
|
146
|
+
kwargs["showline"] = axes_style.show_line
|
|
147
|
+
|
|
148
|
+
# mirror: from theme if available
|
|
149
|
+
if axes_style:
|
|
150
|
+
kwargs["mirror"] = axes_style.mirror
|
|
151
|
+
|
|
152
|
+
tickangle = _get("tickangle")
|
|
153
|
+
if tickangle is not None:
|
|
154
|
+
kwargs["tickangle"] = tickangle
|
|
155
|
+
|
|
156
|
+
# gridcolor: explicit value or theme default
|
|
157
|
+
gridcolor = _get("gridcolor")
|
|
158
|
+
if gridcolor is not None:
|
|
159
|
+
kwargs["gridcolor"] = gridcolor
|
|
160
|
+
elif axes_style:
|
|
161
|
+
kwargs["gridcolor"] = axes_style.grid_color
|
|
162
|
+
|
|
163
|
+
# linecolor: explicit value or theme default
|
|
164
|
+
linecolor = _get("linecolor")
|
|
165
|
+
if linecolor is not None:
|
|
166
|
+
kwargs["linecolor"] = linecolor
|
|
167
|
+
elif axes_style:
|
|
168
|
+
kwargs["linecolor"] = axes_style.line_color
|
|
169
|
+
|
|
170
|
+
tickfont: dict[str, Any] = {}
|
|
171
|
+
if (size := _get("tickfont_size")) is not None:
|
|
172
|
+
tickfont["size"] = size
|
|
173
|
+
if (family := _get("tickfont_family")) is not None:
|
|
174
|
+
tickfont["family"] = family
|
|
175
|
+
if (color := _get("tickfont_color")) is not None:
|
|
176
|
+
tickfont["color"] = color
|
|
177
|
+
if tickfont:
|
|
178
|
+
kwargs["tickfont"] = tickfont
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _configure_numeric(self: Any, axis_name: str, kwargs: dict[str, Any]) -> None:
|
|
182
|
+
def _get(suffix: str) -> Any:
|
|
183
|
+
return getattr(self, f"{axis_name}_{suffix}")
|
|
184
|
+
|
|
185
|
+
if (rng := _get("range")) is not None:
|
|
186
|
+
kwargs["range"] = rng
|
|
187
|
+
|
|
188
|
+
if (dtick := _get("dtick")) is not None:
|
|
189
|
+
kwargs["dtick"] = dtick
|
|
190
|
+
|
|
191
|
+
if (tick0 := _get("tick0")) is not None:
|
|
192
|
+
kwargs["tick0"] = tick0
|
|
193
|
+
|
|
194
|
+
if (tickmode := _get("tickmode")) is not None:
|
|
195
|
+
kwargs["tickmode"] = tickmode
|
|
196
|
+
|
|
197
|
+
if (tickvals := _get("tickvals")) is not None:
|
|
198
|
+
kwargs["tickvals"] = tickvals
|
|
199
|
+
|
|
200
|
+
if (ticktext := _get("ticktext")) is not None:
|
|
201
|
+
kwargs["ticktext"] = ticktext
|
|
202
|
+
|
|
203
|
+
if (fmt := _get("tickformat")) is not None:
|
|
204
|
+
kwargs["tickformat"] = fmt
|
|
205
|
+
|
|
206
|
+
kwargs["zeroline"] = _get("zeroline")
|
|
207
|
+
if (zcolor := _get("zerolinecolor")) is not None:
|
|
208
|
+
kwargs["zerolinecolor"] = zcolor
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _configure_categorical(self: Any, axis_name: str, kwargs: dict[str, Any]) -> None:
|
|
212
|
+
def _get(suffix: str) -> Any:
|
|
213
|
+
return getattr(self, f"{axis_name}_{suffix}")
|
|
214
|
+
|
|
215
|
+
if (order := _get("categoryorder")) is not None:
|
|
216
|
+
kwargs["categoryorder"] = order
|
|
217
|
+
|
|
218
|
+
if (arr := _get("categoryarray")) is not None:
|
|
219
|
+
kwargs["categoryarray"] = arr
|