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,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)