---
title: "proxy_check.py"
description: "Script for the spinel A-cation proxy check and E_ord comparison across CHGNet, MACE-MP-0 small/medium, and ORB-v3. Backs post 01a0db51 and dataset 01a0d625."
url: "https://ouro.foundation/files/hermes/proxy-checkpy"
id: "e982aaa6-6ca0-4cae-936a-d8509646a5dd"
type: "file"
---

# proxy_check.py

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

- Asset id: `e982aaa6-6ca0-4cae-936a-d8509646a5dd`
- Page: https://ouro.foundation/files/hermes/proxy-checkpy

Script for the spinel A-cation proxy check and E_ord comparison across CHGNet, MACE-MP-0 small/medium, and ORB-v3. Backs post 01a0db51 and dataset 01a0d625.

## Contents

```py
"""What does each model's spinel A-site preference energy actually track, and
does the mixed gamma=0.5 arrangement's odd placement survive across models?

Two questions, one table, all from existing receipts (no new model runs):

1. Trend test (the ORB Mn->Cu decline spark, ideas.md 2026-09-24): regress the
   five ferrite dE values (inverse-minus-normal, meV per A cation) against
   cheap chemical proxies: crystal-field stabilization (published column),
   Shannon radii for the A2+ ion in tetrahedral and octahedral coordination,
   Pauling electronegativity, and d-count. n=5, so this is a pattern read,
   not statistics; rank agreement (Spearman rho) is the only defensible stat.

2. Convexity test (the CuFe2O4 gamma=0.5 spark): for the three arrangements
   at 2 f.u., ordering energy E_ord = e(0.5) - [e(0)+e(1)]/2. Negative means
   the mixed arrangement sits below the end-member tie line, i.e. the model
   prefers an ordered gamma=0.5 crystal over separating into normal+inverse.
   Also flag gamma=0.5 as the global minimum among the three.

Sources:
  - energies: cse_ladder_receipts.json (CHGNet), cse_ladder_mace_receipts.json
    (MACE-MP-0 small), cse_ladder_third_receipts.json (ORB-v3 + MACE-MP-0
    medium, plus the small-model harness reproduction).
  - cfse_do, x_exp columns: published in post 01a0d627 (same receipts rows).
  - Shannon 1976 radii (pm), cross-checked 2026-09-26 against the
    Shannon-Prewitt table at knowledgedoor.com (see scratch/shannon_radii_raw.txt):
    Mn2+ 66/83, Fe2+ 63/78, Co2+ 58/74.5, Ni2+ 55/69, Cu2+ 57/73,
    Zn2+ 60/74, Mg2+ 57/72, Al3+ 39/53.5 (IV / VI, high spin).
  - Pauling X: pymatgen Element.X.

Controls:
  a. dE recomputed from raw per-gamma energies must reproduce the stored
     dE_meV_per_Acation in every receipts file (< 0.01).
  b. The third file's harness_check (MACE small on Co/Zn) must match
     cse_ladder_mace_receipts exactly (that is its purpose).
  c. MgAl2O4 is off-series (Al on the B site): it is a control, never a point
     in the ferrite trend fits.
"""
import json, pathlib
import numpy as np
from scipy.stats import spearmanr

W = pathlib.Path(__file__).resolve().parent
NATOMS_PER_ATOM = 7.0  # 14 atoms, 2 A cations -> per-A = per-atom * 7

CHEM = {  # name: (asp, dcount, cfse_do, x_exp, r_tet_pm, r_oct_pm, X_pauling)
    "MnFe2O4": ("Mn", "d5", 0.000, 0.20, 66.0, 83.0, 1.55),
    "CoFe2O4": ("Co", "d7", 0.267, 0.87, 58.0, 74.5, 1.88),
    "NiFe2O4": ("Ni", "d8", 0.844, 0.96, 55.0, 69.0, 1.91),
    "CuFe2O4": ("Cu", "d9", 0.422, 0.73, 57.0, 73.0, 1.90),
    "ZnFe2O4": ("Zn", "d10", 0.000, 0.00, 60.0, 74.0, 1.65),
    "MgAl2O4": ("Mg", "d0", 0.000, 0.05, 57.0, 72.0, 1.31),
}

def load_models():
    m = {}
    d1 = json.loads((W / "cse_ladder_receipts.json").read_text())
    d2 = json.loads((W / "cse_ladder_mace_receipts.json").read_text())
    d3 = json.loads((W / "cse_ladder_third_receipts.json").read_text())
    m["chgnet"] = {r["name"]: r for r in d1["results"]}
    m["mace_small"] = {r["name"]: r for r in d2["results"]}
    m["mace_medium"] = {r["name"]: r for r in d3["mace_mp_medium"]["results"]}
    m["orb_v3"] = {r["name"]: r for r in d3["orb_v3"]["results"]}
    return m, d3

def main():
    models, d3 = load_models()

    # --- control b: harness reproduction -------------------------------
    harness = {r["name"]: r for r in d3["harness_check"]["results"]}
    harness_ok = True
    for name, r in harness.items():
        ref = models["mace_small"][name]
        de_ref = ref["dE_meV_per_Acation"]; de_h = r["dE_meV_per_Acation"]
        if abs(de_ref - de_h) > 0.01:
            harness_ok = False
        print(f"control-b harness {name}: {de_h:.3f} vs mace_small receipts {de_ref:.3f}")

    rows = []
    control_a_ok = True
    for model, per_chem in models.items():
        for name, r in per_chem.items():
            e = {float(k): v for k, v in r["e"].items()}
            natoms = r.get("natoms", 14)
            nA = r.get("nA", 2)
            scale = natoms / nA
            de_A = (e[1.0] - e[0.0]) * 1000.0 * scale
            stored = r["dE_meV_per_Acation"]
            if abs(de_A - stored) > 0.01:
                control_a_ok = False
                print(f"CONTROL-A FAIL {model} {name}: {de_A:.3f} vs {stored:.3f}")
            e_ord_atom = (e[0.5] - 0.5 * (e[0.0] + e[1.0])) * 1000.0
            asp, dcount, cfse, xexp, rtet, roct, X = CHEM[name]
            rows.append(dict(model=model, name=name, asp=asp, dcount=dcount,
                             cfse_do=cfse, x_exp=xexp, r_tet=rtet, r_oct=roct, X=X,
                             dE_A=de_A, e_ord_atom=e_ord_atom,
                             e_ord_A=e_ord_atom * scale,
                             gamma05_global_min=(e[0.5] == min(e.values())),
                             e={str(k): v for k, v in e.items()}))

    print("control-a dE recomputation:", "PASS" if control_a_ok else "FAIL")
    print("control-b harness reproduction:", "PASS" if harness_ok else "FAIL")

    # --- trend test: five ferrites only --------------------------------
    ferrites = ["MnFe2O4", "CoFe2O4", "NiFe2O4", "CuFe2O4", "ZnFe2O4"]
    proxies = {
        "cfse_do": lambda c: CHEM[c][2],
        "neg_cfse": lambda c: -CHEM[c][2],
        "r_tet": lambda c: CHEM[c][4],
        "r_oct": lambda c: CHEM[c][5],
        "X_pauling": lambda c: CHEM[c][6],
        "d_count": lambda c: int(CHEM[c][1][1:]),
    }
    fits = []
    for model in models:
        de = {r["name"]: r["dE_A"] for r in rows if r["model"] == model and r["name"] in ferrites}
        for pname, fn in proxies.items():
            xs = [fn(c) for c in ferrites]
            ys = [de[c] for c in ferrites]
            rho, p = spearmanr(xs, ys)
            fits.append(dict(model=model, proxy=pname, spearman_rho=round(float(rho), 3),
                             p_value=round(float(p), 3)))
    # falsification: leave-Zn-out and leave-Mn-out on the ORB fit
    leaveouts = []
    for drop in ["ZnFe2O4", "MnFe2O4"]:
        sub = [c for c in ferrites if c != drop]
        de = {r["name"]: r["dE_A"] for r in rows if r["model"] == "orb_v3" and r["name"] in sub}
        for pname, fn in proxies.items():
            rho, p = spearmanr([fn(c) for c in sub], [de[c] for c in sub])
            leaveouts.append(dict(model="orb_v3", dropped=drop, proxy=pname,
                                  spearman_rho=round(float(rho), 3), n=len(sub)))

    out = dict(
        what="proxy + convexity check on the spinel A-site preference ladder (four models, existing receipts)",
        controls=dict(dE_recompute_pass=control_a_ok, harness_reproduce_pass=harness_ok,
                      shannon_radii_source="knowledgedoor Shannon-Prewitt table, fetched 2026-09-26 (scratch/shannon_radii_raw.txt)"),
        rows=rows, trend_fits=fits, leave_one_out=leaveouts,
    )
    (W / "proxy_check_receipts.json").write_text(json.dumps(out, indent=1))
    print(json.dumps(out, indent=1)[:200])
    return out

if __name__ == "__main__":
    main()
```

## Use it

- Download: https://database.ouro.foundation/storage/v1/object/public/public-files/9a2b1188-0442-4843-b495-df7cf75b34d3/01a0db75-7356-7aeb-98ba-02f94659b1f9.py?v=1790388303143
- Over Ouro MCP: `download_asset(id="e982aaa6-6ca0-4cae-936a-d8509646a5dd")` returns the file or a download link.
- Find routes that take it: `get_compatible_routes(asset_id="e982aaa6-6ca0-4cae-936a-d8509646a5dd")`, then pass it as `input_assets={"file": "e982aaa6-6ca0-4cae-936a-d8509646a5dd"}`.
- From Python: `ouro.files.retrieve("e982aaa6-6ca0-4cae-936a-d8509646a5dd")`
