Cluster 8 Ising Normalizing Flow
A generative neural network (a normalizing flow) that learns to sample plausible spin configurations of a small 2D magnet (the Ising model), at any coupling strength between neighboring spins. Trained as part of the 9-cluster Scientific AI Cluster Orchestration Framework, which pairs this network with an exact symbolic ("Symetria") critical- temperature formula and two safety checks under LangGraph supervision.
Unlike most of the other clusters in this series, which train a network to predict a single value (a temperature, a velocity, a pressure), this one trains a network to generate samples from an entire probability distribution β the same kind of technique used in modern physics simulation tools called "Boltzmann generators."
Architecture
| Type | Normalizing flow (RealNVP-style), 4 stacked blocks |
| Lattice | 4Γ4 grid of spins (16 total), wraparound edges |
| Input | Random noise (16 numbers) + coupling strength J |
| Output | 16 transformed values, one per spin |
| Parameters | ~35,000 |
A normalizing flow works by repeatedly stretching and squeezing a simple starting distribution (random noise) until it resembles a more complex target one. Each of the 4 blocks only transforms half its inputs at a time, using the other (untouched) half plus the coupling strength J to decide how β a standard trick (called "coupling layers") that keeps the transformation easy to invert and its effect on probability density easy to compute exactly.
Quickstart
import torch
from huggingface_hub import hf_hub_download
from modeling import ParallelNormalizingFlow, normalize_coupling
ckpt_path = hf_hub_download("dave1368/cluster-08-ising-flow", "normalizing_flow.pt")
# weights_only=False: the checkpoint is a dict with metadata (model_state_dict
# plus training info), not a bare tensor, so torch's default-safe loader can't
# be used as-is. Only do this for checkpoints you trust the source of.
checkpoint = torch.load(ckpt_path, map_location="cpu", weights_only=False)
model = ParallelNormalizingFlow(lattice_dim=16)
model.load_state_dict(checkpoint["model_state_dict"]) # checkpoint also carries training-time loss history, see training_metrics.json
model.eval()
# J = coupling strength (try values between 0.1 and 5.0)
j_norm = normalize_coupling(torch.tensor(1.0)).expand(1, 1)
noise = torch.randn(1, 16)
z, log_det = model(noise, j_norm)
soft_spins = torch.tanh(z) # 16 values, each between -1 and 1
print(soft_spins)
Training data
No fixed dataset β training data is generated fresh every step: random noise plus a randomly chosen coupling strength J. There's no single "correct answer" to fit here, unlike Clusters 1-7's field predictions; the network instead learns by minimizing a physics-based objective (variational free energy β a standard method from the Boltzmann-generator literature, e.g. Wu, Wang & Zhang 2019) computed fresh from its own current samples at each step:
- 4,000 training steps, batch size 2,048
- Coupling strength range: J β [0.1, 5.0]
- Simulated at one fixed reference temperature, not each J's own critical temperature (see Design notes below for why)
- Final training loss: β81.62 Β· Final validation loss: β81.89 (lower is better here β it's a free-energy estimate, not an error, so negative values are expected)
Design notes
Every layer of the flow is conditioned on the coupling strength J directly, so a single trained model covers the full slider range rather than being valid at only one coupling value.
Training and evaluation both use one fixed reference temperature, rather than each J's own critical temperature β a deliberate choice, not an oversight. Evaluating each J at its own critical point would actually erase J from the math entirely: at exactly the critical point, the physics has a genuine symmetry (a "universality" property well known in this field) that makes the quantity being minimized come out identical regardless of J. That would give the network nothing J-dependent to learn, defeating the point of conditioning on J at all. The fixed reference temperature is chosen so the low and high ends of the J slider land on opposite sides of the phase transition (ordered vs. disordered), so the coupling strength genuinely matters.
Validated against classical sources (post-deployment finding)
Cross-checked against Ising (1925), Bose & Einstein (1924), and Metropolis et al. (1953) β the papers cited in this cluster's Master Specification. Full data tables in the Space README.
| Check | Result |
|---|---|
| Exact critical-temperature formula vs. independent recomputation | Exact match at every tested coupling strength |
| Historical note: this formula is Onsager's (1944), not Ising's own (1925) result | Documented β Ising's 1925 paper found no transition in 1D and (incorrectly) guessed the same held in higher dimensions |
| Network's own "change in probability density" formula vs. an independent slower calculation | Match to floating-point precision |
| Does the coupling strength J actually change the output? | Yes β average sample energy scales roughly with J across the tested range |
| Does training actually improve the free-energy estimate? | Yes β trained model's estimate is lower (tighter) than an untrained copy's, at every tested coupling strength |
| "Maxwell relation" safety check β can it actually fail? | No β it's a basic calculus identity true for any smooth function, confirmed against random, deliberately-broken, and even physics-free models |
Limitations
- The "Maxwell relation" safety check downstream of this model can't actually distinguish a good model from a broken one β see the finding above. It confirms the surrounding code's calculus is implemented correctly, nothing more.
- The related "subadditivity" check also passed in every test we tried, including deliberately broken models β treat it as unproven-but- untested-to-fail, not confirmed reliable the way some other clusters' safety checks are.
- This network models soft spins (any value between β1 and 1), not the literal discrete Ising model (where spins are exactly β1 or +1) β a standard relaxation for training a smooth neural network, but it means outputs should be read as an approximation of the real system, not a literal sample from it.