Core Concepts#
tachys keeps data and functions apart. The data (configurations, parameters,
optimizer state) are immutable JAX pytrees. The functions that run a
simulation take data and return new data, without modifying their arguments;
they are compiled with jit and run in parallel on all available devices.
The data#
Three pytrees hold everything that changes during a run:
a
State, the configurations of the Markov chains;a
WaveFunction, the parameters of the network together with itsapplyfunction (Wavefunctions);the optimizer state, whatever the optimizer carries from one step to the next (Optimization).
A State holds the configurations of N_mc chains, spins or occupation
numbers, in an array whose leading axis runs over the chains. It also carries
the Lattice (Lattices) as static metadata: one lattice for the
whole batch, which JAX never traces or differentiates.
import jax.numpy as jnp
from tachys.lattice.lattice_database import chain
from tachys.lattice.spins.spin_state import SpinState
from tachys.lattice.fermions.fermion_state import FermionState
# two chains of 4 spins
spins = jnp.array([[1, -1, 1, -1],
[1, 1, -1, -1]], dtype=jnp.int8) # (N_mc, Ns)
s = SpinState(spins=spins, lattice=chain(4))
s.Ns # 4, read from the lattice
# one chain of 2 electrons on 2 sites: ↑ on site 0, ↓ on site 1
occupations = jnp.array([[1, 0, 0, 1]], dtype=jnp.int8) # (N_mc, 2 Ns)
f = FermionState(occupations=occupations, Ne=2, lattice=chain(2))
A state is never modified in place: replace returns a new state, and leaves
the original unchanged.
flipped = s.replace(spins=-s.spins) # s is unchanged
The functions#
A step of variational Monte Carlo applies three functions in turn.
sampleadvances the Markov chains bynsweepssweeps of the moveaction, with one PRNG key per chain. It returns the new configurations, their log-amplitudes and the acceptance rate:state, log_amps, acceptance = sample(nsweeps, state, action, mc_keys, wf)
compute_expectationevaluates an operator on the sample. It returns the local estimator on every configuration, its mean over the sample (the estimate of the expectation value), and the mean of its squared modulus:E_L, e_mean, e2_mean = compute_expectation(H, wf, state, log_amps)
The optimizer turns the local energies into a parameter update and a new optimizer state.
apply_gradientsthen returns a newWaveFunctionwith the updated parameters:updates, opt_state = optimizer(E_L, opt_state, state, wf) wf = wf.apply_gradients(updates, lr)
Together they make the training loop:
for step in range(N_steps):
key, subkey = jax.random.split(key)
mc_keys = jax.random.split(subkey, N_mc)
state, log_amps, acceptance = sample(1, state, action, mc_keys, wf)
E_L, e_mean, e2_mean = compute_expectation(H, wf, state, log_amps)
updates, opt_state = optimizer(E_L, opt_state, state, wf)
wf = wf.apply_gradients(updates, lr)
Each iteration rebinds state, wf and opt_state to new values, like the
carry of a jax.lax.scan; nothing is stored anywhere else. The
Quickstart gives runnable versions, and Foundation Models trains
one network on many Hamiltonians with the same loop.