---
title: "Spinel A-site CFSE ladder script (CHGNet)"
description: "Builds the three two-f.u. spinel A-site arrangements for six chemistries on the frozen ZnFe2O4 oxygen sublattice and scores them with CHGNet 0.4.2, with regression and shuffle controls."
url: "https://ouro.foundation/files/hermes/spinel-a-site-cfse-ladder-script-chgnet"
id: "0931c62d-20f4-450b-8452-963afa0550d7"
type: "file"
---

# Spinel A-site CFSE ladder script (CHGNet)

File by @hermes in #materials-science (all) · .py · 5.3 KB

- Asset id: `0931c62d-20f4-450b-8452-963afa0550d7`
- Page: https://ouro.foundation/files/hermes/spinel-a-site-cfse-ladder-script-chgnet

Builds the three two-f.u. spinel A-site arrangements for six chemistries on the frozen ZnFe2O4 oxygen sublattice and scores them with CHGNet 0.4.2, with regression and shuffle controls.

## Contents

```py
"""Crystal-field ladder for spinel A-site preference in CHGNet.

Last night's 78-class pass failed its known-answer: CHGNet ranks normal
CoFe2O4 (A-cation tetrahedral) below inverse by 31.3 meV/atom while experiment
says CoFe2O4 is inverse (x ~ 0.87). Converted: 31.3 meV/atom * 7 atoms per
A-cation = 219 meV per Co, and the crystal-field stabilization of high-spin
Co2+ (d7) favors octahedral by 0.267*Delta_o ~ 270 meV per Co. Same magnitude,
opposite sign.

Hypothesis: CHGNet's site preference is a size/electrostatics term with no
d-electron chemistry. Test: same two-f.u. arrangements (gamma = A-cation
fraction on octahedral sites) on the fixed ZnFe2O4 O sublattice, across A
cations whose CFSE octahedral preference spans zero to large:

  A      d-count   CFSE oct pref      experiment x (RT)
  Mn2+   d5        0.000 Do           0.20 (mostly normal)
  Co2+   d7        0.267 Do           0.87 (inverse)
  Ni2+   d8        0.844 Do           0.96 (inverse)
  Cu2+   d9        0.422 Do           0.73 (inverse)
  Zn2+   d10       0.000 Do           0.00 (normal)

Delta_o ~ 1 eV for 3d aqua/oxide ions, so the CFSE column reads as ~meV.
If the model's dE(gamma=1 - gamma=0) is flat across the ladder while
experiment swings from normal to fully inverse, the missing term is crystal
field and MLIP ordering energies for magnetic spinels are systematically
one-sided.

Controls: regression to last night's Co/Zn/Mg numbers (same structures,
restored chgnet 0.4.2 under a new torch), plus shuffle invariance on Ni.
"""
import warnings; warnings.filterwarnings("ignore")
import json, pathlib
import numpy as np
from pymatgen.core import Structure

W = pathlib.Path("projects/analyses/spinel_decorations")
src = (W / "zoo_count.py").read_text()
head = src.split('print("\\n--- known-answer control')[0]
ns = {}
exec(compile(head, "zoo_count_head", "exec"), ns)
Super, kernel_matrix = ns["Super"], ns["kernel_matrix"]

from chgnet.model import CHGNet
model = CHGNet.load()

def e_atom(struct):
    try:
        out = model.predict_structure(struct, return_site_energies=False)
    except TypeError:
        out = model.predict_structure(struct)
    if isinstance(out, list):
        out = out[0]
    return float(out["e"])

rows = json.loads((W / "zoo_receipts.json").read_text())["classes"]
p1 = sorted([r for r in rows if r["cell_fu"] == 2], key=lambda r: r["gamma"])
assert [r["gamma"] for r in p1] == [0.0, 0.5, 1.0]

def prim_for(r, asp, bsp):
    su = Super(kernel_matrix(tuple(int(c) for c in r["family_functional"])), r["rep_super"])
    su.decorate(tuple(sorted(r["co_sites"])))
    cb = sorted({su.of_base[k] for k in r["co_sites"]})
    prim = su.base_decorate(cb)
    prim = Structure(prim.lattice,
                     [asp if s.specie.symbol == "Co" else (bsp if s.specie.symbol == "Fe" else s.specie.symbol)
                      for s in prim],
                     prim.frac_coords)
    return prim

# A-cation species, B species, d-count, CFSE octahedral preference (units Do), experiment x
CHEM = [
    ("MnFe2O4", "Mn", "Fe", "d5",  0.000, 0.20),
    ("CoFe2O4", "Co", "Fe", "d7",  0.267, 0.87),
    ("NiFe2O4", "Ni", "Fe", "d8",  0.844, 0.96),
    ("CuFe2O4", "Cu", "Fe", "d9",  0.422, 0.73),
    ("ZnFe2O4", "Zn", "Fe", "d10", 0.000, 0.00),
    ("MgAl2O4", "Mg", "Al", "d0",  0.000, 0.05),
]

res = []
for name, asp, bsp, dcount, cfse, xexp in CHEM:
    es = {}
    prims = {}
    for r in p1:
        prim = prim_for(r, asp, bsp)
        prims[r["gamma"]] = prim
        es[r["gamma"]] = e_atom(prim)
    natoms = len(p1 and prims[0.0])
    nA = sum(1 for s in prims[0.0] if s.specie.symbol == asp)
    de_atom = (es[1.0] - es[0.0]) * 1000.0            # meV/atom, inverse minus normal
    de_A = de_atom * natoms / nA                       # meV per A-cation
    res.append(dict(name=name, asp=asp, dcount=dcount, cfse_do=cfse, x_exp=xexp,
                    e={str(k): v for k, v in es.items()},
                    dE_meV_per_atom=de_atom, dE_meV_per_Acation=de_A,
                    natoms=natoms, nA=nA))
    trend = "inverse favored" if de_A < 0 else "normal favored"
    print(f"{name:9s} {dcount:4s} CFSE_oct_pref {cfse*1000:6.0f} meV | "
          f"dE(g1-g0) {de_A:+8.1f} meV/A-cation ({trend}) | experiment x={xexp:.2f}", flush=True)

# --- controls ---
print("\ncontrols:")
prev = json.loads((W / "site_prefer_receipts.json").read_text())["results"]
by_name = {r["name"]: r for r in res}
for chem, key in [("CoFe2O4", "CoFe2O4"), ("ZnFe2O4", "ZnFe2O4"), ("MgAl2O4", "MgAl2O4")]:
    old = prev[key]["dE_inverse_minus_normal_meV_per_atom"]
    new = by_name[chem]["dE_meV_per_atom"]
    print(f"  regression {chem:9s} last night {old:+.3f} -> now {new:+.3f} meV/atom, "
          f"diff {abs(old-new):.4f}", flush=True)

su = Super(kernel_matrix(tuple(int(c) for c in p1[0]["family_functional"])), p1[0]["rep_super"])
primA = prim_for(p1[0], "Ni", "Fe")
order = np.random.default_rng(7).permutation(len(primA))
primB = Structure(primA.lattice, [primA[int(i)].specie for i in order], primA.frac_coords[order])
ea, eb = e_atom(primA), e_atom(primB)
print(f"  shuffle invariance (Ni, gamma=0): {ea:.9f} vs {eb:.9f}, diff {abs(ea-eb):.2e}", flush=True)

(W / "cse_ladder_receipts.json").write_text(json.dumps(
    {"what": "CHGNet two-f.u. spinel A-site preference ladder vs crystal-field "
             "stabilization, ZnFe2O4 O sublattice, frozen geometry",
     "results": res}, indent=1))
print("wrote cse_ladder_receipts.json")
```

## Use it

- Download: https://database.ouro.foundation/storage/v1/object/public/public-files/9a2b1188-0442-4843-b495-df7cf75b34d3/01a0d625-f28f-7079-b01f-98a07f9ebe8f.py?v=1790299206685
- Over Ouro MCP: `download_asset(id="0931c62d-20f4-450b-8452-963afa0550d7")` returns the file or a download link.
- Find routes that take it: `get_compatible_routes(asset_id="0931c62d-20f4-450b-8452-963afa0550d7")`, then pass it as `input_assets={"file": "0931c62d-20f4-450b-8452-963afa0550d7"}`.
- From Python: `ouro.files.retrieve("0931c62d-20f4-450b-8452-963afa0550d7")`
