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.
Files changed (55) hide show
  1. diffpes/__init__.py +65 -0
  2. diffpes/inout/__init__.py +88 -0
  3. diffpes/inout/chgcar.py +459 -0
  4. diffpes/inout/doscar.py +248 -0
  5. diffpes/inout/eigenval.py +258 -0
  6. diffpes/inout/hdf5.py +818 -0
  7. diffpes/inout/helpers.py +295 -0
  8. diffpes/inout/kpoints.py +433 -0
  9. diffpes/inout/plotting.py +757 -0
  10. diffpes/inout/poscar.py +174 -0
  11. diffpes/inout/procar.py +331 -0
  12. diffpes/inout/py.typed +0 -0
  13. diffpes/maths/__init__.py +68 -0
  14. diffpes/maths/dipole.py +368 -0
  15. diffpes/maths/gaunt.py +496 -0
  16. diffpes/maths/spherical_harmonics.py +336 -0
  17. diffpes/py.typed +0 -0
  18. diffpes/radial/__init__.py +47 -0
  19. diffpes/radial/bessel.py +198 -0
  20. diffpes/radial/integrate.py +128 -0
  21. diffpes/radial/wavefunctions.py +335 -0
  22. diffpes/simul/__init__.py +146 -0
  23. diffpes/simul/broadening.py +258 -0
  24. diffpes/simul/crosssections.py +196 -0
  25. diffpes/simul/expanded.py +1021 -0
  26. diffpes/simul/forward.py +586 -0
  27. diffpes/simul/oam.py +112 -0
  28. diffpes/simul/polarization.py +443 -0
  29. diffpes/simul/py.typed +0 -0
  30. diffpes/simul/resolution.py +122 -0
  31. diffpes/simul/self_energy.py +142 -0
  32. diffpes/simul/spectrum.py +1116 -0
  33. diffpes/simul/workflow.py +424 -0
  34. diffpes/tightb/__init__.py +68 -0
  35. diffpes/tightb/diagonalize.py +340 -0
  36. diffpes/tightb/hamiltonian.py +286 -0
  37. diffpes/tightb/projections.py +134 -0
  38. diffpes/types/__init__.py +162 -0
  39. diffpes/types/aliases.py +52 -0
  40. diffpes/types/bands.py +1064 -0
  41. diffpes/types/dos.py +510 -0
  42. diffpes/types/geometry.py +306 -0
  43. diffpes/types/kpath.py +365 -0
  44. diffpes/types/params.py +496 -0
  45. diffpes/types/py.typed +0 -0
  46. diffpes/types/radial_params.py +482 -0
  47. diffpes/types/self_energy.py +271 -0
  48. diffpes/types/tb_model.py +531 -0
  49. diffpes/types/volumetric.py +608 -0
  50. diffpes/utils/__init__.py +30 -0
  51. diffpes/utils/math.py +255 -0
  52. diffpes/utils/py.typed +0 -0
  53. diffpes-2026.3.1.dist-info/METADATA +176 -0
  54. diffpes-2026.3.1.dist-info/RECORD +55 -0
  55. 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
+ ]