Skip to main content
SlabTerminator Logo

SlabTerminator

Python - Version PyPI - Version License: MIT

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, SlabTerminator is, per case, roughly 15× (index 1) to 92× (index 3) faster than SlabGenerator when 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 pairwise StructureMatcher deduplication. All per-case figures here are geometric means of the per-case ratios. In batch mode 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 the symmetrize=False speedup because pymatgen's atom-removal symmetrizer has pathological worst cases (its slowest polar level-3 orientations run into seconds apiece), whereas SlabTerminator trims 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.py and the full list of mpids used for benchmarking is stored in benchmark_HPC_<date>.meta.md.

    Speedup of SlabTerminator over pymatgen SlabGenerator for symmetrize=False (left) and symmetrize=True (right), as a function of problem size and Miller index, with y-axes matched across the two

    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 higher symmetrize=True speedups are directly readable against the left. Both compare against SlabTerminator at a single fixed layer tolerance (tol = pymatgen ftol = 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-stamped results/benchmark_<date>.csv and results/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 batch mode (10 workers on Northeastern University's Explorer HPC Cluster, build + serialization + JSONL I/O), took 67.5 s with SlabTerminator versus 10121.4 s with pymatgen for symmetrize=False (72k slabs each), a ~150× whole-batch speedup on real production hardware at a matched fixed layer tolerance (tol = ftol = 0.1). With symmetrize=True the same build took 102.8 s versus 11446.1 s (~111×), even though SlabTerminator emits (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 (SlabTerminator can reproduce pymatgen's behavior of dropping every polar slab by passing symmetrize="drop_all_polar" instead of symmetrize=True, keeping only the slabs it can make symmetric). The two SLURM entry points used are scripts/hpc/build_slab_dataset_slurm.py and scripts/hpc/build_slab_dataset_slurm_pymatgen.py, matched on everything but the generator (run-wide details in results/benchmark_HPC_<date>.meta.md).

  • That speed makes it more robust, by making tolerance sweeps cheap. Both methods share a layer-grouping tolerance (SlabTerminator's tol, pymatgen's ftol) that changes the count: too tight over-splits near-coplanar atoms, too loose merges distinct terminations. Any single tolerance is a guess. Because SlabTerminator'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 in slabterminator._tol_scan.TolScanner.select_plateau). Running the same sweep through get_slabs means rebuilding every slab and re-running the pairwise comparison at each tolerance, expensive enough that in practice one picks a single ftol and trusts it.

  • Its polarity verdict is cell-orientation-invariant. To decide whether a slab is nonpolar, SlabTerminator applies 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 of max_normal_search. Running the full SpacegroupAnalyzer on the slab (pymatgen's route) instead misses a genuine flip when that choice leaves the cell's c vector oblique, making the verdict max_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 any max_normal_search. SlabTerminator reports the same verdict in every one of those cells. Because that verdict is also thickness-invariant, SlabTerminator checks 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-slab SpacegroupAnalyzer call 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-slab is_symmetric() bit and no orientation-level polarity classification (the four classes are documented on slabterminator.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 + τ (g is 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 with gap_index, gap_position, multiplicity, symmetric_by_bulk and termination_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, and symmetrize (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, with True/"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 plateau tol="auto" selects from it (help() on it, and on slabterminator._tol_scan.TolScanner.select_plateau for 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_type and diagnostics. The area and the two flags are fixed at construction and no get_unique_slabs argument can move them; the count and the polarity class are reported at the accompanying settings.tol.
  • settings — what the run actually used, so it can be reproduced by passing the values back: the resolved tol, the two symmetry knobs symprec / 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 of SlabEntry: shift, is_symmetric_with_vacuum, top_layer_composition, bottom_layer_composition, slab, nsites, thickness_A, flipped, has_symmetrized_children, symmetrized, window_top, off_stoichiometry, diagnostics and termination_id, plus two content-based join keys: termination_key (per termination family, stable across runs and platforms at a fixed tol) and slab_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 group mm2. Two further named fixtures are included as regression cases: Ce2NiGe3 (mp-1102475), a glide / rich-symmetry cell whose reversal-canonical lower bound over-counts, and Tb5Ir3 (mp-1188649), a P-hexagonal metal whose (1,3,3) surface exercises the symmetrized-window max_normal_search invariance. Every structure in this set runs through the same parametrized sweeps.
  • Five surface-polarity reference cells with published classifications, which back the hinuma_polarity_type tests 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, and PaAs (mp-11106), a symmetrized-child max_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)

Source distribution for slabterminator 0.7.0
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)

Table of built distributions (wheels) for slabterminator 0.7.0
File Interpreter ABI Platform Actions
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page