slabterminator 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slabterminator-0.2.0/.claude/settings.local.json +16 -0
- slabterminator-0.2.0/.github/workflows/ci.yml +29 -0
- slabterminator-0.2.0/.gitignore +134 -0
- slabterminator-0.2.0/CITATION.cff +26 -0
- slabterminator-0.2.0/LICENSE.md +21 -0
- slabterminator-0.2.0/PKG-INFO +333 -0
- slabterminator-0.2.0/README.md +313 -0
- slabterminator-0.2.0/pyproject.toml +54 -0
- slabterminator-0.2.0/ruff.toml +40 -0
- slabterminator-0.2.0/scripts/UniqueSlabsGenerator.py +621 -0
- slabterminator-0.2.0/scripts/benchmark.csv +1353 -0
- slabterminator-0.2.0/scripts/benchmark.meta.json +40 -0
- slabterminator-0.2.0/scripts/benchmark.py +1020 -0
- slabterminator-0.2.0/scripts/benchmark_analysis.py +323 -0
- slabterminator-0.2.0/scripts/benchmark_speedup_vs_size.png +0 -0
- slabterminator-0.2.0/scripts/benchmark_summary.md +60 -0
- slabterminator-0.2.0/scripts/build_slab_dataset_slurm.py +158 -0
- slabterminator-0.2.0/scripts/pmg_fragility_index1.log +229 -0
- slabterminator-0.2.0/scripts/pmg_fragility_sweep.py +264 -0
- slabterminator-0.2.0/scripts/pmg_mismatches_summary.md +267 -0
- slabterminator-0.2.0/scripts/pmg_timing_profile.py +378 -0
- slabterminator-0.2.0/scripts/pmg_timing_profile_Fe3C_101.log +60 -0
- slabterminator-0.2.0/src/slabterminator/__init__.py +7 -0
- slabterminator-0.2.0/src/slabterminator/batch.py +224 -0
- slabterminator-0.2.0/src/slabterminator/core.py +1120 -0
- slabterminator-0.2.0/src/slabterminator/pipeline.py +263 -0
- slabterminator-0.2.0/src/slabterminator/utils.py +474 -0
- slabterminator-0.2.0/tests/conftest.py +342 -0
- slabterminator-0.2.0/tests/test-cifs/AgClO4_mp-22993_conventional_standard.cif +38 -0
- slabterminator-0.2.0/tests/test-cifs/AlN_mp-661_conventional_standard.cif +30 -0
- slabterminator-0.2.0/tests/test-cifs/BAsO4_mp-3277_conventional_standard.cif +38 -0
- slabterminator-0.2.0/tests/test-cifs/BBr3_mp-23225_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/BP_mp-1479_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/Bi_mp-23152_conventional_standard.cif +32 -0
- slabterminator-0.2.0/tests/test-cifs/Ca2B5Os3_mp-542455_conventional_standard.cif +46 -0
- slabterminator-0.2.0/tests/test-cifs/CaF2_mp-2741_conventional_standard.cif +38 -0
- slabterminator-0.2.0/tests/test-cifs/CdAs2_mp-471_conventional_standard.cif +38 -0
- slabterminator-0.2.0/tests/test-cifs/Cd_mp-94_conventional_standard.cif +28 -0
- slabterminator-0.2.0/tests/test-cifs/CeCoC2_mp-20753_conventional_standard.cif +42 -0
- slabterminator-0.2.0/tests/test-cifs/Cr12P7_mp-505626_conventional_standard.cif +45 -0
- slabterminator-0.2.0/tests/test-cifs/CrAu4_mp-1070789_conventional_standard.cif +36 -0
- slabterminator-0.2.0/tests/test-cifs/Fe3C_mp-13154_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/FeO_mp-1279742_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/FeS2_mp-226_conventional_standard.cif +38 -0
- slabterminator-0.2.0/tests/test-cifs/FeSiP4_mp-9198_conventional_standard.cif +35 -0
- slabterminator-0.2.0/tests/test-cifs/FeSi_mp-871_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/GeTe_mp-938_conventional_standard.cif +32 -0
- slabterminator-0.2.0/tests/test-cifs/MoC_mp-2305_conventional_standard.cif +28 -0
- slabterminator-0.2.0/tests/test-cifs/NbAs_mp-2059_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/Ni3TeO6_mp-19448_conventional_standard.cif +56 -0
- slabterminator-0.2.0/tests/test-cifs/Ni8Bi8SI_mp-1105564_conventional_standard.cif +44 -0
- slabterminator-0.2.0/tests/test-cifs/PI2_mp-29443_conventional_standard.cif +32 -0
- slabterminator-0.2.0/tests/test-cifs/PI3_mp-27529_conventional_standard.cif +34 -0
- slabterminator-0.2.0/tests/test-cifs/SrSi2_mp-496_conventional_standard.cif +38 -0
- slabterminator-0.2.0/tests/test-cifs/Ta2H_mp-24082_conventional_standard.cif +32 -0
- slabterminator-0.2.0/tests/test-cifs/Te_mp-19_conventional_standard.cif +29 -0
- slabterminator-0.2.0/tests/test-cifs/TiO2_mp-2657_conventional_standard.cif +32 -0
- slabterminator-0.2.0/tests/test-cifs/U_mp-44_conventional_standard.cif +30 -0
- slabterminator-0.2.0/tests/test-cifs/WCl6_mp-571518_conventional_standard.cif +47 -0
- slabterminator-0.2.0/tests/test-cifs/synthetic_polar_Pna21.cif +34 -0
- slabterminator-0.2.0/tests/test_batch.py +120 -0
- slabterminator-0.2.0/tests/test_pipeline.py +193 -0
- slabterminator-0.2.0/tests/test_slab_terminator.py +1544 -0
- slabterminator-0.2.0/tests/test_utils.py +465 -0
- slabterminator-0.2.0/uv.lock +1452 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(uv run python -c ' *)",
|
|
5
|
+
"Bash(uv run *)",
|
|
6
|
+
"Bash(python3 -)",
|
|
7
|
+
"Bash(python -c \"import ast; ast.parse\\(open\\('tests/test_slab_terminator.py'\\).read\\(\\)\\); ast.parse\\(open\\('scripts/UniqueSlabsGenerator.py'\\).read\\(\\)\\); ast.parse\\(open\\('src/slabterminator/core.py'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
|
8
|
+
"Bash(python3 -c \"import ast; ast.parse\\(open\\('tests/test_slab_terminator.py'\\).read\\(\\)\\); ast.parse\\(open\\('scripts/UniqueSlabsGenerator.py'\\).read\\(\\)\\); ast.parse\\(open\\('src/slabterminator/core.py'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
|
9
|
+
"Bash(cat > *)",
|
|
10
|
+
"Bash(python3 -c ' *)"
|
|
11
|
+
],
|
|
12
|
+
"additionalDirectories": [
|
|
13
|
+
"/tmp"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v5
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0 # full history + tags so hatch-vcs can derive the version
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v7
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Install Python and dependencies
|
|
23
|
+
run: uv sync
|
|
24
|
+
|
|
25
|
+
- name: Lint (ruff)
|
|
26
|
+
run: uv run ruff check src tests scripts
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: uv run pytest
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#Misc
|
|
2
|
+
/*.cif
|
|
3
|
+
.idea
|
|
4
|
+
_archive
|
|
5
|
+
.DS_Store
|
|
6
|
+
|
|
7
|
+
# Byte-compiled / optimized / DLL files
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
|
|
12
|
+
# C extensions
|
|
13
|
+
*.so
|
|
14
|
+
|
|
15
|
+
# Distribution / packaging
|
|
16
|
+
.Python
|
|
17
|
+
build/
|
|
18
|
+
develop-eggs/
|
|
19
|
+
dist/
|
|
20
|
+
downloads/
|
|
21
|
+
eggs/
|
|
22
|
+
.eggs/
|
|
23
|
+
lib/
|
|
24
|
+
lib64/
|
|
25
|
+
parts/
|
|
26
|
+
sdist/
|
|
27
|
+
var/
|
|
28
|
+
wheels/
|
|
29
|
+
pip-wheel-metadata/
|
|
30
|
+
share/python-wheels/
|
|
31
|
+
*.egg-info/
|
|
32
|
+
.installed.cfg
|
|
33
|
+
*.egg
|
|
34
|
+
MANIFEST
|
|
35
|
+
|
|
36
|
+
# PyInstaller
|
|
37
|
+
# Usually these files are written by a python script from a template
|
|
38
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
39
|
+
*.manifest
|
|
40
|
+
*.spec
|
|
41
|
+
|
|
42
|
+
# Installer logs
|
|
43
|
+
pip-log.txt
|
|
44
|
+
pip-delete-this-directory.txt
|
|
45
|
+
|
|
46
|
+
# Unit test / coverage reports
|
|
47
|
+
htmlcov/
|
|
48
|
+
.tox/
|
|
49
|
+
.nox/
|
|
50
|
+
.coverage
|
|
51
|
+
.coverage.*
|
|
52
|
+
.cache
|
|
53
|
+
nosetests.xml
|
|
54
|
+
coverage.xml
|
|
55
|
+
*.cover
|
|
56
|
+
*.py,cover
|
|
57
|
+
.hypothesis/
|
|
58
|
+
.pytest_cache/
|
|
59
|
+
|
|
60
|
+
# Translations
|
|
61
|
+
*.mo
|
|
62
|
+
*.pot
|
|
63
|
+
|
|
64
|
+
# Django stuff:
|
|
65
|
+
local_settings.py
|
|
66
|
+
db.sqlite3
|
|
67
|
+
db.sqlite3-journal
|
|
68
|
+
|
|
69
|
+
# Flask stuff:
|
|
70
|
+
instance/
|
|
71
|
+
.webassets-cache
|
|
72
|
+
|
|
73
|
+
# Scrapy stuff:
|
|
74
|
+
.scrapy
|
|
75
|
+
|
|
76
|
+
# Sphinx documentation
|
|
77
|
+
docs/_build/
|
|
78
|
+
|
|
79
|
+
# PyBuilder
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
.python-version
|
|
91
|
+
|
|
92
|
+
# pipenv
|
|
93
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
+
# install all needed dependencies.
|
|
97
|
+
#Pipfile.lock
|
|
98
|
+
|
|
99
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
100
|
+
__pypackages__/
|
|
101
|
+
|
|
102
|
+
# Celery stuff
|
|
103
|
+
celerybeat-schedule
|
|
104
|
+
celerybeat.pid
|
|
105
|
+
|
|
106
|
+
# SageMath parsed files
|
|
107
|
+
*.sage.py
|
|
108
|
+
|
|
109
|
+
# Environments
|
|
110
|
+
.env
|
|
111
|
+
.venv
|
|
112
|
+
env/
|
|
113
|
+
venv/
|
|
114
|
+
ENV/
|
|
115
|
+
env.bak/
|
|
116
|
+
venv.bak/
|
|
117
|
+
|
|
118
|
+
# Spyder project settings
|
|
119
|
+
.spyderproject
|
|
120
|
+
.spyproject
|
|
121
|
+
|
|
122
|
+
# Rope project settings
|
|
123
|
+
.ropeproject
|
|
124
|
+
|
|
125
|
+
# mkdocs documentation
|
|
126
|
+
/site
|
|
127
|
+
|
|
128
|
+
# mypy
|
|
129
|
+
.mypy_cache/
|
|
130
|
+
.dmypy.json
|
|
131
|
+
dmypy.json
|
|
132
|
+
|
|
133
|
+
# Pyre type checker
|
|
134
|
+
.pyre/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
title: SlabTerminator
|
|
4
|
+
abstract: >-
|
|
5
|
+
Enumerate the symmetrically unique slab terminations of a bulk crystal for a
|
|
6
|
+
given Miller index from crystal symmetry, and generate slabs.
|
|
7
|
+
type: software
|
|
8
|
+
authors:
|
|
9
|
+
- family-names: Schindler
|
|
10
|
+
given-names: Peter
|
|
11
|
+
email: p.schindler@northeastern.edu
|
|
12
|
+
affiliation: Northeastern University
|
|
13
|
+
repository-code: "https://github.com/d2r2group/slabterminator"
|
|
14
|
+
url: "https://github.com/d2r2group/slabterminator"
|
|
15
|
+
license: MIT
|
|
16
|
+
version: 0.1.0
|
|
17
|
+
date-released: "2026-07-24"
|
|
18
|
+
keywords:
|
|
19
|
+
- materials science
|
|
20
|
+
- surface science
|
|
21
|
+
- crystallography
|
|
22
|
+
- slab
|
|
23
|
+
- surface termination
|
|
24
|
+
- Miller index
|
|
25
|
+
- symmetry
|
|
26
|
+
- pymatgen
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Schindler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slabterminator
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Enumerate the symmetrically unique slab terminations of a bulk crystal for a given Miller index from crystal symmetry, and generate slabs.
|
|
5
|
+
Project-URL: Homepage, https://github.com/d2r2group/slabterminator
|
|
6
|
+
Project-URL: Repository, https://github.com/d2r2group/slabterminator
|
|
7
|
+
Author-email: Peter Schindler <p.schindler@northeastern.edu>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Keywords: Miller index,crystallography,materials science,pymatgen,slab,surface science,surface termination,symmetry
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: pymatgen>=2026.5.4
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# SlabTerminator
|
|
22
|
+
|
|
23
|
+
Enumerate the **symmetrically unique slab terminations** of a bulk crystal for a
|
|
24
|
+
given Miller index from crystal symmetry.
|
|
25
|
+
|
|
26
|
+
Given a bulk `pymatgen` `Structure` and a Miller index, `SlabTerminator` finds every
|
|
27
|
+
distinct way the crystal can be cleaved along that plane, tells you which
|
|
28
|
+
terminations are polar vs. nonpolar, and (optionally) builds the ready-to-use slab
|
|
29
|
+
structures with vacuum.
|
|
30
|
+
|
|
31
|
+
### Why not just use pymatgen?
|
|
32
|
+
|
|
33
|
+
pymatgen's `SlabGenerator.get_slabs()` enumerates candidate cleaves by clustering
|
|
34
|
+
atoms along the normal within a tolerance, builds a full slab for each, and then
|
|
35
|
+
deduplicates those slabs by comparing them pairwise with `StructureMatcher` (a
|
|
36
|
+
tolerance-based lattice-reduction + site-matching comparison). `SlabTerminator` instead
|
|
37
|
+
works purely from group theory: it projects the oriented cell's space-group operations
|
|
38
|
+
onto the surface normal and groups candidate cleaves into symmetry orbits *before* any
|
|
39
|
+
vacuum is added (see [How it works](#how-it-works)), so it never builds a slab just to
|
|
40
|
+
decide uniqueness and never runs a pairwise structure comparison. Two practical
|
|
41
|
+
consequences:
|
|
42
|
+
|
|
43
|
+
- **It is much faster.** On our benchmark of 1352 (structure, Miller) cases,
|
|
44
|
+
`SlabTerminator` is roughly **26 to 82× faster** than `SlabGenerator` when building
|
|
45
|
+
slabs (**69× on average**) and **~350× faster on average** when only counting terminations,
|
|
46
|
+
because it skips both the per-cleave slab construction and the pairwise `StructureMatcher`
|
|
47
|
+
deduplication.
|
|
48
|
+
|
|
49
|
+
<div align="center"><img src="scripts/benchmark_speedup_vs_size.png" width="70%" alt="Speedup of SlabTerminator over pymatgen SlabGenerator as a function of problem size and Miller index"></div>
|
|
50
|
+
|
|
51
|
+
*Per-case speedup (pymatgen `SlabGenerator` ÷ `SlabTerminator`) vs. problem size and
|
|
52
|
+
Miller index, over 1352 cases up to max index 3. Both run at a single fixed layer
|
|
53
|
+
tolerance (`tol` = pymatgen `ftol` = 0.1, no auto scan); matched geometry (≥ 8 Å slab,
|
|
54
|
+
10 Å vacuum, `max_normal_search=1`, centered), best of 3 runs, on an Apple M5 (heat can
|
|
55
|
+
throttle absolute timings, but methods are timed interleaved so the relative speedup is
|
|
56
|
+
barely affected) / pymatgen 2026.5.4. "Cleavage planes" = candidate interlayer
|
|
57
|
+
cleaves before symmetry reduction. Per-case data in `scripts/benchmark.csv`.*
|
|
58
|
+
|
|
59
|
+
- **That speed makes it more robust, by making tolerance sweeps cheap.** Both methods
|
|
60
|
+
share a layer-grouping tolerance (`SlabTerminator`'s `tol`, pymatgen's `ftol`) that
|
|
61
|
+
changes the count: too tight over-splits near-coplanar atoms, too loose merges
|
|
62
|
+
distinct terminations. Any single tolerance is a guess. Because `SlabTerminator`'s
|
|
63
|
+
analysis reuses one cached oriented cell and its symmetry operations — no slab
|
|
64
|
+
rebuilds, no extra spglib calls — sweeping a whole grid of tolerances is nearly free,
|
|
65
|
+
so it can report the count as a function of tolerance and pick a stable plateau
|
|
66
|
+
automatically (`scan_termination_stability()` / `tol="auto"`; see [Choosing the
|
|
67
|
+
tolerance automatically](#choosing-the-tolerance-automatically)). Running the same
|
|
68
|
+
sweep through `get_slabs` means rebuilding every slab and re-running the pairwise
|
|
69
|
+
comparison at each tolerance, expensive enough that in practice one picks a single
|
|
70
|
+
`ftol` and trusts it.
|
|
71
|
+
|
|
72
|
+
## How it works
|
|
73
|
+
|
|
74
|
+
The oriented unit cell is periodic along the surface normal. `SlabTerminator` runs
|
|
75
|
+
two complementary symmetry analyses, one on each side of adding vacuum:
|
|
76
|
+
|
|
77
|
+
- **Without vacuum → which cleaves are the same slab.** The cell's space-group
|
|
78
|
+
operations are projected onto the 1D coordinate along the surface normal as
|
|
79
|
+
`g → ±g + τ`. Candidate interlayer gaps are grouped into orbits under these maps;
|
|
80
|
+
each orbit is one unique termination. Screw axes, glide planes, and pure
|
|
81
|
+
c-translations (which only exist while the cell is periodic along the normal) are
|
|
82
|
+
what relate cleaves recurring at different heights, so this must be done *before*
|
|
83
|
+
vacuum is added.
|
|
84
|
+
- **With vacuum → is a slab polar.** Each built slab-with-vacuum is checked for a
|
|
85
|
+
surviving operation that maps the normal to its negative. If one exists the two
|
|
86
|
+
faces are equivalent (**nonpolar**); otherwise the slab is **polar**. This must be
|
|
87
|
+
done *with* vacuum, since the periodic cell can otherwise report a false symmetry
|
|
88
|
+
through a glide/screw whose translation the vacuum breaks.
|
|
89
|
+
|
|
90
|
+
This is both faster and more robust than fingerprint-based enumeration. See
|
|
91
|
+
[`scripts/benchmark.py`](scripts/benchmark.py) (which stores every per-case count and
|
|
92
|
+
timing in `scripts/benchmark.csv` — its only generated artifact, alongside the meta
|
|
93
|
+
sidecar) for a correctness + speed comparison against `UniqueSlabsGenerator`, pymatgen's
|
|
94
|
+
`SlabGenerator`, and `StructureMatcher`. By default it recomputes every timing; the
|
|
95
|
+
`--reuse` flag pulls whole method **groups** — `new` (SlabTerminator), `old` (old
|
|
96
|
+
fingerprint), `pmg` (pymatgen) — from the existing CSV instead of re-measuring them, so
|
|
97
|
+
e.g. `--reuse pmg` re-times the fast ST methods fresh while keeping the slow, cached
|
|
98
|
+
pymatgen numbers (each group keeps its own "measured at" timestamp in the meta sidecar),
|
|
99
|
+
and `--reuse all` is report-only. [`scripts/benchmark_analysis.py`](scripts/benchmark_analysis.py)
|
|
100
|
+
reads that CSV to plot the speedup-vs-size analysis (`scripts/benchmark_speedup_vs_size.png`)
|
|
101
|
+
and write `scripts/benchmark_summary.md`.
|
|
102
|
+
|
|
103
|
+
## Installation
|
|
104
|
+
|
|
105
|
+
Requires Python ≥ 3.12. The project uses [`uv`](https://docs.astral.sh/uv/):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv sync
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The only runtime dependency is `pymatgen`.
|
|
112
|
+
|
|
113
|
+
## Quick start
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pymatgen.core import Structure
|
|
117
|
+
from slabterminator.core import SlabTerminator
|
|
118
|
+
|
|
119
|
+
structure = Structure.from_file("tests/test-cifs/Fe3C_mp-13154_conventional_standard.cif")
|
|
120
|
+
|
|
121
|
+
# Analyze the (1, 0, 1) surface.
|
|
122
|
+
gen = SlabTerminator(structure, (1, 0, 1))
|
|
123
|
+
|
|
124
|
+
# Cheap: just enumerate the unique terminations (no slabs built).
|
|
125
|
+
for term in gen.get_unique_terminations():
|
|
126
|
+
print(term)
|
|
127
|
+
# Termination(gap_index=..., gap_position=0.125, multiplicity=..., symmetric_by_bulk=False)
|
|
128
|
+
# ... 4 terminations for Fe3C(101)
|
|
129
|
+
|
|
130
|
+
# Full: build one slab per unique termination, with vacuum.
|
|
131
|
+
result = gen.get_unique_slabs(
|
|
132
|
+
vacuum_size=15.0, # Angstrom of vacuum along c
|
|
133
|
+
min_slab_thickness=10.0, # grow the slab until it exceeds this thickness (Angstrom)
|
|
134
|
+
max_normal_search=1, # search for a more orthogonal output cell
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
print(result.properties.n_unique_terminations) # 4
|
|
138
|
+
print(round(result.properties.surface_area, 2)) # 27.56
|
|
139
|
+
|
|
140
|
+
for i, entry in enumerate(result.slabs):
|
|
141
|
+
print(i, round(entry.shift, 4),
|
|
142
|
+
"polar" if not entry.is_symmetric_with_vacuum else "nonpolar",
|
|
143
|
+
entry.top_layer_composition, "/", entry.bottom_layer_composition)
|
|
144
|
+
entry.slab.to(filename=f"Fe3C_101_{i}.cif") # entry.slab is a pymatgen Structure
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Output:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
0 0.125 polar Fe / Fe
|
|
151
|
+
1 0.1844 polar Fe / C
|
|
152
|
+
2 0.2292 polar C / Fe
|
|
153
|
+
3 0.4553 polar Fe / Fe
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## API
|
|
157
|
+
|
|
158
|
+
### `SlabTerminator(structure, miller_index, tol=0.1, symprec=0.1, sym_tol=1e-3, slab_symprec=None, tol_scan=None)`
|
|
159
|
+
|
|
160
|
+
Constructs the analyzer for one `(structure, miller_index)` pair. The symmetry
|
|
161
|
+
analysis runs here, on the cheapest oriented cell, and is independent of how output
|
|
162
|
+
slabs are later built. Raises `ValueError` for the `(0, 0, 0)` index.
|
|
163
|
+
|
|
164
|
+
`tol` is the layer c-tolerance (Angstrom) used to group atoms into atomic layers.
|
|
165
|
+
Pass `tol="auto"` to have it chosen automatically from a tolerance-stability scan
|
|
166
|
+
(see [Choosing the tolerance automatically](#choosing-the-tolerance-automatically)
|
|
167
|
+
below); `tol_scan` overrides the tolerances swept in that case.
|
|
168
|
+
|
|
169
|
+
- **`get_unique_terminations()`** → `list[Termination]`, one per unique termination,
|
|
170
|
+
each with `gap_index`, `gap_position` (fractional c of the cleave), `multiplicity`
|
|
171
|
+
(number of candidate cleaves that collapsed into it), and `symmetric_by_bulk`
|
|
172
|
+
(cheap pre-vacuum face-symmetry estimate). No slab structures are built; this is the
|
|
173
|
+
fast path.
|
|
174
|
+
|
|
175
|
+
- **`get_unique_slabs(...)`** → `UniqueSlabsResult`, building one slab per unique
|
|
176
|
+
termination. Key options:
|
|
177
|
+
- `vacuum_size` (default `10.0`): vacuum thickness in Angstrom.
|
|
178
|
+
- `slab_thickness_cells` / `min_slab_thickness`: stack the oriented cell to a fixed
|
|
179
|
+
number of repeats, or grow it until it exceeds a target thickness in Angstrom.
|
|
180
|
+
- `all_unique_terminations_to_top` (default `False`): also emit the mirrored
|
|
181
|
+
counterpart of each *polar* slab (the other face brought to the top).
|
|
182
|
+
- `center_slab` (default `True`): center the slab along c, else leave vacuum on top.
|
|
183
|
+
- `max_normal_search` (default `None`): search for a more orthogonal (but thicker)
|
|
184
|
+
output cell. Affects only slab geometry, not which terminations are found.
|
|
185
|
+
- `force_orthogonal_cell` (default `False`): force c orthogonal to the surface plane
|
|
186
|
+
as a final step (see docstring for caveats).
|
|
187
|
+
|
|
188
|
+
The returned `UniqueSlabsResult` is a `NamedTuple`:
|
|
189
|
+
- `properties`: `n_unique_terminations`, `surface_area`, and
|
|
190
|
+
`is_surface_symmetric_without_vacuum`.
|
|
191
|
+
- `settings`: the resolved settings actually used (handy for reproducibility),
|
|
192
|
+
including `tol`, the layer c-tolerance actually applied (the plateau value when
|
|
193
|
+
`tol="auto"`), so a run can be reproduced exactly by passing that float back.
|
|
194
|
+
- `oriented_unit_cell`: the cell the slabs were built in.
|
|
195
|
+
- `slabs`: a list of `SlabEntry`, each with `shift`, `is_symmetric_with_vacuum`,
|
|
196
|
+
`top_layer_composition`, `bottom_layer_composition`, `slab` (a pymatgen
|
|
197
|
+
`Structure`), and `face` (`'as_cut'` or `'mirrored'`).
|
|
198
|
+
|
|
199
|
+
### `regenerate_slabs(shifts, vacuum_size, oriented_unit_cell, ...)`
|
|
200
|
+
|
|
201
|
+
Rebuilds the final slab `Structure`s directly from stored `UniqueSlabsResult` fields,
|
|
202
|
+
skipping the whole symmetry analysis; useful for persisting a compact result
|
|
203
|
+
(shifts + oriented cell + settings) and reconstructing the slabs later.
|
|
204
|
+
|
|
205
|
+
### Choosing the tolerance automatically
|
|
206
|
+
|
|
207
|
+
The number of unique terminations can depend on the layer c-tolerance `tol`: too
|
|
208
|
+
tight over-splits near-coplanar atoms into separate terminations, too loose merges
|
|
209
|
+
genuinely distinct ones. This is the same shift-enumeration tolerance pymatgen's
|
|
210
|
+
`SlabGenerator` exposes as `ftol`.
|
|
211
|
+
|
|
212
|
+
`scan_termination_stability()` sweeps a grid of tolerances and reports how the count
|
|
213
|
+
varies, reusing the cached oriented cell and symmetry operations (no rebuild, no
|
|
214
|
+
extra spglib calls, so the whole scan is nearly free):
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
scan = SlabTerminator(structure, (3, 2, 3)).scan_termination_stability()
|
|
218
|
+
print(scan.curve) # [(0.01, 8), (0.015, 8), ..., (0.1, 3), ...]
|
|
219
|
+
print(scan.chosen_tol) # 0.0612 -- the selected plateau tolerance
|
|
220
|
+
print(scan.chosen_count) # 5
|
|
221
|
+
print(scan.is_ambiguous) # False
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Passing `tol="auto"` to the constructor runs this scan and adopts the selected
|
|
225
|
+
tolerance, all on the single oriented cell (no second build):
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
gen = SlabTerminator(structure, (3, 2, 3), tol="auto")
|
|
229
|
+
print(gen.tol) # 0.0612 (also gen.tol_scan_result -> TolScanResult)
|
|
230
|
+
print(gen.get_unique_slabs().settings.tol) # 0.0612 (recorded for reproducibility)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Selection **anchors on the conventional default `0.1`** and only overrides it with
|
|
234
|
+
cause. The count-vs-tol curve is usually a monotone step-down whose two ends are
|
|
235
|
+
traps (the fine end over-splits, the loose end collapses toward a single
|
|
236
|
+
termination), so the rule is:
|
|
237
|
+
|
|
238
|
+
- if `0.1`'s count is stable (shared with a neighboring tol), **keep `0.1`** (auto
|
|
239
|
+
is a no-op for the common case);
|
|
240
|
+
- if `0.1` sits on a lone one-tol ledge, pick the widest **interior** plateau (a run
|
|
241
|
+
touching neither scan end, excluding both saturations); its tolerance is the
|
|
242
|
+
geometric mean of the plateau's endpoints;
|
|
243
|
+
- if `0.1` is a ledge with no interior plateau, the count is genuinely
|
|
244
|
+
tolerance-ambiguous: **keep `0.1`** and set `is_ambiguous=True` (with a warning).
|
|
245
|
+
|
|
246
|
+
So `tol="auto"` never returns a degenerate over-merged or over-split count; where the
|
|
247
|
+
answer is truly resolution-dependent it says so rather than guessing.
|
|
248
|
+
|
|
249
|
+
### `slabterminator.utils`
|
|
250
|
+
|
|
251
|
+
Helpers used by the core, including
|
|
252
|
+
`get_symmetrically_distinct_miller_indices(structure, max_index)` to enumerate the
|
|
253
|
+
Miller indices worth analyzing:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from slabterminator.utils import get_symmetrically_distinct_miller_indices
|
|
257
|
+
|
|
258
|
+
for miller in get_symmetrically_distinct_miller_indices(structure, max_index=1):
|
|
259
|
+
result = SlabTerminator(structure, miller).get_unique_slabs()
|
|
260
|
+
print(miller, result.properties.n_unique_terminations)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Batch generation over many materials
|
|
264
|
+
|
|
265
|
+
`SlabTerminator` handles one `(structure, Miller index)` pair. Two higher-level
|
|
266
|
+
modules build on it for high-throughput datasets: one bulk material in, all its
|
|
267
|
+
slabs out — and many materials in parallel.
|
|
268
|
+
|
|
269
|
+
### `slabterminator.pipeline` — one material, all Miller indices
|
|
270
|
+
|
|
271
|
+
`build_slabs_for_material(structure, config)` enumerates the symmetrically distinct
|
|
272
|
+
Miller indices (up to `config.max_miller_index`), runs `SlabTerminator` on each, and
|
|
273
|
+
returns a flat list of records — one per built slab — instead of raising on a bad
|
|
274
|
+
material (it returns `MaterialResult(ok=False, error=...)` so a batch can keep going).
|
|
275
|
+
Parameters are grouped into a single `SlabGenConfig` rather than a long argument list:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from slabterminator.pipeline import build_slabs_for_material, SlabGenConfig
|
|
279
|
+
|
|
280
|
+
config = SlabGenConfig(
|
|
281
|
+
max_miller_index=3,
|
|
282
|
+
tol="auto", # pick each surface's layer tolerance from its plateau
|
|
283
|
+
min_slab_thickness=15.0,
|
|
284
|
+
vacuum_size=15.0,
|
|
285
|
+
center_slab=False,
|
|
286
|
+
)
|
|
287
|
+
result = build_slabs_for_material(structure, config, material_id="mp-13154")
|
|
288
|
+
|
|
289
|
+
print(result.ok, result.n_slabs) # True 90
|
|
290
|
+
for rec in result.records:
|
|
291
|
+
print(rec.miller, rec.term_index, rec.face,
|
|
292
|
+
"polar" if not rec.is_symmetric_with_vacuum else "nonpolar",
|
|
293
|
+
rec.top_layer_composition, "/", rec.bottom_layer_composition)
|
|
294
|
+
# rec.slab is the with-vacuum pymatgen Structure; rec.oriented_unit_cell + rec.shift
|
|
295
|
+
# reconstruct the no-vacuum slab via regenerate_slabs(vacuum_size=0.0).
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Each `SlabRecord` carries the built `slab`, its `shift`, `oriented_unit_cell`, and the
|
|
299
|
+
per-slab and aggregate properties (`is_symmetric_with_vacuum`,
|
|
300
|
+
`is_surface_symmetric_without_vacuum`, `surface_area`, `n_unique_terminations`, the
|
|
301
|
+
resolved `tol` and `max_normal_search`, …). The no-vacuum slab is not stored: the
|
|
302
|
+
oriented cell plus the shift reconstruct it exactly via `regenerate_slabs`.
|
|
303
|
+
|
|
304
|
+
### `slabterminator.batch` — many materials in parallel
|
|
305
|
+
|
|
306
|
+
`run_batch(materials, config, *, on_result, ...)` fans `build_slabs_for_material` out
|
|
307
|
+
across worker processes and streams each finished `MaterialResult` to a sink callback.
|
|
308
|
+
It is scheduler- and output-agnostic: you provide the `(id, structure)` stream and an
|
|
309
|
+
`on_result` writer (CSV, database, in-memory list, …). A material whose worker overruns
|
|
310
|
+
`max_material_seconds` (or crashes) is killed and recorded as a failure rather than
|
|
311
|
+
stalling the run — this is why it uses raw processes rather than a pool, whose futures
|
|
312
|
+
cannot interrupt a running task.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from slabterminator.batch import run_batch
|
|
316
|
+
|
|
317
|
+
records = []
|
|
318
|
+
def sink(result):
|
|
319
|
+
if result.ok:
|
|
320
|
+
records.extend(result.records)
|
|
321
|
+
|
|
322
|
+
materials = [("mp-13154", struct_a), ("mp-2657", struct_b)] # structures or as_dict() forms
|
|
323
|
+
n = run_batch(materials, config, n_workers=4,
|
|
324
|
+
max_material_seconds=3 * 3600, on_result=sink)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Testing
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
uv run pytest
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Fixtures for a range of space groups live in [`tests/test-cifs/`](tests/test-cifs/).
|