PyTree Architecture¶
diffpes.types defines every structured value as a JAX PyTree. These values
include band structures, orbital projections, spectra, and simulation
parameters. This guide describes the available types and their validation.
It also describes JAX transformations, HDF5 persistence, and Equinox modules.
The Single-Home Rule¶
diffpes enforces one structural rule: all types live in diffpes.types.
The src/diffpes/types/ directory owns every PyTree, type alias, and
make_* factory. The consuming subpackages import these types from
diffpes.types. They do not define local carriers.
The inverse problem requires this rule. The fitting layer compares
ArpesSpectrum objects from simulations, measurements, and different
parameter sets. Therefore, one ArpesSpectrum must have one import surface
and one validation contract. A solver result container is also a type. Define
it in diffpes.types.
The Type Inventory¶
The package groups types by physics domain, with one module for each family:
Module |
PyTrees |
Factories |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
scalar aliases (below) |
The following carriers define important contracts:
BandStructurecontainseigenvalues [K, B],kpoints [K, 3],kpoint_weights [K],fermi_energy(0-d). The spin-resolved variantSpinBandStructurecarrieseigenvalues_up/eigenvalues_downinstead.OrbitalProjectioncontainsprojections [K, B, A, 9]in the VASP ordering, plus optionalspin [K, B, A, 6]andoam [K, B, A, 3]channels.SpinOrbitalProjectionuses the same shapes but requiresspin. This contract distinguishes optional spin data from required SOC data.ArpesSpectrumcontains the simulation output:intensity [K, E]andenergy_axis [E].SimulationParamscontains six 0-d float arrays (energy_min,energy_max,sigma,gamma,temperature,photon_energy) plus one Pythonint,fidelity, which sets the energy-axis length.SelfEnergyConfigcontainscoefficients [P], optionalenergy_nodes [P], and amodestring. Its coefficients are differentiable, sojax.gradsupports self-energy fitting.
The scalar aliases in types/aliases.py accept Python scalars and 0-d JAX
arrays. The aliases include ScalarFloat, ScalarInteger, ScalarComplex,
ScalarBool, ScalarNumeric, and NonJaxNumber. Therefore, APIs accept both
sigma=0.04 and sigma=jnp.asarray(0.04).
Registration: Children vs. Auxiliary Data¶
Every carrier is an eqx.Module. Field declarations automatically define
the flatten and unflatten operations. The fields have two categories:
Children are JAX arrays. JAX traces, differentiates, and batches them. Examples include eigenvalues, projections, intensities, and broadening widths.
Auxiliary data is static Python metadata, declared with
eqx.field(static=True). This metadata is a compile-time constant. A change retriggers compilation for eachjit-compiled function that receives the PyTree.
Two deliberate examples of auxiliary data:
SimulationParams.fidelityis a Pythonintbecause it sets the energy-axis length. JAX requires static shapes underjit.PolarizationConfig.polarization_typeis a Pythonstr("LVP","LHP","RCP","LCP","LAP","unpolarized"). It selects branches in the matrix-element calculation.
JAX does not trace these fields. A gradient for an energy-point count or a polarization label has no physical meaning.
Factories and Two-Tier Validation¶
Use the make_* factories to construct PyTrees. Each factory validates inputs
and casts float arrays to float64. Validation has two tiers:
Static checks resolve at trace time and use ordinary Python errors. The
@jaxtyped(typechecker=beartype)decorator enforces shape consistency. For example, unequalKdimensions ineigenvaluesandkpointsfail immediately. Structural checks raiseValueError.make_slater_paramsrejects azetalength that differs from the orbital basis.make_self_energy_configrejectsmode="tabulated"withoutenergy_nodes.Traced checks are data-dependent (finiteness, non-negativity) and cannot use Python
ifunderjit. The factories useequinox.error_ifto keep these checks traceable.
import jax.numpy as jnp
from diffpes.types import make_band_structure
bands = make_band_structure(
eigenvalues=jnp.linspace(-2.0, 0.5, 100).reshape(20, 5), # [K, B]
kpoints=jnp.zeros((20, 3)), # [K, 3]
fermi_energy=0.0,
)
print(bands.eigenvalues.shape, float(bands.fermi_energy)) # (20, 5) 0.0
When omitted, kpoint_weights uses uniform weights. Every float field returns
as a float64 JAX array because diffpes enables x64 at import. See
JAX Transformability and Gradients.
Immutability and the jit/grad/vmap Flow¶
eqx.Module keeps PyTrees immutable, so field assignment raises an error.
Updates use functional operations:
eqx.tree_at(lambda t: t.fermi_energy, bands, jnp.asarray(0.1)) builds a
new instance with one changed leaf. This behavior lets JAX transformations
assume that traced values do not change unexpectedly.
JAX transformations can accept a whole carrier because children are ordinary leaves:
import jax
import jax.numpy as jnp
from diffpes.simul import simulate_expanded
eigenbands = jnp.linspace(-2.0, 0.5, 100).reshape(20, 5)
surface_orb = jnp.ones((20, 5, 2, 9)) * 0.1
def peak(ef):
spectrum = simulate_expanded(
level="basic", eigenbands=eigenbands, surface_orb=surface_orb,
ef=ef, sigma=0.04, fidelity=500,
temperature=15.0, photon_energy=11.0,
)
return jnp.max(spectrum.intensity)
print(jax.grad(peak)(0.0)) # d(peak intensity)/d(E_F), a 0-d array
The gradient flows through the ArpesSpectrum PyTree without manual
unpacking. JAX flattens the carrier, differentiates its leaves, and
reassembles it. vmap uses the same process. A photon-energy batch adds a
leading batch axis to ArpesSpectrum.intensity.
HDF5 Round-Trip¶
diffpes.inout.hdf5 preserves any registered PyTree without data loss. Each
named PyTree becomes an HDF5 group. Array children become datasets named after
their fields. A JSON group attribute stores auxiliary data. The
_none_fields attribute records optional fields that contain None.
from diffpes.inout import load_from_h5, save_to_h5
save_to_h5("run.h5", bands=bands, spectrum=spectrum)
everything = load_from_h5("run.h5") # dict: {"bands": ..., "spectrum": ...}
bands_back = load_from_h5("run.h5", name="bands") # single PyTree
The _pytree_type attribute stores the type name. load_from_h5 uses
tree_unflatten to reconstruct the exact class and its static auxiliary data.
Unknown type names raise an error. The loader does not return a lossy array
dictionary. See
VASP Data Ingestion for the full ingest-simulate-save
pipeline.
The Equinox Module Pattern¶
The contributing guide
defines the diffpes.types architecture. Every structured type is an
Equinox module (eqx.Module). The eqx.field(static=True) declaration
marks static metadata:
import equinox as eqx
from jaxtyping import Array, Float
class BandStructure(eqx.Module):
eigenvalues: Float[Array, "K B"]
kpoints: Float[Array, "K 3"]
kpoint_weights: Float[Array, " K"]
fermi_energy: Float[Array, ""]
This pattern implements the child and auxiliary-data split without custom
registration. eqx.Module derives flattening operations from the field
declarations. eqx.field(static=True) identifies metadata such as fidelity
and polarization_type. The zero-legacy migration removed the earlier
hand-registered NamedTuple form. The user contract remains unchanged. Use
validated, immutable PyTrees from diffpes.types.