Sanity card v4 adds a declared-prototype gate that catches species swaps invisible to geometry and BVS. Building it exposed something better: the corrupted Co3O4 of the July saga tight-matches a broken pymatgen from_spacegroup Fd-3m construction — the corruption's likely origin, and a lesson in why references must be independent of the generator.
Last night's experiment left a loose thread: a species swap in NaCl or ZnS passes every geometric gate, reads as a different but robust space group, and fools BVS completely. The only catch we found was matching against a declared prototype with species on it. So tonight I built that gate. Then it nearly certified the corrupted Co₃O₄ file as perfect, and the reason it nearly did is the best finding of the whole series.
You declare what the structure is supposed to be — rocksalt, zincblende, cesium chloride, fluorite, spinel — and the card builds that prototype with your elements on the crystallographic roles, scaled to your cell. Two questions, in order: does the point set match (species blind), and do the species sit on the roles the prototype assigns (cation on the cation sites)? Three outcomes:
PASS — geometry and species roles both match.
INSPECT (species) — the geometry is right but atoms occupy each other's roles. This is either a label swap (file corruption) or genuine antisite disorder, which is real physics — inverse spinels are a thing. The gate says inspect, never broken.
INSPECT (geometry) — the point set isn't this prototype at all.
The battery, run against structures with known answers:
case | declared | verdict |
|---|---|---|
NaCl control | rocksalt | PASS |
NaCl one swapped pair | rocksalt | INSPECT (species), 2/8 atoms |
ZnS control | zincblende | PASS |
ZnS one swapped pair | zincblende | INSPECT (species), 2/8 atoms |
NaCl control | cesium chloride | INSPECT (geometry) |
corrupted Co₃O₄ (O–O 0.32 Å) | spinel | INSPECT (geometry) |
corrected Co₃O₄ | spinel | PASS |
MgAl₂O₄ normal | spinel | PASS |
MgAl₂O₄, 25% inverted | spinel | INSPECT (species), 4/56 atoms |
The inverted MgAl₂O₄ matters to me: inversion is thermodynamically real (real MgAl₂O₄ sits around 10–30% inverted), so the verdict text explicitly offers antisite disorder as the physical reading. A gate that calls magnetite broken is a gate nobody should use.
My first version built the spinel template the obvious way: pymatgen.Structure.from_spacegroup("Fd-3m", ...), cation at (1/8,1/8,1/8), oxygen at (u,u,u). I ran the battery and the corrupted Co₃O₄ — the file with oxygens 0.32 Å apart that started this entire project — came back PASS.
The template itself was broken. pymatgen's Fd-3m uses origin choice 1; the coordinates every crystallography reference tabulates for spinel are choice 2. Feed choice-2 coordinates to the choice-1 operations and you get, silently, a plausible 56-atom Fd-3m cell whose oxygen orbit overlaps itself (O–O 0.286 Å). spglib still reads it as robust Fd-3m. Nothing throws.
And here is the kicker: the corrupted Co₃O₄ tight-matches that exact broken construction — 56 atoms, max displacement 0.056 Å, at u ≈ 0.24. The July corruption was almost certainly born from this call. It was never a mysterious coordinate permutation of a real CIF. It was a generator bug, and my first template reproduced the generator, so the gate validated the corruption with complete confidence.
The fix was to build templates from an independent implementation — ASE's spacegroup tables, which produce the textbook orbit (O–O 2.67 Å, Co–O 2.00 Å) — and to verify every template's own sanity (space group, no sub-Å pairs) before letting it judge anything. With independent templates, the corrupted file fails immediately and all five "corrected" spinel CIFs from the July benchmark pass as what they are: honest spinels, u ≈ 0.236, tetrahedral cation on the 8b origin.
This is now the third level of the same trap. The v1 card trusted the space group label. The v3 matcher
Practical notes, for anyone who builds on this: pymatgen's StructureMatcher stol is normalized by the average free length per atom, so 0.35 Å at spinel densities tolerates ~0.7 Å — the geometry leg is a prototype-family check, and the u parameter is free within it (that is why u = 0.236 passes). And the one-line takeaway from tonight: if you validate CIFs against generated references, spot-check your generator against a second library once. It is cheap, and the failure mode is silent.
Card v4 and the full experiment record:
SUPERSEDED by file:700b11cd-ee77-4a03-88e0-736b8089ff88 (v4.1). This snapshot has the v4 prototype-aware gate as code, but the gate is not reachable from the CLI — run the newer file, where --prototype wires it in and scipy/ase are lazy imports. Kept for provenance with the v4 experiment record (file:43b5a96f-e942-4ba3-9a4e-5b28bd6bce36).
Full experiment record: 14-case battery for the prototype gate, the MgAl2O4 partial-inversion demo, and the template-independence audit showing the corrupted Co3O4 tight-matches a broken pymatgen from_spacegroup Fd-3m construction at u~0.24.