The pipeline uses one global MILLER = (1, 1, 1) (pipeline.ipynb, Part 1 config) for every structure. The oxide branch of build_slab (models/structure.py) applies these indices to the spglib primitive cell, which has different consequences per oxide:
- **Cr₂O₃ (corundum): ** (111) of the rhombohedral primitive cell = the hexagonal (0001) basal plane — the standard, stable corundum surface. Correct as-is, no action needed.
- NiO (rock salt): (111) of the FCC-primitive cell = cubic NiO(111) — a polar Tasker type-III surface (alternating pure-Ni and pure-O planes). Polar terminations have formally divergent surface energy and heavily reconstruct in reality; H₂ dissociation barriers computed on an ideal polar slab are not physically meaningful. The stable nonpolar cleavage is NiO(100) (mixed Ni+O planes).
Note the index conversion trap: cubic (100) is not (1,0,0) of the primitive cell — in the FCC-primitive basis it becomes (0,1,1)-type indices, and the exact permutation depends on spglib's standardized cell orientation.
NiO is currently excluded from Parts 1–2 (surface NEB + permeation). Its Part 3 bulk diffusivity is unaffected and should keep running.
Plan to solve:
- Add a per-structure 'miller' key to METAL_CONFIGS in pipeline.ipynb (default: the global MILLER), and pass cfg['miller'] in the Part 1/2 generation loops instead of the global.
- In build_slab's oxide branch (models/structure.py, metal_type == 'oxide'), add a termination sanity printout after building: composition of the top 3 atomic planes (reuse the gap-clustering helper _z_plane_clusters from models/surface_graph.py). Mixed-composition planes ⇒ nonpolar (good); alternating pure-cation/pure-anion planes ⇒ polar (warn loudly).
- Determine the correct primitive-basis indices for NiO(100): build trial slabs with the (0,1,1) permutations, run the step-2 printout, and pick the one showing mixed Ni+O planes. Alternatively bypass the ambiguity: for rock-salt oxides let spglib return the conventional cell (spglib.standardize_cell instead of find_primitive) and use plain (1,0,0).
- Set NiO's miller in METAL_CONFIGS, re-enable it in Parts 1–2, and validate: build_surface_graph(metal_type='oxide') on the relaxed slab should show a mixed top plane with both Ni_atop/O_atop ontop sites and NiO_bridge sites (the geometric enumerator, already implemented, handles it from there).
- Acceptance: smoke-level Section A on NiO(100) produces sites, and the top-plane printout confirms nonpolar termination.
The pipeline uses one global
MILLER = (1, 1, 1)(pipeline.ipynb, Part 1 config) for every structure. The oxide branch ofbuild_slab(models/structure.py)applies these indices to the spglib primitive cell, which has different consequences per oxide:Note the index conversion trap: cubic (100) is not (1,0,0) of the primitive cell — in the FCC-primitive basis it becomes (0,1,1)-type indices, and the exact permutation depends on spglib's standardized cell orientation.
NiO is currently excluded from Parts 1–2 (surface NEB + permeation). Its Part 3 bulk diffusivity is unaffected and should keep running.
Plan to solve: