SlabTerminator
Enumerate the symmetrically unique slab terminations of a bulk crystal for a given Miller index from crystal symmetry.
Given a bulk pymatgen Structure and a Miller index, SlabTerminator finds every
distinct way the crystal can be cleaved along that plane, tells you which
terminations are polar vs. nonpolar, and (optionally) builds the ready-to-use slab
structures with vacuum.
Why not just use pymatgen?
pymatgen's SlabGenerator.get_slabs() enumerates candidate cleaves by clustering
atoms along the normal within a tolerance, builds a full slab for each, and then
deduplicates those slabs by comparing them pairwise with StructureMatcher (a
tolerance-based lattice-reduction + site-matching comparison). SlabTerminator instead
works purely from group theory: it projects the oriented cell's space-group operations
onto the surface normal and groups candidate cleaves into symmetry orbits before any
vacuum is added (see How it works), so it never builds a slab just to
decide uniqueness and never runs a pairwise structure comparison. Two practical
consequences:
-
It is much faster. For plain slab generation (
symmetrize=False; left plot), on our benchmark of 30,302 (material, Miller-index) cases across 1000 materials,SlabTerminatoris, per case, roughly 15× (index 1) to 92× (index 3) faster thanSlabGeneratorwhen building slabs, a geometric-mean ~62× across the full dataset (median ~75×; the tail-weighted whole-workload aggregate is ~97×). And ~108× faster when only counting terminations, because it skips both the per-cleave slab construction and the pairwiseStructureMatcherdeduplication. All per-case figures here are geometric means of the per-case ratios. Inbatchmode there is a further speedup on top, as the bulk symmetry is computed once per material and reused across all its Miller indices: 1.67× aggregate wall-clock over 17 materials (a whole-workload figure, not a per-case geometric mean — per material the median is 1.48×, up to 2.83×).When the slabs are being symmetrized (
symmetrize=True), the per-case generation speedup over pymatgen is even more pronounced: geometric-mean roughly 19× (index 1) to 197× (index 3) per case (~113× across the full dataset; the tail-weighted aggregate is ~274×). This per-case figure is higher than thesymmetrize=Falsespeedup because pymatgen's atom-removal symmetrizer has pathological worst cases (its slowest polar level-3 orientations run into seconds apiece), whereasSlabTerminatortrims straight to a verified crystal flip center in closed form. pymatgen additionally trims each polar slab atom-by-atom and re-runs its symmetry analysis (detailed speedup breakdown, see right plot).Both benchmarks are run with
scripts/benchmark/benchmark.pyand the full list of mpids used for benchmarking is stored inbenchmark_HPC_<date>.meta.md.Per-case speedup (pymatgen
SlabGenerator÷SlabTerminator) vs. problem size and Miller index, up to max index 3. Left: plain generation (symmetrize=False), right: symmetrized generation (symmetrize=True); all 30,302 cases completed for both tools (300 s timeout, none excluded). The top scatter shows every case; the two bar panels show the geometric-mean speedup per bin with multiplicative geometric-standard-deviation whiskers. The two columns share matched y-axes per panel, so the highersymmetrize=Truespeedups are directly readable against the left. Both compare againstSlabTerminatorat a single fixed layer tolerance (tol= pymatgenftol= 0.1, no auto scan); matched geometry (≥ 8 Å slab, 10 Å vacuum,max_normal_search=1, centered), best of 3 runs (best of 2 for pymatgen), on an Intel Xeon Gold 6448Y HPC node (all methods timed interleaved per case, so the relative speedup is unaffected by any drift) / pymatgen 2026.5.4. "Cleavage planes" = candidate interlayer cleaves before symmetry reduction. Per-case data in date-stampedresults/benchmark_<date>.csvandresults/benchmark_symmetrize_<date>.csv.This holds up end-to-end, too. The same 1000-material benchmark set, generated to a Miller index of 3 in full
batchmode (10 workers on Northeastern University's Explorer HPC Cluster, build + serialization + JSONL I/O), took 67.5 s withSlabTerminatorversus 10121.4 s with pymatgen forsymmetrize=False(72k slabs each), a ~150× whole-batch speedup on real production hardware at a matched fixed layer tolerance (tol=ftol= 0.1). Withsymmetrize=Truethe same build took 102.8 s versus 11446.1 s (~111×), even thoughSlabTerminatoremits (and serializes to disk) ~1.66× more slabs than pymatgen there (143k vs. 86k), because it keeps the polar parents and unsymmetrizable Tasker-III terminations pymatgen drops (SlabTerminatorcan reproduce pymatgen's behavior of dropping every polar slab by passingsymmetrize="drop_all_polar"instead ofsymmetrize=True, keeping only the slabs it can make symmetric). The two SLURM entry points used arescripts/hpc/build_slab_dataset_slurm.pyandscripts/hpc/build_slab_dataset_slurm_pymatgen.py, matched on everything but the generator (run-wide details inresults/benchmark_HPC_<date>.meta.md). -
That speed makes it more robust, by making tolerance sweeps cheap. Both methods share a layer-grouping tolerance (
SlabTerminator'stol, pymatgen'sftol) that changes the count: too tight over-splits near-coplanar atoms, too loose merges distinct terminations. Any single tolerance is a guess. BecauseSlabTerminator's analysis reuses one cached oriented cell and its symmetry operations (no slab rebuilds, no extra spglib calls), sweeping a whole grid of tolerances is nearly free, so it can report the count as a function of tolerance and pick a stable plateau automatically (scan_termination_stability()/tol="auto"; the selection rules are inslabterminator._tol_scan.TolScanner.select_plateau). Running the same sweep throughget_slabsmeans rebuilding every slab and re-running the pairwise comparison at each tolerance, expensive enough that in practice one picks a singleftoland trusts it. -
Its polarity verdict is cell-orientation-invariant. To decide whether a slab is nonpolar,
SlabTerminatorapplies the bulk flipping operations directly to the built slab's atoms and tests self-coincidence in the surface{a, b}lattice, so the answer depends only on the surface, not on the choice ofmax_normal_search. Running the fullSpacegroupAnalyzeron the slab (pymatgen's route) instead misses a genuine flip when that choice leaves the cell's c vector oblique, making the verdictmax_normal_search- dependent: at the library defaults,4mm(1,1,0),3m(1,1,0)and-6m2(1,1,0)read polar at the cheap cell but nonpolar at anymax_normal_search.SlabTerminatorreports the same verdict in every one of those cells. Because that verdict is also thickness-invariant,SlabTerminatorchecks it on the minimal single-cell oriented cell rather than the full slab, so for a 15 Å-thick slab it is ~5× faster per termination than the full-slabSpacegroupAnalyzercall pymatgen must run. -
A full surface-polarity classification is built in. Because the orbits already come from the crystal's projected symmetry operations, that same machinery classifies each orientation's polarity at no extra cost: whether a flipping operation exists at all (
has_flip_op_by_bulk, equivalently whether any nonpolar slab is achievable at all -- its negation is the truly-polar Tasker III verdict), whether a point flip survives adding vacuum (has_symmorphic_flip_op_by_bulk), and the full crystallographic surface-polarity class of Hinuma et al. (hinuma_polarity_type:polar/nonpolar_A/nonpolar_B/nonpolar_C, mapping onto the three Tasker ionic-surface types). pymatgen returns only a per-slabis_symmetric()bit and no orientation-level polarity classification (the four classes are documented onslabterminator.datatypes.SlabProperties).
How it works
The oriented unit cell is periodic along the surface normal. SlabTerminator runs
two complementary symmetry analyses, one on each side of adding vacuum:
- Without vacuum → which cleaves are the same slab. The cell's space-group
operations are projected onto the 1D coordinate along the surface normal as
g → ±g + τ(gis a cleave's position along the normal,+/−keeps/flips the normal direction, andτis the operation's translation along the normal). Candidate interlayer gaps are grouped into orbits under these maps; each orbit is one unique termination. Screw axes, glide planes, and pure c-translations (which only exist while the cell is periodic along the normal) are what relate cleaves recurring at different heights, so this must be done before vacuum is added. - With vacuum → is a slab polar. Each built slab-with-vacuum is checked for a surviving operation that maps the normal to its negative. If one exists the two faces are equivalent (nonpolar); otherwise the slab is polar. This must be done with vacuum, since the periodic cell can otherwise report a false symmetry through a glide/screw whose translation the vacuum breaks.
Terminology: termination vs. face
Throughout SlabTerminator, a termination means one symmetrically distinct
cleave (a distinct way to cut the crystal along the Miller plane), and there is
exactly one termination (one as-cut slab) per orbit of candidate cleavage gaps.
get_unique_terminations(), n_unique_terminations, and termination_id all count and
label cleaves in this sense.
A single cleave produces a slab with two faces (top and bottom). When the crystal has
a flipping operation relating them the slab is nonpolar and both faces are equivalent;
when it does not, the slab is polar and its two faces are inequivalent surface
terminations in the surface-science sense. So one termination (one cleave) can expose two
distinct faces, which is exactly why a polar slab yields two symmetrized children (one
per face) and why include_flipped=True emits the opposite-face counterpart of each polar
slab. To keep the vocabulary unambiguous, "termination" in this codebase always denotes the
cleave, never a single face; the face notion appears only through include_flipped and the
flipped field.
Benchmarking script details
This method is both faster and more robust than the fingerprint-based enumeration of an
older, unpublished version of this code (2022).
scripts/benchmark/benchmark.py is the correctness +
speed comparison against that UniqueSlabsGenerator and against pymatgen's
SlabGenerator (StructureMatcher-based): one interleaved run co-measures both datasets,
so their timings share a single thermal state, and stores every per-case count and timing
in the date-stamped results/benchmark_<date>.csv (plain generation) and
results/benchmark_symmetrize_<date>.csv (symmetrized), one row per case keyed by its
mpid, each CSV beside a .meta.json sidecar recording the input dataset, the machine and
the slabterminator / pymatgen / python versions it ran against.
scripts/benchmark/benchmark_analysis.py reads
those CSVs, plots the figures above
(results/benchmark_speedup_vs_size_<date>.png, its symmetrize twin and the combined
figure) and writes results/benchmark_summary_<date>.md and
results/benchmark_symmetrize_summary_<date>.md, each stamped with the analyzed run's
date. Each script documents its own flags, in its module docstring and its --help:
benchmark.py --reuse pulls whole method groups (new/old/pmg and
new_sym/auto_sym/pmg_sym) from the existing CSVs instead of re-measuring them, and
benchmark_analysis.py --date analyzes a run other than the most recent on disk.
A note on AI-assisted development
The original (unpublished) version of this software was written in 2021/22 and used a fingerprint approach based on nearest-neighbor analysis to distinguish terminations. With the help of Claude Code, an entirely new approach was developed from group theory and symmetry operations, and I worked extensively back and forth with AI to ensure its fidelity against both pymatgen and the older fingerprint method, as well as to optimize and analyze the resulting speedup.
Installation
Requires Python ≥ 3.12. The package is on PyPI:
# with uv
uv add slabterminator
# with pip
pip install slabterminator
The runtime dependencies are pymatgen and numpy.
To work on the source instead, clone the repo and use uv
to install it with its dev dependencies:
git clone https://github.com/d2r2group/slabterminator
cd slabterminator
uv sync
Quick start
from pymatgen.core import Structure
from slabterminator.core import SlabTerminator
structure = Structure.from_file("tests/test-cifs/Fe3C_mp-13154_conventional_standard.cif")
# Analyze the (1, 0, 1) surface.
gen = SlabTerminator(structure, (1, 0, 1))
# Cheap: just enumerate the unique terminations (no slabs built).
for term in gen.get_unique_terminations():
print(term)
# Termination(gap_index=7, gap_position=0.0894..., multiplicity=2,
# symmetric_by_bulk=False, termination_key='Fe1@0.42|Fe1;Fe1@0.53|Fe1')
# ... 4 terminations for Fe3C(101)
# Full: build one slab per unique termination, with vacuum.
result = gen.get_unique_slabs(
vacuum_size=15.0, # Angstrom of vacuum along c
min_slab_thickness=10.0, # grow the slab until it exceeds this thickness (Angstrom)
max_normal_search=1, # search for a more orthogonal output cell
)
print(result.properties.n_unique_terminations) # 4
print(round(result.properties.surface_area, 2)) # 27.56
for i, entry in enumerate(result.slabs):
print(i, round(entry.shift, 4),
"polar" if not entry.is_symmetric_with_vacuum else "nonpolar",
entry.top_layer_composition, "/", entry.bottom_layer_composition)
entry.slab.to(filename=f"Fe3C_101_{i}.cif") # entry.slab is a pymatgen Structure
Output:
0 0.0447 polar Fe / Fe
1 0.125 polar Fe / Fe
2 0.1844 polar Fe / C
3 0.2292 polar C / Fe
All surfaces of one material
To enumerate the distinct surfaces of a bulk material, use the
SlabTerminator.for_miller_family classmethod. It runs the bulk symmetry analysis
once and hands the resulting operations to each per-facet SlabTerminator, so a
whole family costs one symmetry analysis instead of one per Miller index — the same reuse the
batch pipeline does, in a
lightweight form for library/interactive use:
from slabterminator.core import SlabTerminator
# One representative per distinct family up to |h|,|k|,|l| = 2 (e.g. (100), (110),
# (111), ... for cubic). Each yielded generator is ready to build slabs.
for miller, gen in SlabTerminator.for_miller_family(structure, max_miller_index=2):
result = gen.get_unique_slabs(vacuum_size=15.0, min_slab_thickness=10.0)
print(miller, result.properties.n_unique_terminations)
Construction only analyzes terminations; the vacuum/thickness/symmetrize knobs stay on
each get_unique_slabs(...) call. Other SlabTerminator keywords (tol, slab_symprec,
output_format, ...) pass through, and symprec reaches both the shared bulk analysis and
every generator, so set it here (help(SlabTerminator.for_miller_family) has the checks).
For finished slab records across many materials in parallel, use
slabterminator.pipeline / batch instead.
API
SlabTerminator is the library's entry point: construct it for one (structure, Miller index) pair, then ask it for terminations or for built slabs. This section is that surface
at one line per item; every argument, return field, diagnostics code and symmetry flag is
documented in full by the docstrings that ship with the package, so help() on the names
below is the reference — SlabTerminator and regenerate_slabs from slabterminator.core,
SlabProperties, SlabSettings and SlabEntry from slabterminator.datatypes. The
repository additionally carries
docs/REFERENCE.md,
which sets the same contract out as prose.
SlabTerminator(structure, miller_index, tol=0.1, symprec=0.1, slab_symprec=None, bulk_symmetry_ops=None, output_format="pmg")
The symmetry analysis runs at construction, on the cheapest oriented cell, and is independent of how output slabs are later built. Pass a conventional cell: both the Miller index and the symmetry operations are read in the basis given, so a cell whose lattice is not point-group invariant — an anisotropic supercell — silently loses operations and emits duplicate terminations.
| argument | meaning |
|---|---|
structure |
the bulk pymatgen Structure, ordered (one species per site) and well-formed |
miller_index |
three integers, not all zero |
tol (0.1) |
layer c-tolerance in Å for grouping atoms into layers, or "auto" to pick it from a stability scan (see scan_termination_stability()) |
symprec (0.1) |
crystallographic tolerance on the bulk cell; drives flip-op detection, the Hinuma class and orbit reduction, and fixes the two cleave-matching tolerances gen.orbit_tol and gen.sym_tol, derived from it rather than settable |
slab_symprec (=symprec) |
coincidence tolerance in Å for the with-vacuum face-symmetry check, which is independent of vacuum_size and max_normal_search; must be the equal-or-coarser of the two |
bulk_symmetry_ops |
the bulk's Cartesian space-group operations, reused across one material's Miller indices — one symmetry analysis per material instead of one per index |
output_format ("pmg") |
a pymatgen Structure per slab, or "dict" for an as_dict()-shaped dict built straight from the cleave arrays |
Every argument is rejected outside its domain rather than silently reinterpreted, and the
tolerance bounds are absolute rather than advisory: a negative symprec, or a non-finite
structure, reaches spglib and segfaults the interpreter uncatchably, and tol=nan would
collapse any structure to a single termination. A disordered (partially occupied) or
malformed structure raises ValueError
(help(SlabTerminator) gives the full domain, and why each bound is there).
Methods
get_unique_terminations()→list[Termination], one per unique termination, each withgap_index,gap_position,multiplicity,symmetric_by_bulkandtermination_key. No slab structures are built; this is the fast path.get_unique_slabs(...)→UniqueSlabsResult, building one slab per unique termination. Options:vacuum_size(10.0),slab_thickness_cells/min_slab_thickness(a floor, not a target, enforced on the occupied atomic span),include_flipped(False),center_slab(True),max_normal_search(None),force_orthogonal_cell(False),prebuilt_output_cell, andsymmetrize(False) — which additionally emits up to two nonpolar symmetrized slabs per polar termination, trimmed in closed form straight to a verified crystal flip center rather than one atom at a time, withTrue/"add","drop_parents"and"drop_all_polar"selecting which polar slabs are kept alongside the children (help(SlabTerminator.get_unique_slabs)documents every option in full).scan_termination_stability()→TolScanResult, the count-vs-tolerance curve and the plateautol="auto"selects from it (help()on it, and onslabterminator._tol_scan.TolScanner.select_plateaufor the selection rules).SlabTerminator.for_miller_family(structure, max_miller_index, ...)→ an iterator of(miller_index, generator)pairs, one per distinct Miller index, off one shared bulk analysis (see All surfaces of one material).regenerate_slabs(shifts, vacuum_size, oriented_unit_cell, ...), module-level, rebuilds slabs from stored result fields and skips the analysis entirely (help(regenerate_slabs)).
UniqueSlabsResult is a NamedTuple of properties, settings, oriented_unit_cell and
slabs:
properties—n_unique_terminations,surface_area,has_flip_op_by_bulk,has_symmorphic_flip_op_by_bulk,hinuma_polarity_typeanddiagnostics. The area and the two flags are fixed at construction and noget_unique_slabsargument can move them; the count and the polarity class are reported at the accompanyingsettings.tol.settings— what the run actually used, so it can be reproduced by passing the values back: the resolvedtol, the two symmetry knobssymprec/slab_symprec(constructor arguments, but they move the emitted slabs), and the thickness request three times over — the resolved cell count beside the two arguments as passed, since no two of the three determine the third.slabs— a list ofSlabEntry:shift,is_symmetric_with_vacuum,top_layer_composition,bottom_layer_composition,slab,nsites,thickness_A,flipped,has_symmetrized_children,symmetrized,window_top,off_stoichiometry,diagnosticsandtermination_id, plus two content-based join keys:termination_key(per termination family, stable across runs and platforms at a fixedtol) andslab_key(per individual slab). Both carry caveats worth reading before joining datasets on them (help(SlabEntry)is the field reference, those caveats included).
A build records any provenance concern it hits as a closed set of five string codes,
durably on SlabProperties.diagnostics (orientation-level) and SlabEntry.diagnostics
(per-slab) as well as through a live warnings.warn at the origin — tol_ambiguous,
degenerate_layering, flip_flag_disagreement, thin_image_vacuum and internal_vacuum.
help(SlabProperties) documents the three orientation-level codes, and help(SlabEntry)
which two reach a slab: flip_flag_disagreement
(the cheap without-vacuum flip flag contradicted the authoritative with-vacuum check, which is
the one that decides — the emitted slab is correct) and internal_vacuum (the slab holds a
vacuum-sized empty band between its own two faces, so its thickness_A overstates how
much material it has). The durable
field is what a batch has: a warning raised inside a worker process never reaches
run_batch's log. Every warning carries the SlabTerminatorWarning category, so it can be
filtered or escalated on its own.
slabterminator.utils holds the helpers the core uses, chiefly
get_sym_distinct_miller_indices_and_symops(structure, max_index), which returns the Miller
indices worth analyzing and the bulk operations to thread through them, from one
symmetry analysis of the structure (its docstring is the contract).
Symmetry flags
Each boolean symmetry tag is named for how it is determined: from bulk ops (algebra
on the crystal's projected symmetry operations, no slab built), from a verified
with-vacuum slab, or from bulk ops + stoichiometry (a per-layer composition check).
Orientation-level flags belong to the (crystal, Miller index) pair and are mirrored onto
every SlabRecord; termination-level flags describe one termination (cleave) or its slab.
| Flag | Level | From | Meaning |
|---|---|---|---|
has_flip_op_by_bulk |
orientation | bulk ops | a flipping op exists (normal → −normal) — exactly the "a nonpolar slab is obtainable at all" verdict, so False is truly polar (Tasker III, unreconstructable) |
has_symmorphic_flip_op_by_bulk |
orientation | bulk ops | a point flip op exists — an exact certificate that a nonpolar termination or trimming exists in the crystal |
hinuma_polarity_type |
orientation | bulk ops + stoichiometry | the crystallographic surface-polarity class: polar / nonpolar_A / nonpolar_B / nonpolar_C |
symmetric_by_bulk |
termination | bulk ops | a bulk flip leaves the cut inside the cleave's own empty band, so the flip maps the atom set onto itself |
is_symmetric_with_vacuum |
termination | verified | the built slab's two faces are genuinely equivalent (nonpolar as-cut) |
has_symmetrized_children |
termination | verified | this polar parent produced verified symmetrized children (symmetrize=True only) |
The names are chosen so the implications read directly: the four other boolean flags each
imply has_flip_op_by_bulk, and the negations cascade the other way. One is not a filter for
another, though — symmetric_by_bulk does not predict is_symmetric_with_vacuum in
either direction, so filter a dataset on is_symmetric_with_vacuum, which is the verified
verdict. help(SlabProperties) documents the four polarity classes and their approximate
Tasker correspondence; the full implication set, and the meaningful gaps between the flags,
are in docs/REFERENCE.md.
Batch generation over many materials
SlabTerminator handles one (structure, Miller index) pair. Two higher-level modules
build on it for high-throughput datasets. slabterminator.pipeline takes one bulk material
and returns a MaterialResult holding a flat list of SlabRecords across all its Miller
indices — enumerating the distinct indices itself, sharing one bulk symmetry analysis across
them, and reporting a bad material as MaterialResult(ok=False, error=...) rather than
raising, so a batch can keep going:
from slabterminator.pipeline import build_slabs_for_material, SlabGenConfig
config = SlabGenConfig(
max_miller_index=3,
tol="auto", # pick each surface's layer tolerance from its plateau
min_slab_thickness=15.0,
vacuum_size=15.0,
center_slab=False,
)
result = build_slabs_for_material(structure, config, material_id="mp-13154")
print(result.ok, result.n_slabs) # True 95
for rec in result.records:
print(rec.miller, rec.slab_id, rec.symmetrized or ("flipped" if rec.flipped else "as_cut"),
"polar" if not rec.is_symmetric_with_vacuum else "nonpolar",
rec.top_layer_composition, "/", rec.bottom_layer_composition)
# rec.slab is the with-vacuum pymatgen Structure (an as_dict()-shaped dict under
# SlabGenConfig(output_format="dict")); rec.regenerate() rebuilds the no-vacuum slab,
# and rec.regenerate(vacuum_size=config.vacuum_size) reproduces rec.slab itself.
slabterminator.batch fans that out across worker processes and streams each finished
MaterialResult to a sink callback. It is scheduler- and output-agnostic: you provide the
(id, structure) stream and an on_result writer (CSV, database, in-memory list, …), and a
material whose worker overruns max_material_seconds or crashes is recorded as a failure
rather than stalling the run:
from slabterminator.batch import run_batch
records = []
def sink(result):
if result.ok:
records.extend(result.records)
# The __main__ guard is required, not stylistic: run_batch starts worker processes, and
# under the "spawn" start method (the default on macOS and Windows) each worker re-imports
# this module. Without the guard it would re-execute run_batch on import, recursively.
if __name__ == "__main__":
materials = [("mp-13154", struct_a), ("mp-2657", struct_b)] # structures or as_dict() forms
n = run_batch(materials, config, n_workers=4,
max_material_seconds=3 * 3600, on_result=sink)
The docstrings document the rest: help(SlabGenConfig) for the four fields that
deliberately default differently from the library, help(SlabRecord) for its fields,
help(SlabRecord.from_row) for the to_row() / from_row() round trip and its
precise_float caveat, help(SlabRecord.regenerate) for the overrides, and
help(run_batch) for result_transform's two process placements, the run manifest
config_path= writes, and the failure channels. SlabGenConfig documents its three
pipeline-specific fields (max_miller_index, max_normal_search_limit, angle_tol) on
itself and defers the rest to the SlabTerminator / get_unique_slabs arguments they are
passed to.
Testing
uv run pytest # everything, serially
# what CI runs: the parallel arm, then the one wall-clock-sensitive test
TIMED=tests/test_batch.py::TestRunBatch::test_finished_worker_is_not_reclaimed_as_a_timeout
uv run pytest -n auto --dist worksteal --deselect $TIMED
uv run pytest $TIMED
The two-command form is the faster one. That single test is split out because it asserts the batch
supervisor against a wall-clock budget with only ~0.6 s of margin, which needs a machine that is
not oversubscribed; everything else — the rest of tests/test_batch.py included — is
parallel-safe under pytest-xdist.
CI also measures branch coverage of src/ and gates it with --fail-under, a one-way ratchet
seeded one item below the measured total (at ~2,100 items, one uncovered statement is ~0.05 pp).
Which settings that number depends on is commented in pyproject.toml's [tool.coverage.run];
statements deliberately outside the denominator carry a # pragma: no cover - <reason> at the guard.
The suite is heavily parametrized across the bundled fixtures; pytest --collect-only -q
reports how many cases that is on the tree you have. The 41 bundled CIFs in
tests/test-cifs/ are three groups:
- The sweep set (34 structures). One representative for each of the 32
crystallographic point groups, spanning all 7 crystal systems, so the symmetry handling
is exercised across the full range of surface symmetries: 31 Materials Project
conventional standard cells plus one synthetic polar structure
(
synthetic_polar_Pna21.cif) for the point groupmm2. Two further named fixtures are included as regression cases:Ce2NiGe3(mp-1102475), a glide / rich-symmetry cell whose reversal-canonical lower bound over-counts, andTb5Ir3(mp-1188649), a P-hexagonal metal whose (1,3,3) surface exercises the symmetrized-windowmax_normal_searchinvariance. Every structure in this set runs through the same parametrized sweeps. - Five surface-polarity reference cells with published classifications, which back the
hinuma_polarity_typetests against the paper's own results: MgO rocksalt ((100) type A, (111) type C), SrTiO₃ perovskite ((001) type C, the Hinuma-vs-Tasker exception), and the paper's three worked type-B examples BeSO₄, high-pressure AgI and FeSe₂. Wurtzite AlN ((0001) polar) reuses its point-group cell. These are deliberately kept out of the sweep set so the point-group survey stays exactly one structure per point group. - Two un-parametrized cells loaded directly by individual regression tests:
TiNi(mp-1179013), pulled for a count-mismatch investigation, andPaAs(mp-11106), a symmetrized-childmax_normal_search-invariance case.
The higher-index sweep (every distinct Miller index up to 3, for every sweep fixture) is validated without hand-curated counts: each case is checked against independent, orbit-free bounds — a hard gap-count ceiling, a symmetry-gated lower floor, and shift-free count brackets — backed by an independently re-derived projected-orbit oracle for the screw / sub-period regime those bounds cannot pin.
References
The hinuma_polarity_type surface-polarity classification (polar / nonpolar_A /
nonpolar_B / nonpolar_C) implements the crystallographic scheme of:
Y. Hinuma, Y. Kumagai, F. Oba, I. Tanaka, "Categorization of surface polarity from a crystallographic approach", Computational Materials Science 113 (2016) 221–230. doi:10.1016/j.commatsci.2015.11.042
The A/B/C classes map (barring formal-charge exceptions) onto the three ionic-surface types of:
P. W. Tasker, "The stability of ionic crystal surfaces", Journal of Physics C: Solid State Physics 12 (1979) 4977–4984. doi:10.1088/0022-3719/12/22/036
Metadata
Release files for slabterminator 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| slabterminator-0.7.0.tar.gz | 603.8 kB | Without provenance | Details for slabterminator-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| slabterminator-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for slabterminator-0.7.0-py3-none-any.whl |
Total release size: 724.9 kB
Release files / slabterminator-0.7.0.tar.gz
| Download URL | slabterminator-0.7.0.tar.gz |
|---|---|
| Size | 603.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
470af1890279a6482781e1928f95c6127a296d3208718610d011f7285301ed43
|
|
BLAKE2b-256 checksum How to use checksums |
dfd1197d52f7cd1a763e40ac1a23cc20da79e1d0693bb267fdfbdd0068a8b187
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / slabterminator-0.7.0-py3-none-any.whl
| Download URL | slabterminator-0.7.0-py3-none-any.whl |
|---|---|
| Size | 121.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7f59a2ab43a878e38cf87038ddbe2a508b47d0a33ce3922958934095f8626c9d
|
|
BLAKE2b-256 checksum How to use checksums |
1c9abed0441488a4d4d911dc2625345adfc60b38f6fc2384aac921a324dbda2b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for slabterminator 0.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.