diffpes 2026.3.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.
- diffpes/__init__.py +65 -0
- diffpes/inout/__init__.py +88 -0
- diffpes/inout/chgcar.py +459 -0
- diffpes/inout/doscar.py +248 -0
- diffpes/inout/eigenval.py +258 -0
- diffpes/inout/hdf5.py +818 -0
- diffpes/inout/helpers.py +295 -0
- diffpes/inout/kpoints.py +433 -0
- diffpes/inout/plotting.py +757 -0
- diffpes/inout/poscar.py +174 -0
- diffpes/inout/procar.py +331 -0
- diffpes/inout/py.typed +0 -0
- diffpes/maths/__init__.py +68 -0
- diffpes/maths/dipole.py +368 -0
- diffpes/maths/gaunt.py +496 -0
- diffpes/maths/spherical_harmonics.py +336 -0
- diffpes/py.typed +0 -0
- diffpes/radial/__init__.py +47 -0
- diffpes/radial/bessel.py +198 -0
- diffpes/radial/integrate.py +128 -0
- diffpes/radial/wavefunctions.py +335 -0
- diffpes/simul/__init__.py +146 -0
- diffpes/simul/broadening.py +258 -0
- diffpes/simul/crosssections.py +196 -0
- diffpes/simul/expanded.py +1021 -0
- diffpes/simul/forward.py +586 -0
- diffpes/simul/oam.py +112 -0
- diffpes/simul/polarization.py +443 -0
- diffpes/simul/py.typed +0 -0
- diffpes/simul/resolution.py +122 -0
- diffpes/simul/self_energy.py +142 -0
- diffpes/simul/spectrum.py +1116 -0
- diffpes/simul/workflow.py +424 -0
- diffpes/tightb/__init__.py +68 -0
- diffpes/tightb/diagonalize.py +340 -0
- diffpes/tightb/hamiltonian.py +286 -0
- diffpes/tightb/projections.py +134 -0
- diffpes/types/__init__.py +162 -0
- diffpes/types/aliases.py +52 -0
- diffpes/types/bands.py +1064 -0
- diffpes/types/dos.py +510 -0
- diffpes/types/geometry.py +306 -0
- diffpes/types/kpath.py +365 -0
- diffpes/types/params.py +496 -0
- diffpes/types/py.typed +0 -0
- diffpes/types/radial_params.py +482 -0
- diffpes/types/self_energy.py +271 -0
- diffpes/types/tb_model.py +531 -0
- diffpes/types/volumetric.py +608 -0
- diffpes/utils/__init__.py +30 -0
- diffpes/utils/math.py +255 -0
- diffpes/utils/py.typed +0 -0
- diffpes-2026.3.1.dist-info/METADATA +176 -0
- diffpes-2026.3.1.dist-info/RECORD +55 -0
- diffpes-2026.3.1.dist-info/WHEEL +4 -0
diffpes/inout/hdf5.py
ADDED
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
"""HDF5 serializer and deserializer for diffpes PyTrees.
|
|
2
|
+
|
|
3
|
+
Extended Summary
|
|
4
|
+
----------------
|
|
5
|
+
Provides functions for saving and loading diffpes PyTree objects
|
|
6
|
+
(NamedTuples registered with ``@register_pytree_node_class``)
|
|
7
|
+
to and from HDF5 files via ``h5py``. Each PyTree's named fields
|
|
8
|
+
become HDF5 datasets, and auxiliary data (non-JAX metadata) is
|
|
9
|
+
stored as HDF5 group attributes in JSON format.
|
|
10
|
+
|
|
11
|
+
Routine Listings
|
|
12
|
+
----------------
|
|
13
|
+
:func:`load_from_h5`
|
|
14
|
+
Load PyTrees from an HDF5 file.
|
|
15
|
+
:func:`save_to_h5`
|
|
16
|
+
Save one or more named PyTrees to an HDF5 file.
|
|
17
|
+
|
|
18
|
+
Notes
|
|
19
|
+
-----
|
|
20
|
+
All eight diffpes PyTree types are supported:
|
|
21
|
+
``DensityOfStates``, ``BandStructure``, ``ArpesSpectrum``,
|
|
22
|
+
``OrbitalProjection``, ``SimulationParams``,
|
|
23
|
+
``PolarizationConfig``, ``KPathInfo``, ``CrystalGeometry``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
import h5py
|
|
31
|
+
import jax.numpy as jnp
|
|
32
|
+
import numpy as np
|
|
33
|
+
from beartype import beartype
|
|
34
|
+
from beartype.typing import Any, Callable, Optional, Union
|
|
35
|
+
|
|
36
|
+
from diffpes.types import (
|
|
37
|
+
ArpesSpectrum,
|
|
38
|
+
BandStructure,
|
|
39
|
+
CrystalGeometry,
|
|
40
|
+
DensityOfStates,
|
|
41
|
+
KPathInfo,
|
|
42
|
+
OrbitalProjection,
|
|
43
|
+
PolarizationConfig,
|
|
44
|
+
SimulationParams,
|
|
45
|
+
SOCVolumetricData,
|
|
46
|
+
SpinOrbitalProjection,
|
|
47
|
+
VolumetricData,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_ATTR_TYPE: str = "_pytree_type"
|
|
51
|
+
_ATTR_AUX: str = "_aux_data_json"
|
|
52
|
+
_ATTR_NONE: str = "_none_fields"
|
|
53
|
+
_KPATH_AUX_WITH_COMMENT_LEN: int = 3
|
|
54
|
+
_KPATH_AUX_WITH_COORD_MODE_LEN: int = 4
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class _PyTreeMeta:
|
|
59
|
+
"""Serialization metadata for a registered PyTree type.
|
|
60
|
+
|
|
61
|
+
Stores the class reference, the ordered names of JAX-traced
|
|
62
|
+
children fields (matching the order returned by
|
|
63
|
+
``tree_flatten``), and encoder/decoder callables for
|
|
64
|
+
converting auxiliary data to and from JSON-serializable form.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
cls : type
|
|
69
|
+
The NamedTuple PyTree class.
|
|
70
|
+
children_fields : tuple[str, ...]
|
|
71
|
+
Ordered field names of JAX array children.
|
|
72
|
+
aux_encoder : Callable[[Any], Any]
|
|
73
|
+
Converts aux_data to a JSON-serializable value.
|
|
74
|
+
aux_decoder : Callable[[Any], Any]
|
|
75
|
+
Converts JSON-decoded value back to the Python type
|
|
76
|
+
expected by ``tree_unflatten``.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
cls: Any # PyTree NamedTuple class
|
|
80
|
+
children_fields: tuple[str, ...]
|
|
81
|
+
aux_encoder: Callable[[Any], Any]
|
|
82
|
+
aux_decoder: Callable[[Any], Any]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _encode_none(
|
|
86
|
+
_aux: None, # noqa: ARG001
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Encode PyTree auxiliary data ``None`` for JSON storage.
|
|
89
|
+
|
|
90
|
+
Used for PyTree types that have no auxiliary data (e.g. DensityOfStates).
|
|
91
|
+
The value is returned unchanged; when written to JSON it becomes
|
|
92
|
+
``null``.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
_aux : None
|
|
97
|
+
The auxiliary data to encode (must be None).
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
None
|
|
102
|
+
Unchanged; serializers write this as JSON ``null``.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _decode_none(
|
|
107
|
+
_val: None, # noqa: ARG001
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Decode JSON ``null`` back to PyTree auxiliary data ``None``.
|
|
110
|
+
|
|
111
|
+
Inverse of ``_encode_none``. Used when loading PyTrees that have
|
|
112
|
+
no auxiliary data.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
_val : None
|
|
117
|
+
The decoded value (expected to be None / JSON null).
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
None
|
|
122
|
+
The reconstructed auxiliary data for the PyTree.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _encode_int(aux: int) -> int:
|
|
127
|
+
"""Encode PyTree auxiliary integer for JSON storage.
|
|
128
|
+
|
|
129
|
+
Converts the Python int to a JSON-serialisable integer. Used for
|
|
130
|
+
types that store a single int in auxiliary data (e.g. SimulationParams
|
|
131
|
+
fidelity).
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
aux : int
|
|
136
|
+
The auxiliary integer to encode.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
int
|
|
141
|
+
The same value, guaranteed to be a plain Python int for JSON.
|
|
142
|
+
"""
|
|
143
|
+
return int(aux)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _decode_int(val: Any) -> int: # noqa: ANN401
|
|
147
|
+
"""Decode a JSON integer back to Python int for PyTree auxiliary data.
|
|
148
|
+
|
|
149
|
+
Inverse of ``_encode_int``. Accepts any value and casts to int so that
|
|
150
|
+
JSON number types are correctly restored.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
val : Any
|
|
155
|
+
The value read from JSON (typically an int or float).
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
int
|
|
160
|
+
The reconstructed auxiliary integer.
|
|
161
|
+
"""
|
|
162
|
+
return int(val)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _encode_str(aux: str) -> str:
|
|
166
|
+
"""Encode PyTree auxiliary string for JSON storage.
|
|
167
|
+
|
|
168
|
+
Returns the string unchanged so it can be written as a JSON string.
|
|
169
|
+
Used for single-string auxiliary fields.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
aux : str
|
|
174
|
+
The auxiliary string to encode.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
str
|
|
179
|
+
The same string for JSON serialisation.
|
|
180
|
+
"""
|
|
181
|
+
return str(aux)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _decode_str(val: Any) -> str: # noqa: ANN401
|
|
185
|
+
"""Decode a JSON string back to Python str for PyTree auxiliary data.
|
|
186
|
+
|
|
187
|
+
Inverse of ``_encode_str``. Converts the loaded value to str so that
|
|
188
|
+
non-string JSON types (if any) are normalised.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
val : Any
|
|
193
|
+
The value read from JSON (typically a string).
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
str
|
|
198
|
+
The reconstructed auxiliary string.
|
|
199
|
+
"""
|
|
200
|
+
return str(val)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _encode_tuple_str(
|
|
204
|
+
aux: tuple[str, ...],
|
|
205
|
+
) -> list[str]:
|
|
206
|
+
"""Encode PyTree auxiliary tuple of strings for JSON storage.
|
|
207
|
+
|
|
208
|
+
JSON does not support tuples; the tuple is converted to a list of
|
|
209
|
+
strings so that it can be serialised. Used for types that store
|
|
210
|
+
sequences of strings (e.g. KPathInfo labels).
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
aux : tuple[str, ...]
|
|
215
|
+
The auxiliary tuple of strings to encode.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
list[str]
|
|
220
|
+
A list of the same strings for JSON array serialisation.
|
|
221
|
+
"""
|
|
222
|
+
return list(aux)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _decode_tuple_str(
|
|
226
|
+
val: Any, # noqa: ANN401
|
|
227
|
+
) -> tuple[str, ...]:
|
|
228
|
+
"""Decode a JSON array of strings to a tuple for PyTree auxiliary data.
|
|
229
|
+
|
|
230
|
+
Inverse of ``_encode_tuple_str``. Each element is coerced to str and
|
|
231
|
+
the result is returned as an immutable tuple.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
val : Any
|
|
236
|
+
The value read from JSON (typically a list of strings).
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
tuple[str, ...]
|
|
241
|
+
The reconstructed auxiliary tuple of strings.
|
|
242
|
+
"""
|
|
243
|
+
return tuple(str(s) for s in val)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _encode_kpath_aux(
|
|
247
|
+
aux: tuple[str, tuple[str, ...], str, str],
|
|
248
|
+
) -> list[Any]:
|
|
249
|
+
"""Encode KPathInfo auxiliary string metadata for JSON storage.
|
|
250
|
+
|
|
251
|
+
Extended Summary
|
|
252
|
+
----------------
|
|
253
|
+
KPathInfo stores ``(mode, labels, comment, coordinate_mode)`` as
|
|
254
|
+
its PyTree auxiliary data. Since JSON does not support Python
|
|
255
|
+
tuples, this encoder converts the 4-element tuple into a JSON
|
|
256
|
+
list ``[mode_str, [label_str, ...], comment_str,
|
|
257
|
+
coordinate_mode_str]``.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
aux : tuple[str, tuple[str, ...], str, str]
|
|
262
|
+
The KPathInfo auxiliary data:
|
|
263
|
+
``(mode, labels, comment, coordinate_mode)``.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
list[Any]
|
|
268
|
+
JSON-serializable list representation.
|
|
269
|
+
"""
|
|
270
|
+
mode: str
|
|
271
|
+
labels: tuple[str, ...]
|
|
272
|
+
comment: str
|
|
273
|
+
coordinate_mode: str
|
|
274
|
+
mode, labels, comment, coordinate_mode = aux
|
|
275
|
+
result: list[Any] = [
|
|
276
|
+
str(mode),
|
|
277
|
+
list(labels),
|
|
278
|
+
str(comment),
|
|
279
|
+
str(coordinate_mode),
|
|
280
|
+
]
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _decode_kpath_aux(
|
|
285
|
+
val: Any, # noqa: ANN401
|
|
286
|
+
) -> tuple[str, tuple[str, ...], str, str]:
|
|
287
|
+
"""Decode JSON list back to KPathInfo auxiliary string metadata.
|
|
288
|
+
|
|
289
|
+
Extended Summary
|
|
290
|
+
----------------
|
|
291
|
+
Inverse of :func:`_encode_kpath_aux`. Supports both the legacy
|
|
292
|
+
2-element format ``[mode, labels]`` (written by older versions of
|
|
293
|
+
the serializer) and the current 4-element format
|
|
294
|
+
``[mode, labels, comment, coordinate_mode]``. Missing fields
|
|
295
|
+
default to empty strings.
|
|
296
|
+
|
|
297
|
+
Implementation Logic
|
|
298
|
+
--------------------
|
|
299
|
+
1. Extract ``mode`` from index 0 and ``labels`` from index 1.
|
|
300
|
+
2. If the list has >= 3 elements, extract ``comment`` from index 2;
|
|
301
|
+
otherwise default to ``""``.
|
|
302
|
+
3. If the list has >= 4 elements, extract ``coordinate_mode`` from
|
|
303
|
+
index 3; otherwise default to ``""``.
|
|
304
|
+
4. Return as a 4-tuple matching the KPathInfo auxiliary signature.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
val : Any
|
|
309
|
+
The value read from JSON (a list of 2-4 elements).
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
tuple[str, tuple[str, ...], str, str]
|
|
314
|
+
Reconstructed ``(mode, labels, comment, coordinate_mode)``.
|
|
315
|
+
"""
|
|
316
|
+
mode: str = str(val[0])
|
|
317
|
+
labels: tuple[str, ...] = tuple(str(s) for s in val[1])
|
|
318
|
+
comment: str = (
|
|
319
|
+
str(val[2]) if len(val) >= _KPATH_AUX_WITH_COMMENT_LEN else ""
|
|
320
|
+
)
|
|
321
|
+
coordinate_mode: str = (
|
|
322
|
+
str(val[3]) if len(val) >= _KPATH_AUX_WITH_COORD_MODE_LEN else ""
|
|
323
|
+
)
|
|
324
|
+
return (mode, labels, comment, coordinate_mode)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _encode_volumetric_aux(
|
|
328
|
+
aux: tuple[tuple[int, int, int], tuple[str, ...]],
|
|
329
|
+
) -> list[Any]:
|
|
330
|
+
"""Encode VolumetricData auxiliary data for JSON storage.
|
|
331
|
+
|
|
332
|
+
Extended Summary
|
|
333
|
+
----------------
|
|
334
|
+
VolumetricData and SOCVolumetricData store
|
|
335
|
+
``(grid_shape, symbols)`` as their PyTree auxiliary data. This
|
|
336
|
+
encoder converts the nested tuple into a JSON-serializable list
|
|
337
|
+
``[[NGX, NGY, NGZ], [symbol_str, ...]]``.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
aux : tuple[tuple[int, int, int], tuple[str, ...]]
|
|
342
|
+
The volumetric auxiliary data:
|
|
343
|
+
``(grid_shape, symbols)``.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
list[Any]
|
|
348
|
+
JSON-serializable nested list representation.
|
|
349
|
+
"""
|
|
350
|
+
grid_shape: tuple[int, int, int]
|
|
351
|
+
symbols: tuple[str, ...]
|
|
352
|
+
grid_shape, symbols = aux
|
|
353
|
+
result: list[Any] = [list(grid_shape), list(symbols)]
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _decode_volumetric_aux(
|
|
358
|
+
val: Any, # noqa: ANN401
|
|
359
|
+
) -> tuple[tuple[int, int, int], tuple[str, ...]]:
|
|
360
|
+
"""Decode JSON list back to VolumetricData auxiliary data.
|
|
361
|
+
|
|
362
|
+
Extended Summary
|
|
363
|
+
----------------
|
|
364
|
+
Inverse of :func:`_encode_volumetric_aux`. Converts the nested
|
|
365
|
+
JSON list ``[[NGX, NGY, NGZ], [symbol_str, ...]]`` back into the
|
|
366
|
+
Python tuple ``(grid_shape, symbols)`` expected by the
|
|
367
|
+
VolumetricData / SOCVolumetricData ``tree_unflatten`` method.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
val : Any
|
|
372
|
+
The value read from JSON (a list of two sub-lists).
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
tuple[tuple[int, int, int], tuple[str, ...]]
|
|
377
|
+
Reconstructed ``(grid_shape, symbols)``.
|
|
378
|
+
"""
|
|
379
|
+
grid_shape: tuple[int, int, int] = (
|
|
380
|
+
int(val[0][0]),
|
|
381
|
+
int(val[0][1]),
|
|
382
|
+
int(val[0][2]),
|
|
383
|
+
)
|
|
384
|
+
symbols: tuple[str, ...] = tuple(str(s) for s in val[1])
|
|
385
|
+
return (grid_shape, symbols)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
_PYTREE_REGISTRY: dict[str, _PyTreeMeta] = {
|
|
389
|
+
"DensityOfStates": _PyTreeMeta(
|
|
390
|
+
cls=DensityOfStates,
|
|
391
|
+
children_fields=(
|
|
392
|
+
"energy",
|
|
393
|
+
"total_dos",
|
|
394
|
+
"fermi_energy",
|
|
395
|
+
),
|
|
396
|
+
aux_encoder=_encode_none,
|
|
397
|
+
aux_decoder=_decode_none,
|
|
398
|
+
),
|
|
399
|
+
"BandStructure": _PyTreeMeta(
|
|
400
|
+
cls=BandStructure,
|
|
401
|
+
children_fields=(
|
|
402
|
+
"eigenvalues",
|
|
403
|
+
"kpoints",
|
|
404
|
+
"kpoint_weights",
|
|
405
|
+
"fermi_energy",
|
|
406
|
+
),
|
|
407
|
+
aux_encoder=_encode_none,
|
|
408
|
+
aux_decoder=_decode_none,
|
|
409
|
+
),
|
|
410
|
+
"ArpesSpectrum": _PyTreeMeta(
|
|
411
|
+
cls=ArpesSpectrum,
|
|
412
|
+
children_fields=(
|
|
413
|
+
"intensity",
|
|
414
|
+
"energy_axis",
|
|
415
|
+
),
|
|
416
|
+
aux_encoder=_encode_none,
|
|
417
|
+
aux_decoder=_decode_none,
|
|
418
|
+
),
|
|
419
|
+
"OrbitalProjection": _PyTreeMeta(
|
|
420
|
+
cls=OrbitalProjection,
|
|
421
|
+
children_fields=(
|
|
422
|
+
"projections",
|
|
423
|
+
"spin",
|
|
424
|
+
"oam",
|
|
425
|
+
),
|
|
426
|
+
aux_encoder=_encode_none,
|
|
427
|
+
aux_decoder=_decode_none,
|
|
428
|
+
),
|
|
429
|
+
"SpinOrbitalProjection": _PyTreeMeta(
|
|
430
|
+
cls=SpinOrbitalProjection,
|
|
431
|
+
children_fields=(
|
|
432
|
+
"projections",
|
|
433
|
+
"spin",
|
|
434
|
+
"oam",
|
|
435
|
+
),
|
|
436
|
+
aux_encoder=_encode_none,
|
|
437
|
+
aux_decoder=_decode_none,
|
|
438
|
+
),
|
|
439
|
+
"SimulationParams": _PyTreeMeta(
|
|
440
|
+
cls=SimulationParams,
|
|
441
|
+
children_fields=(
|
|
442
|
+
"energy_min",
|
|
443
|
+
"energy_max",
|
|
444
|
+
"sigma",
|
|
445
|
+
"gamma",
|
|
446
|
+
"temperature",
|
|
447
|
+
"photon_energy",
|
|
448
|
+
),
|
|
449
|
+
aux_encoder=_encode_int,
|
|
450
|
+
aux_decoder=_decode_int,
|
|
451
|
+
),
|
|
452
|
+
"PolarizationConfig": _PyTreeMeta(
|
|
453
|
+
cls=PolarizationConfig,
|
|
454
|
+
children_fields=(
|
|
455
|
+
"theta",
|
|
456
|
+
"phi",
|
|
457
|
+
"polarization_angle",
|
|
458
|
+
),
|
|
459
|
+
aux_encoder=_encode_str,
|
|
460
|
+
aux_decoder=_decode_str,
|
|
461
|
+
),
|
|
462
|
+
"KPathInfo": _PyTreeMeta(
|
|
463
|
+
cls=KPathInfo,
|
|
464
|
+
children_fields=(
|
|
465
|
+
"num_kpoints",
|
|
466
|
+
"label_indices",
|
|
467
|
+
"points_per_segment",
|
|
468
|
+
"segments",
|
|
469
|
+
"kpoints",
|
|
470
|
+
"weights",
|
|
471
|
+
"grid",
|
|
472
|
+
"shift",
|
|
473
|
+
),
|
|
474
|
+
aux_encoder=_encode_kpath_aux,
|
|
475
|
+
aux_decoder=_decode_kpath_aux,
|
|
476
|
+
),
|
|
477
|
+
"CrystalGeometry": _PyTreeMeta(
|
|
478
|
+
cls=CrystalGeometry,
|
|
479
|
+
children_fields=(
|
|
480
|
+
"lattice",
|
|
481
|
+
"reciprocal_lattice",
|
|
482
|
+
"coords",
|
|
483
|
+
"atom_counts",
|
|
484
|
+
),
|
|
485
|
+
aux_encoder=_encode_tuple_str,
|
|
486
|
+
aux_decoder=_decode_tuple_str,
|
|
487
|
+
),
|
|
488
|
+
"VolumetricData": _PyTreeMeta(
|
|
489
|
+
cls=VolumetricData,
|
|
490
|
+
children_fields=(
|
|
491
|
+
"lattice",
|
|
492
|
+
"coords",
|
|
493
|
+
"charge",
|
|
494
|
+
"magnetization",
|
|
495
|
+
"atom_counts",
|
|
496
|
+
),
|
|
497
|
+
aux_encoder=_encode_volumetric_aux,
|
|
498
|
+
aux_decoder=_decode_volumetric_aux,
|
|
499
|
+
),
|
|
500
|
+
"SOCVolumetricData": _PyTreeMeta(
|
|
501
|
+
cls=SOCVolumetricData,
|
|
502
|
+
children_fields=(
|
|
503
|
+
"lattice",
|
|
504
|
+
"coords",
|
|
505
|
+
"charge",
|
|
506
|
+
"magnetization",
|
|
507
|
+
"magnetization_vector",
|
|
508
|
+
"atom_counts",
|
|
509
|
+
),
|
|
510
|
+
aux_encoder=_encode_volumetric_aux,
|
|
511
|
+
aux_decoder=_decode_volumetric_aux,
|
|
512
|
+
),
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@beartype
|
|
517
|
+
def _dataset_write_kwargs(
|
|
518
|
+
data: np.ndarray,
|
|
519
|
+
compression: Optional[str],
|
|
520
|
+
compression_opts: Any, # noqa: ANN401
|
|
521
|
+
shuffle: bool,
|
|
522
|
+
fletcher32: bool,
|
|
523
|
+
chunks: Optional[Union[bool, tuple[int, ...]]],
|
|
524
|
+
) -> dict[str, Any]:
|
|
525
|
+
"""Build ``h5py.create_dataset`` keyword arguments for one child array.
|
|
526
|
+
|
|
527
|
+
Extended Summary
|
|
528
|
+
----------------
|
|
529
|
+
HDF5 storage filters (compression, shuffle, checksums) and chunking
|
|
530
|
+
are only valid for datasets with non-scalar dataspace. This helper
|
|
531
|
+
inspects the array dimensionality and returns the appropriate
|
|
532
|
+
keyword dictionary for ``h5py.Group.create_dataset``.
|
|
533
|
+
|
|
534
|
+
Implementation Logic
|
|
535
|
+
--------------------
|
|
536
|
+
1. For scalar datasets (``data.ndim == 0``), return an empty dict
|
|
537
|
+
since HDF5 filter/chunk flags are invalid for scalar dataspace.
|
|
538
|
+
2. For non-scalar datasets, conditionally include each supported
|
|
539
|
+
storage flag (``compression``, ``compression_opts``, ``shuffle``,
|
|
540
|
+
``fletcher32``, ``chunks``) only when the corresponding argument
|
|
541
|
+
is not ``None`` / ``False``.
|
|
542
|
+
|
|
543
|
+
Parameters
|
|
544
|
+
----------
|
|
545
|
+
data : np.ndarray
|
|
546
|
+
The NumPy array to be written. Its ``ndim`` determines whether
|
|
547
|
+
filters are applicable.
|
|
548
|
+
compression : Optional[str]
|
|
549
|
+
HDF5 compression filter name (e.g. ``"gzip"``, ``"lzf"``).
|
|
550
|
+
compression_opts : Any
|
|
551
|
+
Compression-specific options (e.g. gzip level 1-9).
|
|
552
|
+
shuffle : bool
|
|
553
|
+
Whether to enable the HDF5 byte-shuffle filter.
|
|
554
|
+
fletcher32 : bool
|
|
555
|
+
Whether to enable the Fletcher32 checksum filter.
|
|
556
|
+
chunks : Optional[Union[bool, tuple[int, ...]]]
|
|
557
|
+
Chunking policy: ``True`` for auto-chunking, or an explicit
|
|
558
|
+
chunk shape tuple.
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
dict[str, Any]
|
|
563
|
+
Keyword arguments to pass to ``h5py.Group.create_dataset``.
|
|
564
|
+
Empty dict for scalar datasets.
|
|
565
|
+
"""
|
|
566
|
+
if data.ndim == 0:
|
|
567
|
+
return {}
|
|
568
|
+
|
|
569
|
+
kwargs: dict[str, Any] = {}
|
|
570
|
+
if compression is not None:
|
|
571
|
+
kwargs["compression"] = compression
|
|
572
|
+
if compression_opts is not None:
|
|
573
|
+
kwargs["compression_opts"] = compression_opts
|
|
574
|
+
if shuffle:
|
|
575
|
+
kwargs["shuffle"] = True
|
|
576
|
+
if fletcher32:
|
|
577
|
+
kwargs["fletcher32"] = True
|
|
578
|
+
if chunks is not None:
|
|
579
|
+
kwargs["chunks"] = chunks
|
|
580
|
+
return kwargs
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@beartype
|
|
584
|
+
def save_to_h5(
|
|
585
|
+
path: Union[str, Path],
|
|
586
|
+
/,
|
|
587
|
+
*,
|
|
588
|
+
compression: Optional[str] = None,
|
|
589
|
+
compression_opts: Any = None, # noqa: ANN401
|
|
590
|
+
shuffle: bool = False,
|
|
591
|
+
fletcher32: bool = False,
|
|
592
|
+
chunks: Optional[Union[bool, tuple[int, ...]]] = None,
|
|
593
|
+
**pytrees: Any, # noqa: ANN401
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Save one or more named PyTrees to an HDF5 file.
|
|
596
|
+
|
|
597
|
+
Serializes each keyword-argument PyTree into a named HDF5
|
|
598
|
+
group. JAX array fields become HDF5 datasets (named by the
|
|
599
|
+
NamedTuple field name), and auxiliary data is stored as a
|
|
600
|
+
JSON-encoded group attribute.
|
|
601
|
+
|
|
602
|
+
Implementation Logic
|
|
603
|
+
--------------------
|
|
604
|
+
1. **Validate inputs**:
|
|
605
|
+
Ensure at least one PyTree is provided.
|
|
606
|
+
|
|
607
|
+
2. **Iterate over keyword arguments**:
|
|
608
|
+
For each ``(group_name, pytree)`` pair:
|
|
609
|
+
|
|
610
|
+
a. Look up ``type(pytree).__name__`` in
|
|
611
|
+
``_PYTREE_REGISTRY`` to obtain serialization metadata.
|
|
612
|
+
|
|
613
|
+
b. Call ``pytree.tree_flatten()`` to separate the PyTree
|
|
614
|
+
into ``(children, aux_data)``.
|
|
615
|
+
|
|
616
|
+
c. Create an HDF5 group named ``group_name``.
|
|
617
|
+
|
|
618
|
+
d. Store the type name as ``_pytree_type`` attribute and
|
|
619
|
+
the JSON-encoded aux_data as ``_aux_data_json``.
|
|
620
|
+
|
|
621
|
+
e. For each child field: if the value is ``None``
|
|
622
|
+
(Optional field), record the field name in
|
|
623
|
+
``_none_fields``; otherwise create an HDF5 dataset
|
|
624
|
+
from ``numpy.asarray(child)`` with optional storage
|
|
625
|
+
flags (compression/chunk/checksum) for non-scalar
|
|
626
|
+
datasets.
|
|
627
|
+
|
|
628
|
+
f. Store the ``_none_fields`` list as a JSON attribute.
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
path : Union[str, Path]
|
|
633
|
+
File path for the HDF5 file to create.
|
|
634
|
+
compression : Optional[str], optional
|
|
635
|
+
HDF5 compression filter name (e.g. ``"gzip"``, ``"lzf"``).
|
|
636
|
+
Applied to non-scalar datasets only.
|
|
637
|
+
compression_opts : Any, optional
|
|
638
|
+
Compression options passed through to h5py (e.g. gzip level).
|
|
639
|
+
Must be ``None`` when ``compression`` is ``None``.
|
|
640
|
+
shuffle : bool, optional
|
|
641
|
+
If True, enable HDF5 shuffle filter on non-scalar datasets.
|
|
642
|
+
fletcher32 : bool, optional
|
|
643
|
+
If True, enable HDF5 Fletcher32 checksum on non-scalar datasets.
|
|
644
|
+
chunks : Optional[Union[bool, tuple[int, ...]]], optional
|
|
645
|
+
Chunking policy for non-scalar datasets. ``True`` enables
|
|
646
|
+
auto-chunking, or provide an explicit chunk-shape tuple.
|
|
647
|
+
**pytrees : PyTree
|
|
648
|
+
Named PyTree instances. Each keyword argument name
|
|
649
|
+
becomes an HDF5 group name.
|
|
650
|
+
|
|
651
|
+
Raises
|
|
652
|
+
------
|
|
653
|
+
ValueError
|
|
654
|
+
If no PyTrees are provided.
|
|
655
|
+
ValueError
|
|
656
|
+
If ``compression_opts`` is provided without ``compression``.
|
|
657
|
+
TypeError
|
|
658
|
+
If a PyTree's class is not in the registry.
|
|
659
|
+
|
|
660
|
+
Notes
|
|
661
|
+
-----
|
|
662
|
+
Scalar datasets (shape ``()``) are always written without HDF5
|
|
663
|
+
filter/chunk flags because those options are invalid for scalar
|
|
664
|
+
dataspace in HDF5.
|
|
665
|
+
"""
|
|
666
|
+
if not pytrees:
|
|
667
|
+
msg = "At least one PyTree must be provided."
|
|
668
|
+
raise ValueError(msg)
|
|
669
|
+
if compression is None and compression_opts is not None:
|
|
670
|
+
msg = "compression_opts requires compression to be set."
|
|
671
|
+
raise ValueError(msg)
|
|
672
|
+
|
|
673
|
+
file_path: Path = Path(path)
|
|
674
|
+
with h5py.File(file_path, "w") as f:
|
|
675
|
+
for group_name, pytree in pytrees.items():
|
|
676
|
+
type_name: str = type(pytree).__name__
|
|
677
|
+
if type_name not in _PYTREE_REGISTRY:
|
|
678
|
+
msg = f"Unsupported PyTree type: {type_name}"
|
|
679
|
+
raise TypeError(msg)
|
|
680
|
+
|
|
681
|
+
meta: _PyTreeMeta = _PYTREE_REGISTRY[type_name]
|
|
682
|
+
children: list[Any]
|
|
683
|
+
aux_data: Any
|
|
684
|
+
children, aux_data = pytree.tree_flatten()
|
|
685
|
+
|
|
686
|
+
grp: h5py.Group = f.create_group(group_name)
|
|
687
|
+
grp.attrs[_ATTR_TYPE] = type_name
|
|
688
|
+
aux_serializable: Any = meta.aux_encoder(aux_data)
|
|
689
|
+
grp.attrs[_ATTR_AUX] = json.dumps(aux_serializable)
|
|
690
|
+
|
|
691
|
+
none_fields: list[str] = []
|
|
692
|
+
for field_name, child in zip(
|
|
693
|
+
meta.children_fields,
|
|
694
|
+
children,
|
|
695
|
+
strict=True,
|
|
696
|
+
):
|
|
697
|
+
if child is None:
|
|
698
|
+
none_fields.append(field_name)
|
|
699
|
+
else:
|
|
700
|
+
child_arr: np.ndarray = np.asarray(child)
|
|
701
|
+
ds_kwargs: dict[str, Any] = _dataset_write_kwargs(
|
|
702
|
+
data=child_arr,
|
|
703
|
+
compression=compression,
|
|
704
|
+
compression_opts=compression_opts,
|
|
705
|
+
shuffle=shuffle,
|
|
706
|
+
fletcher32=fletcher32,
|
|
707
|
+
chunks=chunks,
|
|
708
|
+
)
|
|
709
|
+
grp.create_dataset(
|
|
710
|
+
field_name,
|
|
711
|
+
data=child_arr,
|
|
712
|
+
**ds_kwargs,
|
|
713
|
+
)
|
|
714
|
+
grp.attrs[_ATTR_NONE] = json.dumps(none_fields)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
@beartype
|
|
718
|
+
def load_from_h5(
|
|
719
|
+
path: Union[str, Path],
|
|
720
|
+
name: Optional[str] = None,
|
|
721
|
+
) -> Any: # noqa: ANN401
|
|
722
|
+
"""Load PyTrees from an HDF5 file.
|
|
723
|
+
|
|
724
|
+
Deserializes HDF5 groups back into diffpes PyTree objects
|
|
725
|
+
by reading datasets as JAX arrays and reconstructing the
|
|
726
|
+
NamedTuple via ``tree_unflatten``.
|
|
727
|
+
|
|
728
|
+
Implementation Logic
|
|
729
|
+
--------------------
|
|
730
|
+
1. **Open the HDF5 file** for reading.
|
|
731
|
+
|
|
732
|
+
2. **Select groups to load**:
|
|
733
|
+
If ``name`` is provided, load only that group. If
|
|
734
|
+
``name`` is ``None``, load all top-level groups.
|
|
735
|
+
|
|
736
|
+
3. **For each group**:
|
|
737
|
+
|
|
738
|
+
a. Read ``_pytree_type`` attribute and look up the class
|
|
739
|
+
in ``_PYTREE_REGISTRY``.
|
|
740
|
+
|
|
741
|
+
b. Read ``_aux_data_json`` attribute and decode it via
|
|
742
|
+
the type-specific ``aux_decoder``.
|
|
743
|
+
|
|
744
|
+
c. Read ``_none_fields`` attribute (defaulting to empty
|
|
745
|
+
list).
|
|
746
|
+
|
|
747
|
+
d. For each children field name: if the name appears in
|
|
748
|
+
``_none_fields``, set the child to ``None``;
|
|
749
|
+
otherwise read the HDF5 dataset and convert to a JAX
|
|
750
|
+
array via ``jnp.asarray``.
|
|
751
|
+
|
|
752
|
+
e. Call ``cls.tree_unflatten(aux_data, children)`` to
|
|
753
|
+
reconstruct the PyTree.
|
|
754
|
+
|
|
755
|
+
Parameters
|
|
756
|
+
----------
|
|
757
|
+
path : Union[str, Path]
|
|
758
|
+
File path to the HDF5 file to read.
|
|
759
|
+
name : Optional[str], optional
|
|
760
|
+
Name of a specific group to load. If ``None``, all
|
|
761
|
+
groups are loaded and returned as a dict.
|
|
762
|
+
|
|
763
|
+
Returns
|
|
764
|
+
-------
|
|
765
|
+
result : PyTree or dict[str, PyTree]
|
|
766
|
+
A single PyTree if ``name`` is given, otherwise a dict
|
|
767
|
+
mapping group names to PyTree instances.
|
|
768
|
+
|
|
769
|
+
Raises
|
|
770
|
+
------
|
|
771
|
+
KeyError
|
|
772
|
+
If ``name`` is specified but does not exist in the file.
|
|
773
|
+
TypeError
|
|
774
|
+
If a group's ``_pytree_type`` is not in the registry.
|
|
775
|
+
"""
|
|
776
|
+
file_path: Path = Path(path)
|
|
777
|
+
|
|
778
|
+
def _load_group(
|
|
779
|
+
grp: h5py.Group,
|
|
780
|
+
) -> Any: # noqa: ANN401
|
|
781
|
+
type_name: str = str(grp.attrs[_ATTR_TYPE])
|
|
782
|
+
if type_name not in _PYTREE_REGISTRY:
|
|
783
|
+
msg = f"Unknown PyTree type: {type_name}"
|
|
784
|
+
raise TypeError(msg)
|
|
785
|
+
|
|
786
|
+
meta: _PyTreeMeta = _PYTREE_REGISTRY[type_name]
|
|
787
|
+
aux_json: Any = json.loads(str(grp.attrs[_ATTR_AUX]))
|
|
788
|
+
aux_data: Any = meta.aux_decoder(aux_json)
|
|
789
|
+
|
|
790
|
+
none_fields: list[str] = json.loads(str(grp.attrs[_ATTR_NONE]))
|
|
791
|
+
|
|
792
|
+
children: list[Any] = []
|
|
793
|
+
for field_name in meta.children_fields:
|
|
794
|
+
if field_name in none_fields:
|
|
795
|
+
children.append(None)
|
|
796
|
+
else:
|
|
797
|
+
arr: np.ndarray = grp[field_name][()]
|
|
798
|
+
children.append(jnp.asarray(arr))
|
|
799
|
+
|
|
800
|
+
return meta.cls.tree_unflatten(aux_data, tuple(children))
|
|
801
|
+
|
|
802
|
+
with h5py.File(file_path, "r") as f:
|
|
803
|
+
if name is not None:
|
|
804
|
+
if name not in f:
|
|
805
|
+
msg = f"Group '{name}' not found in {path}"
|
|
806
|
+
raise KeyError(msg)
|
|
807
|
+
return _load_group(f[name])
|
|
808
|
+
|
|
809
|
+
result: dict[str, Any] = {}
|
|
810
|
+
for group_name in f:
|
|
811
|
+
result[group_name] = _load_group(f[group_name])
|
|
812
|
+
return result
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
__all__: list[str] = [
|
|
816
|
+
"load_from_h5",
|
|
817
|
+
"save_to_h5",
|
|
818
|
+
]
|