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,394 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import warnings
|
|
3
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
4
|
+
from operator import itemgetter
|
|
5
|
+
from typing import Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import powerlaw as pl
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
import diffract.core.utils.imports as import_utils
|
|
12
|
+
from diffract.core.compute.decorator import kernel
|
|
13
|
+
|
|
14
|
+
# Availability is probed without importing the accelerated module itself:
|
|
15
|
+
# importing it initializes the taichi runtime, which must not happen (and
|
|
16
|
+
# must not be able to fail) during `import diffract`.
|
|
17
|
+
_DIFFRACT_FIT_AVAILABLE = import_utils.is_available("taichi")
|
|
18
|
+
|
|
19
|
+
_fit_class: Any = None
|
|
20
|
+
_DIFFRACT_FIT_BROKEN = False
|
|
21
|
+
|
|
22
|
+
# The statistical floor of the accelerated fitter: its xmin candidates
|
|
23
|
+
# must leave a tail of MIN_TAIL_SIZE (50) points, per Clauset et al., so
|
|
24
|
+
# below ~2x that it cannot fit at all. From this size up it is also
|
|
25
|
+
# faster than the `powerlaw` library at every tail size once warm.
|
|
26
|
+
AUTO_DIFFRACT_MIN_SIZE = 100
|
|
27
|
+
|
|
28
|
+
FitMethod = Literal["auto", "powerlaw", "diffract"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _accelerated_fit_ready() -> bool:
|
|
32
|
+
"""True when the accelerated implementation can actually initialize.
|
|
33
|
+
|
|
34
|
+
taichi may be importable yet fail at runtime initialization; `auto`
|
|
35
|
+
must degrade to the powerlaw library then, not raise.
|
|
36
|
+
"""
|
|
37
|
+
global _DIFFRACT_FIT_BROKEN # noqa: PLW0603
|
|
38
|
+
if _DIFFRACT_FIT_BROKEN or not _DIFFRACT_FIT_AVAILABLE:
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
_get_fit_class()
|
|
42
|
+
except ModuleNotFoundError:
|
|
43
|
+
_DIFFRACT_FIT_BROKEN = True
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_fit_method(fit_method: FitMethod, data_size: int) -> str:
|
|
49
|
+
"""Resolve `auto` to an implementation by data size and availability.
|
|
50
|
+
|
|
51
|
+
`auto` never raises over the accelerated path: explicit
|
|
52
|
+
fit_method="diffract" surfaces initialization errors instead.
|
|
53
|
+
"""
|
|
54
|
+
if fit_method != "auto":
|
|
55
|
+
return fit_method
|
|
56
|
+
if data_size < AUTO_DIFFRACT_MIN_SIZE:
|
|
57
|
+
return "powerlaw"
|
|
58
|
+
return "diffract" if _accelerated_fit_ready() else "powerlaw"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_fit_class() -> Any:
|
|
62
|
+
"""Import the accelerated Fit implementation on first use."""
|
|
63
|
+
global _fit_class # noqa: PLW0603
|
|
64
|
+
if _fit_class is None:
|
|
65
|
+
try:
|
|
66
|
+
module = import_utils.get_module(
|
|
67
|
+
"diffract.core.compute.extensions.power_law"
|
|
68
|
+
)
|
|
69
|
+
_fit_class = None if module is None else module.Fit
|
|
70
|
+
except Exception as e: # optional accelerator must not crash callers
|
|
71
|
+
raise ModuleNotFoundError(
|
|
72
|
+
"The accelerated 'diffract' fit implementation failed to "
|
|
73
|
+
f"initialize: {e}. Use fit_method='powerlaw' instead."
|
|
74
|
+
) from e
|
|
75
|
+
if _fit_class is None:
|
|
76
|
+
raise ModuleNotFoundError(
|
|
77
|
+
"The accelerated 'diffract' fit implementation needs taichi. "
|
|
78
|
+
'Install it with: pip install "diffract-core[taichi]", '
|
|
79
|
+
"or use fit_method='powerlaw'."
|
|
80
|
+
)
|
|
81
|
+
return _fit_class
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# region general
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@kernel(name="pl_concentration", require_fields=("esd", "pl_esd_xmin"))
|
|
88
|
+
@kernel(name="tpl_concentration", require_fields=("esd", "tpl_esd_xmin"))
|
|
89
|
+
@kernel(name="expon_concentration", require_fields=("esd", "expon_esd_xmin"))
|
|
90
|
+
def ht_concentration(esd: NDArray[np.floating[Any]], ht_esd_xmin: float) -> float:
|
|
91
|
+
"""Compute heavy-tailed concentration (tail size / total size)."""
|
|
92
|
+
tail_size = cast("int", np.sum(esd >= ht_esd_xmin).item())
|
|
93
|
+
|
|
94
|
+
return tail_size / esd.size
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@kernel(name="pl_presence", require_fields=("esd_min", "esd_max", "pl_esd_xmin"))
|
|
98
|
+
@kernel(name="tpl_presence", require_fields=("esd_min", "esd_max", "tpl_esd_xmin"))
|
|
99
|
+
@kernel(name="expon_presence", require_fields=("esd_min", "esd_max", "expon_esd_xmin"))
|
|
100
|
+
def ht_presence(esd_min: float, esd_max: float, ht_esd_xmin: float) -> float:
|
|
101
|
+
"""Compute heavy-tailed presence (tail width / total width)."""
|
|
102
|
+
esd_width = esd_max - esd_min
|
|
103
|
+
ht_width = esd_max - ht_esd_xmin
|
|
104
|
+
|
|
105
|
+
return ht_width / esd_width if esd_width > 0 else float("nan")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@kernel(name="tpl_scale", require_fields=("esd_max", "tpl_lambda"))
|
|
109
|
+
@kernel(name="expon_scale", require_fields=("esd_max", "expon_lambda"))
|
|
110
|
+
def ht_scale(esd_max: float, ht_lambda: float) -> float:
|
|
111
|
+
"""Compute heavy-tailed scale (max * lambda)."""
|
|
112
|
+
return esd_max * ht_lambda
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# endregion general
|
|
116
|
+
|
|
117
|
+
# region power_law
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def power_law_fit_powerlaw_implementation(
|
|
121
|
+
data: NDArray[np.floating[Any]],
|
|
122
|
+
) -> tuple[float, float, float]:
|
|
123
|
+
"""Fit power law using powerlaw library."""
|
|
124
|
+
f = io.StringIO()
|
|
125
|
+
with redirect_stdout(f), redirect_stderr(f), warnings.catch_warnings():
|
|
126
|
+
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
|
127
|
+
|
|
128
|
+
distribution = "power_law"
|
|
129
|
+
fit = pl.Fit(
|
|
130
|
+
data,
|
|
131
|
+
distribution=distribution,
|
|
132
|
+
xmin_distribution=distribution,
|
|
133
|
+
discrete=False,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
fit_result = getattr(fit, distribution)
|
|
137
|
+
pl_alpha = fit_result.alpha
|
|
138
|
+
pl_esd_xmin = fit_result.xmin
|
|
139
|
+
pl_ks = fit_result.KS()
|
|
140
|
+
|
|
141
|
+
return pl_alpha, pl_esd_xmin, pl_ks
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def power_law_fit_diffract_implementation(
|
|
145
|
+
data: NDArray[np.floating[Any]],
|
|
146
|
+
) -> tuple[float, float, float]:
|
|
147
|
+
"""Fit power law using diffract's accelerated Taichi implementation."""
|
|
148
|
+
fit = _get_fit_class()(data, "power_law")
|
|
149
|
+
try:
|
|
150
|
+
params = fit.fit_params()
|
|
151
|
+
finally:
|
|
152
|
+
fit.close()
|
|
153
|
+
|
|
154
|
+
pl_alpha, pl_esd_xmin, pl_ks = itemgetter("pl_alpha", "xmin", "ks_distance")(params)
|
|
155
|
+
|
|
156
|
+
return pl_alpha, pl_esd_xmin, pl_ks
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@kernel(produce_fields=("pl_alpha", "pl_esd_xmin", "pl_ks"))
|
|
160
|
+
def power_law_fit(
|
|
161
|
+
esd: NDArray[np.floating[Any]],
|
|
162
|
+
*,
|
|
163
|
+
fit_method: FitMethod = "auto",
|
|
164
|
+
) -> tuple[float, float, float]:
|
|
165
|
+
"""Fit power law distribution to ESD data.
|
|
166
|
+
|
|
167
|
+
`auto` uses the accelerated implementation when the taichi extra is
|
|
168
|
+
installed and the ESD is large enough for it to fit at all.
|
|
169
|
+
"""
|
|
170
|
+
match _resolve_fit_method(fit_method, esd.size):
|
|
171
|
+
case "powerlaw":
|
|
172
|
+
return power_law_fit_powerlaw_implementation(esd)
|
|
173
|
+
case "diffract":
|
|
174
|
+
return power_law_fit_diffract_implementation(esd)
|
|
175
|
+
case _:
|
|
176
|
+
msg = (
|
|
177
|
+
f"Fit method {fit_method!r} not implemented, choose method "
|
|
178
|
+
"from ('auto', 'powerlaw', 'diffract')."
|
|
179
|
+
)
|
|
180
|
+
raise ValueError(msg)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def pl_p_value(
|
|
184
|
+
esd: NDArray[np.floating[Any]], pl_alpha: float, pl_esd_xmin: float, pl_ks: float
|
|
185
|
+
) -> float:
|
|
186
|
+
"""Compute p-value for power law fit (requires the taichi extra)."""
|
|
187
|
+
fit = _get_fit_class()(esd, "power_law")
|
|
188
|
+
try:
|
|
189
|
+
fit.set_params(
|
|
190
|
+
xmin=pl_esd_xmin,
|
|
191
|
+
pl_alpha=pl_alpha,
|
|
192
|
+
ks_distance=pl_ks,
|
|
193
|
+
tail_size=cast("int", np.sum(esd >= pl_esd_xmin).item()),
|
|
194
|
+
)
|
|
195
|
+
_, p_value = fit.p_value_test()
|
|
196
|
+
finally:
|
|
197
|
+
fit.close()
|
|
198
|
+
|
|
199
|
+
return p_value
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# endregion power_law
|
|
203
|
+
|
|
204
|
+
# region truncated_power_law
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def truncated_power_law_fit_powerlaw_implementation(
|
|
208
|
+
data: NDArray[np.floating[Any]],
|
|
209
|
+
) -> tuple[float, float, float, float]:
|
|
210
|
+
"""Fit truncated power law using powerlaw library."""
|
|
211
|
+
f = io.StringIO()
|
|
212
|
+
with redirect_stdout(f), redirect_stderr(f), warnings.catch_warnings():
|
|
213
|
+
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
|
214
|
+
|
|
215
|
+
distribution = "truncated_power_law"
|
|
216
|
+
fit = pl.Fit(
|
|
217
|
+
data,
|
|
218
|
+
distribution=distribution,
|
|
219
|
+
xmin_distribution=distribution,
|
|
220
|
+
discrete=False,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
fit_result = getattr(fit, distribution)
|
|
224
|
+
tpl_alpha = fit_result.alpha
|
|
225
|
+
tpl_lambda = fit_result.Lambda
|
|
226
|
+
tpl_esd_xmin = fit_result.xmin
|
|
227
|
+
tpl_ks = fit_result.KS()
|
|
228
|
+
|
|
229
|
+
return tpl_alpha, tpl_lambda, tpl_esd_xmin, tpl_ks
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def truncated_power_law_fit_diffract_implementation(
|
|
233
|
+
data: NDArray[np.floating[Any]],
|
|
234
|
+
) -> tuple[float, float, float, float]:
|
|
235
|
+
"""Fit truncated power law using diffract's accelerated Taichi implementation."""
|
|
236
|
+
fit = _get_fit_class()(data, "truncated_power_law")
|
|
237
|
+
try:
|
|
238
|
+
params = fit.fit_params()
|
|
239
|
+
finally:
|
|
240
|
+
fit.close()
|
|
241
|
+
|
|
242
|
+
tpl_alpha, tpl_lambda, tpl_esd_xmin, tpl_ks = itemgetter(
|
|
243
|
+
"pl_alpha", "expon_lambda", "xmin", "ks_distance"
|
|
244
|
+
)(params)
|
|
245
|
+
|
|
246
|
+
return tpl_alpha, tpl_lambda, tpl_esd_xmin, tpl_ks
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@kernel(produce_fields=("tpl_alpha", "tpl_lambda", "tpl_esd_xmin", "tpl_ks"))
|
|
250
|
+
def truncated_power_law_fit(
|
|
251
|
+
esd: NDArray[np.floating[Any]],
|
|
252
|
+
*,
|
|
253
|
+
fit_method: FitMethod = "auto",
|
|
254
|
+
) -> tuple[float, float, float, float]:
|
|
255
|
+
"""Fit truncated power law distribution to ESD data.
|
|
256
|
+
|
|
257
|
+
`auto` uses the accelerated implementation when the taichi extra is
|
|
258
|
+
installed and the ESD is large enough for it to fit at all.
|
|
259
|
+
"""
|
|
260
|
+
match _resolve_fit_method(fit_method, esd.size):
|
|
261
|
+
case "powerlaw":
|
|
262
|
+
return truncated_power_law_fit_powerlaw_implementation(esd)
|
|
263
|
+
case "diffract":
|
|
264
|
+
return truncated_power_law_fit_diffract_implementation(esd)
|
|
265
|
+
case _:
|
|
266
|
+
msg = (
|
|
267
|
+
f"Fit method {fit_method!r} not implemented, choose method "
|
|
268
|
+
"from ('auto', 'powerlaw', 'diffract')."
|
|
269
|
+
)
|
|
270
|
+
raise ValueError(msg)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def tpl_p_value(
|
|
274
|
+
esd: NDArray[np.floating[Any]],
|
|
275
|
+
tpl_alpha: float,
|
|
276
|
+
tpl_lambda: float,
|
|
277
|
+
tpl_esd_xmin: float,
|
|
278
|
+
tpl_ks: float,
|
|
279
|
+
) -> float:
|
|
280
|
+
"""Compute p-value for truncated power law fit (requires the taichi extra)."""
|
|
281
|
+
fit = _get_fit_class()(esd, "truncated_power_law")
|
|
282
|
+
try:
|
|
283
|
+
fit.set_params(
|
|
284
|
+
xmin=tpl_esd_xmin,
|
|
285
|
+
pl_alpha=tpl_alpha,
|
|
286
|
+
expon_lambda=tpl_lambda,
|
|
287
|
+
ks_distance=tpl_ks,
|
|
288
|
+
tail_size=cast("int", np.sum(esd >= tpl_esd_xmin).item()),
|
|
289
|
+
)
|
|
290
|
+
_, p_value = fit.p_value_test()
|
|
291
|
+
finally:
|
|
292
|
+
fit.close()
|
|
293
|
+
|
|
294
|
+
return p_value
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# endregion truncated_power_law
|
|
298
|
+
|
|
299
|
+
# region exponential
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def exponential_fit_powerlaw_implementation(
|
|
303
|
+
data: NDArray[np.floating[Any]],
|
|
304
|
+
) -> tuple[float, float, float]:
|
|
305
|
+
"""Fit exponential distribution using powerlaw library."""
|
|
306
|
+
f = io.StringIO()
|
|
307
|
+
with redirect_stdout(f), redirect_stderr(f), warnings.catch_warnings():
|
|
308
|
+
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
|
309
|
+
|
|
310
|
+
distribution = "exponential"
|
|
311
|
+
fit = pl.Fit(
|
|
312
|
+
data,
|
|
313
|
+
distribution=distribution,
|
|
314
|
+
xmin_distribution=distribution,
|
|
315
|
+
discrete=False,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
fit_result = getattr(fit, distribution)
|
|
319
|
+
expon_lambda = fit_result.Lambda
|
|
320
|
+
expon_esd_xmin = fit_result.xmin
|
|
321
|
+
expon_ks = fit_result.KS()
|
|
322
|
+
|
|
323
|
+
return expon_lambda, expon_esd_xmin, expon_ks
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def exponential_fit_diffract_implementation(
|
|
327
|
+
data: NDArray[np.floating[Any]],
|
|
328
|
+
) -> tuple[float, float, float]:
|
|
329
|
+
"""Fit exponential distribution using the accelerated implementation."""
|
|
330
|
+
fit = _get_fit_class()(data, "exponential")
|
|
331
|
+
try:
|
|
332
|
+
params = fit.fit_params()
|
|
333
|
+
finally:
|
|
334
|
+
fit.close()
|
|
335
|
+
|
|
336
|
+
expon_lambda, expon_esd_xmin, expon_ks = itemgetter(
|
|
337
|
+
"expon_lambda", "xmin", "ks_distance"
|
|
338
|
+
)(params)
|
|
339
|
+
|
|
340
|
+
return expon_lambda, expon_esd_xmin, expon_ks
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@kernel(produce_fields=("expon_lambda", "expon_esd_xmin", "expon_ks"))
|
|
344
|
+
def exponential_fit(
|
|
345
|
+
esd: NDArray[np.floating[Any]],
|
|
346
|
+
*,
|
|
347
|
+
fit_method: FitMethod = "auto",
|
|
348
|
+
) -> tuple[float, float, float]:
|
|
349
|
+
"""Fit exponential distribution to ESD data.
|
|
350
|
+
|
|
351
|
+
`auto` uses the accelerated implementation when the taichi extra is
|
|
352
|
+
installed and the ESD is large enough for it to fit at all.
|
|
353
|
+
"""
|
|
354
|
+
match _resolve_fit_method(fit_method, esd.size):
|
|
355
|
+
case "powerlaw":
|
|
356
|
+
return exponential_fit_powerlaw_implementation(esd)
|
|
357
|
+
case "diffract":
|
|
358
|
+
return exponential_fit_diffract_implementation(esd)
|
|
359
|
+
case _:
|
|
360
|
+
msg = (
|
|
361
|
+
f"Fit method {fit_method!r} not implemented, choose method "
|
|
362
|
+
"from ('auto', 'powerlaw', 'diffract')."
|
|
363
|
+
)
|
|
364
|
+
raise ValueError(msg)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def expon_p_value(
|
|
368
|
+
esd: NDArray[np.floating[Any]],
|
|
369
|
+
expon_lambda: float,
|
|
370
|
+
expon_esd_xmin: float,
|
|
371
|
+
expon_ks: float,
|
|
372
|
+
) -> float:
|
|
373
|
+
"""Compute p-value for exponential fit (requires the taichi extra)."""
|
|
374
|
+
fit = _get_fit_class()(esd, "exponential")
|
|
375
|
+
try:
|
|
376
|
+
fit.set_params(
|
|
377
|
+
xmin=expon_esd_xmin,
|
|
378
|
+
expon_lambda=expon_lambda,
|
|
379
|
+
ks_distance=expon_ks,
|
|
380
|
+
tail_size=cast("int", np.sum(esd >= expon_esd_xmin).item()),
|
|
381
|
+
)
|
|
382
|
+
_, p_value = fit.p_value_test()
|
|
383
|
+
finally:
|
|
384
|
+
fit.close()
|
|
385
|
+
|
|
386
|
+
return p_value
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# The p-value kernels are only executable with the accelerated implementation,
|
|
390
|
+
# so they join the registry only when it is importable.
|
|
391
|
+
if _DIFFRACT_FIT_AVAILABLE:
|
|
392
|
+
kernel(pl_p_value)
|
|
393
|
+
kernel(tpl_p_value)
|
|
394
|
+
kernel(expon_p_value)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
|
|
6
|
+
from diffract.core.compute.decorator import kernel
|
|
7
|
+
from diffract.core.compute.extensions.rmt import marchenko_pastur_cdf
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@kernel(produce_fields=("mp_esd_max", "mp_esd_min", "mp_bulk_std"))
|
|
11
|
+
def marchenko_pastur_fit(
|
|
12
|
+
esd_rand: NDArray[np.floating[Any]],
|
|
13
|
+
esd_max: float,
|
|
14
|
+
weights_std: NDArray[np.floating[Any]],
|
|
15
|
+
aspect_ratio: float,
|
|
16
|
+
lower_dim: int,
|
|
17
|
+
) -> tuple[float, float, float]:
|
|
18
|
+
"""Fit Marchenko-Pastur distribution to random ESD."""
|
|
19
|
+
sigma_2_fit = weights_std**2
|
|
20
|
+
eig_max_fit = sigma_2_fit * (1 + (1 / np.sqrt(aspect_ratio))) ** 2
|
|
21
|
+
|
|
22
|
+
eigenvals_bleeding_out = esd_rand[esd_rand > eig_max_fit]
|
|
23
|
+
if eigenvals_bleeding_out.size > 0:
|
|
24
|
+
sigma_2_corrected = sigma_2_fit - (np.sum(eigenvals_bleeding_out) / lower_dim)
|
|
25
|
+
eig_max_corrected = sigma_2_corrected * (1 + (1 / np.sqrt(aspect_ratio))) ** 2
|
|
26
|
+
mp_esd_max = eig_max_corrected
|
|
27
|
+
mp_bulk_std = np.sqrt(sigma_2_corrected)
|
|
28
|
+
else:
|
|
29
|
+
mp_esd_max = eig_max_fit
|
|
30
|
+
mp_bulk_std = weights_std
|
|
31
|
+
|
|
32
|
+
mp_esd_max = min(mp_esd_max, esd_max)
|
|
33
|
+
|
|
34
|
+
mp_esd_min = (mp_bulk_std**2) * (1 - (1 / np.sqrt(aspect_ratio))) ** 2
|
|
35
|
+
|
|
36
|
+
return mp_esd_max, mp_esd_min, mp_bulk_std
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@kernel
|
|
40
|
+
def mp_sval_max(mp_esd_max: float, greater_dim: int) -> float:
|
|
41
|
+
"""Compute maximum singular value from MP ESD max."""
|
|
42
|
+
return np.sqrt(mp_esd_max * greater_dim)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@kernel
|
|
46
|
+
def mp_ks(
|
|
47
|
+
aspect_ratio: float,
|
|
48
|
+
mp_bulk_std: float,
|
|
49
|
+
esd: NDArray[np.floating[Any]],
|
|
50
|
+
mp_esd_max: float,
|
|
51
|
+
mp_esd_min: float,
|
|
52
|
+
) -> float:
|
|
53
|
+
"""Compute Kolmogorov-Smirnov distance for Marchenko-Pastur fit."""
|
|
54
|
+
ratio = 1 / (aspect_ratio + 1e-8)
|
|
55
|
+
sigma = mp_bulk_std # we consider the fitted bulk std as the standard deviation
|
|
56
|
+
|
|
57
|
+
esd_mask = (mp_esd_min < esd) & (esd < mp_esd_max)
|
|
58
|
+
if np.sum(esd_mask).astype(int).item() == 0:
|
|
59
|
+
ks_distance = 1
|
|
60
|
+
else:
|
|
61
|
+
esd_filtered = esd[esd_mask]
|
|
62
|
+
model_cdf = marchenko_pastur_cdf(esd_filtered, ratio, sigma)
|
|
63
|
+
|
|
64
|
+
empirical_cdf = np.arange(esd_filtered.size) / esd_filtered.size
|
|
65
|
+
|
|
66
|
+
ks_distance = np.max(np.abs(empirical_cdf - model_cdf))
|
|
67
|
+
|
|
68
|
+
return ks_distance
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@kernel
|
|
72
|
+
def mp_concentration(
|
|
73
|
+
esd: NDArray[np.floating[Any]], mp_esd_max: float, mp_esd_min: float
|
|
74
|
+
) -> float:
|
|
75
|
+
"""Compute MP bulk concentration (bulk size / total size)."""
|
|
76
|
+
mask = (mp_esd_min <= esd) & (esd <= mp_esd_max)
|
|
77
|
+
bulk_size = cast("int", np.sum(mask.astype(int)).item())
|
|
78
|
+
|
|
79
|
+
return bulk_size / esd.size
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@kernel
|
|
83
|
+
def mp_presence(
|
|
84
|
+
esd_min: float,
|
|
85
|
+
esd_max: float,
|
|
86
|
+
mp_esd_max: float,
|
|
87
|
+
mp_esd_min: float,
|
|
88
|
+
) -> float:
|
|
89
|
+
"""Compute MP bulk presence (bulk width / total width)."""
|
|
90
|
+
esd_width = esd_max - esd_min
|
|
91
|
+
mp_width = mp_esd_max - mp_esd_min
|
|
92
|
+
|
|
93
|
+
return mp_width / esd_width
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@kernel
|
|
97
|
+
def mp_num_spikes(esd: NDArray[np.floating[Any]], mp_esd_max: float) -> int:
|
|
98
|
+
"""Count number of spikes above MP bulk maximum."""
|
|
99
|
+
return cast("int", np.sum((esd > mp_esd_max).astype(int)).item())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
import diffract.core.utils.imports as import_utils
|
|
8
|
+
from diffract.core.compute.decorator import kernel
|
|
9
|
+
|
|
10
|
+
torch = import_utils.optional_import("torch")
|
|
11
|
+
TORCH_AVAILABLE = torch is not None
|
|
12
|
+
if TORCH_AVAILABLE:
|
|
13
|
+
from diffract.core.compute.extensions.utils import torch_cuda_wrapper
|
|
14
|
+
|
|
15
|
+
IS_CUDA_AVAILABLE = bool(torch.cuda.is_available())
|
|
16
|
+
else:
|
|
17
|
+
IS_CUDA_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@kernel(
|
|
21
|
+
name="weights_svd",
|
|
22
|
+
require_fields=("weights",),
|
|
23
|
+
produce_fields=(
|
|
24
|
+
"weights_lsvs",
|
|
25
|
+
"weights_svals",
|
|
26
|
+
"weights_rsvs",
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
@kernel(
|
|
30
|
+
name="weights_rand_svd",
|
|
31
|
+
require_fields=("weights_rand",),
|
|
32
|
+
produce_fields=(
|
|
33
|
+
"weights_rand_lsvs",
|
|
34
|
+
"weights_rand_svals",
|
|
35
|
+
"weights_rand_rsvs",
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
def svd(
|
|
39
|
+
mat: NDArray[np.floating[Any]], *, allow_cuda: bool = True
|
|
40
|
+
) -> tuple[
|
|
41
|
+
NDArray[np.floating[Any]], NDArray[np.floating[Any]], NDArray[np.floating[Any]]
|
|
42
|
+
]:
|
|
43
|
+
"""Compute SVD decomposition of matrix."""
|
|
44
|
+
if IS_CUDA_AVAILABLE and allow_cuda:
|
|
45
|
+
svd_fn = functools.partial(torch.linalg.svd, full_matrices=False)
|
|
46
|
+
u, svals, vt = torch_cuda_wrapper(mat, svd_fn)
|
|
47
|
+
else:
|
|
48
|
+
u, svals, vt = np.linalg.svd(mat, full_matrices=False)
|
|
49
|
+
|
|
50
|
+
svals = np.abs(svals.real).flatten()
|
|
51
|
+
svals_argsort_index = np.argsort(svals)
|
|
52
|
+
|
|
53
|
+
lsvs_sorted = u[:, svals_argsort_index]
|
|
54
|
+
svals_sorted = svals[svals_argsort_index]
|
|
55
|
+
rsvs_sorted = vt.T[:, svals_argsort_index]
|
|
56
|
+
|
|
57
|
+
return lsvs_sorted, svals_sorted, rsvs_sorted
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@kernel(name="esd", require_fields=("weights_svals", "greater_dim"))
|
|
61
|
+
@kernel(name="esd_rand", require_fields=("weights_rand_svals", "greater_dim"))
|
|
62
|
+
def esd(
|
|
63
|
+
svals: NDArray[np.floating[Any]], greater_dim: int
|
|
64
|
+
) -> NDArray[np.floating[Any]]:
|
|
65
|
+
"""Compute empirical spectral distribution from singular values."""
|
|
66
|
+
result = np.square(svals) / greater_dim
|
|
67
|
+
return cast("NDArray[np.floating[Any]]", result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# region utils
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@kernel(name="max_weights_sval", require_fields=("weights_svals",))
|
|
74
|
+
@kernel(name="max_weights_rand_sval", require_fields=("weights_rand_svals",))
|
|
75
|
+
def max_sval(svals: NDArray[np.floating[Any]]) -> float:
|
|
76
|
+
"""Get maximum singular value."""
|
|
77
|
+
return cast("float", svals[-1].item())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@kernel(name="min_weights_sval", require_fields=("weights_svals",))
|
|
81
|
+
@kernel(name="min_weights_rand_sval", require_fields=("weights_rand_svals",))
|
|
82
|
+
def min_sval(svals: NDArray[np.floating[Any]]) -> float:
|
|
83
|
+
"""Get minimum singular value."""
|
|
84
|
+
return cast("float", svals[0].item())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@kernel(name="esd_max", require_fields=("esd",))
|
|
88
|
+
@kernel(name="esd_rand_max", require_fields=("esd_rand",))
|
|
89
|
+
def esd_max(esd: NDArray[np.floating[Any]]) -> float:
|
|
90
|
+
"""Get maximum ESD value."""
|
|
91
|
+
return cast("float", esd[-1].item())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@kernel(name="esd_min", require_fields=("esd",))
|
|
95
|
+
@kernel(name="esd_rand_min", require_fields=("esd_rand",))
|
|
96
|
+
def esd_min(esd: NDArray[np.floating[Any]]) -> float:
|
|
97
|
+
"""Get minimum ESD value."""
|
|
98
|
+
return cast("float", esd[0].item())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# endregion utils
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
|
|
6
|
+
from diffract.core.compute.decorator import kernel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@kernel
|
|
10
|
+
def greater_dim(weights: NDArray[int]) -> int:
|
|
11
|
+
"""Get greater dimension from shape."""
|
|
12
|
+
return max(weights.shape)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@kernel
|
|
16
|
+
def lower_dim(weights: NDArray[int]) -> int:
|
|
17
|
+
"""Get lower dimension from shape."""
|
|
18
|
+
return min(weights.shape)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@kernel
|
|
22
|
+
def aspect_ratio(greater_dim: int, lower_dim: int) -> float:
|
|
23
|
+
"""Compute aspect ratio (greater_dim / lower_dim)."""
|
|
24
|
+
return greater_dim / lower_dim
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@kernel
|
|
28
|
+
def weights_std(weights: NDArray[np.floating[Any]]) -> float:
|
|
29
|
+
"""Compute standard deviation of weights."""
|
|
30
|
+
return cast("float", weights.std())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@kernel
|
|
34
|
+
def weights_rand(
|
|
35
|
+
weights: NDArray[np.floating[Any]],
|
|
36
|
+
*,
|
|
37
|
+
n_randomise_iterations: int = 1,
|
|
38
|
+
seed: int = 42,
|
|
39
|
+
) -> NDArray[np.floating[Any]]:
|
|
40
|
+
"""Generate randomized version of weights by shuffling."""
|
|
41
|
+
rng = np.random.default_rng(None if seed == -1 else seed)
|
|
42
|
+
|
|
43
|
+
result = weights.copy()
|
|
44
|
+
|
|
45
|
+
shape = result.shape
|
|
46
|
+
index = np.arange(np.prod(shape))
|
|
47
|
+
|
|
48
|
+
result = result.reshape(-1)
|
|
49
|
+
for _ in range(n_randomise_iterations):
|
|
50
|
+
rng.shuffle(index)
|
|
51
|
+
result = result[index]
|
|
52
|
+
|
|
53
|
+
return result.reshape(shape)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
from scipy import stats
|
|
7
|
+
|
|
8
|
+
from diffract.core.compute.decorator import kernel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@kernel
|
|
12
|
+
def pl_alpha_weighted(esd_max: float, pl_alpha: float) -> float:
|
|
13
|
+
"""Compute power law alpha weighted by log ESD max."""
|
|
14
|
+
return pl_alpha * math.log(esd_max)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@kernel
|
|
18
|
+
def rand_distance(
|
|
19
|
+
esd: NDArray[np.floating[Any]], esd_rand: NDArray[np.floating[Any]]
|
|
20
|
+
) -> float:
|
|
21
|
+
"""Compute Jensen-Shannon divergence between ESD and randomized ESD."""
|
|
22
|
+
avg = 0.5 * (esd + esd_rand)
|
|
23
|
+
divergence = 0.5 * (
|
|
24
|
+
cast("float", stats.entropy(esd, avg))
|
|
25
|
+
+ cast("float", stats.entropy(esd_rand, avg))
|
|
26
|
+
)
|
|
27
|
+
return math.sqrt(divergence)
|