metalsurfer 0.3.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.
- metalsurfer-0.3.0/PKG-INFO +41 -0
- metalsurfer-0.3.0/README.md +427 -0
- metalsurfer-0.3.0/pyproject.toml +108 -0
- metalsurfer-0.3.0/setup.cfg +4 -0
- metalsurfer-0.3.0/src/metalsurfer/__init__.py +147 -0
- metalsurfer-0.3.0/src/metalsurfer/_logging.py +325 -0
- metalsurfer-0.3.0/src/metalsurfer/_utils.py +13 -0
- metalsurfer-0.3.0/src/metalsurfer/campaigns.py +583 -0
- metalsurfer-0.3.0/src/metalsurfer/config.py +695 -0
- metalsurfer-0.3.0/src/metalsurfer/conformers.py +249 -0
- metalsurfer-0.3.0/src/metalsurfer/exceptions.py +29 -0
- metalsurfer-0.3.0/src/metalsurfer/filters.py +604 -0
- metalsurfer-0.3.0/src/metalsurfer/io_results.py +751 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/__init__.py +11 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/bayesian.py +927 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/dataset.py +217 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/features.py +147 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/predict.py +123 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/regression.py +307 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/reproduce.py +158 -0
- metalsurfer-0.3.0/src/metalsurfer/ml/schema.py +579 -0
- metalsurfer-0.3.0/src/metalsurfer/models.py +773 -0
- metalsurfer-0.3.0/src/metalsurfer/optimization.py +1300 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/__init__.py +64 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/_constants.py +211 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/_material.py +59 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/generators.py +1435 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/geometry.py +856 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/policy.py +206 -0
- metalsurfer-0.3.0/src/metalsurfer/placement/sites.py +1815 -0
- metalsurfer-0.3.0/src/metalsurfer/py.typed +0 -0
- metalsurfer-0.3.0/src/metalsurfer/surface_prep/__init__.py +69 -0
- metalsurfer-0.3.0/src/metalsurfer/surface_prep/prep.py +372 -0
- metalsurfer-0.3.0/src/metalsurfer/surfaces.py +1004 -0
- metalsurfer-0.3.0/src/metalsurfer/symmetry.py +460 -0
- metalsurfer-0.3.0/src/metalsurfer/workflow/__init__.py +15 -0
- metalsurfer-0.3.0/src/metalsurfer/workflow/bayesian.py +601 -0
- metalsurfer-0.3.0/src/metalsurfer/workflow/core.py +498 -0
- metalsurfer-0.3.0/src/metalsurfer/workflow/reference.py +92 -0
- metalsurfer-0.3.0/src/metalsurfer/workflow/saturation.py +913 -0
- metalsurfer-0.3.0/src/metalsurfer/workflow/shared.py +1070 -0
- metalsurfer-0.3.0/src/metalsurfer.egg-info/PKG-INFO +41 -0
- metalsurfer-0.3.0/src/metalsurfer.egg-info/SOURCES.txt +68 -0
- metalsurfer-0.3.0/src/metalsurfer.egg-info/dependency_links.txt +1 -0
- metalsurfer-0.3.0/src/metalsurfer.egg-info/requires.txt +29 -0
- metalsurfer-0.3.0/src/metalsurfer.egg-info/top_level.txt +1 -0
- metalsurfer-0.3.0/tests/test_bayesian.py +606 -0
- metalsurfer-0.3.0/tests/test_campaigns.py +589 -0
- metalsurfer-0.3.0/tests/test_config_validation.py +632 -0
- metalsurfer-0.3.0/tests/test_conformers.py +281 -0
- metalsurfer-0.3.0/tests/test_constraints.py +173 -0
- metalsurfer-0.3.0/tests/test_dependency_behavior.py +229 -0
- metalsurfer-0.3.0/tests/test_exceptions.py +36 -0
- metalsurfer-0.3.0/tests/test_filters.py +1244 -0
- metalsurfer-0.3.0/tests/test_import.py +226 -0
- metalsurfer-0.3.0/tests/test_integration_co2_mof.py +125 -0
- metalsurfer-0.3.0/tests/test_integration_ethene_ru.py +106 -0
- metalsurfer-0.3.0/tests/test_integration_h2_pt12.py +148 -0
- metalsurfer-0.3.0/tests/test_integration_h2_ru_slab.py +108 -0
- metalsurfer-0.3.0/tests/test_integration_seeded.py +177 -0
- metalsurfer-0.3.0/tests/test_ml.py +684 -0
- metalsurfer-0.3.0/tests/test_models.py +442 -0
- metalsurfer-0.3.0/tests/test_observability.py +518 -0
- metalsurfer-0.3.0/tests/test_optimization.py +322 -0
- metalsurfer-0.3.0/tests/test_placement.py +1915 -0
- metalsurfer-0.3.0/tests/test_saturation.py +1595 -0
- metalsurfer-0.3.0/tests/test_surface_prep.py +260 -0
- metalsurfer-0.3.0/tests/test_surfaces.py +864 -0
- metalsurfer-0.3.0/tests/test_symmetry.py +378 -0
- metalsurfer-0.3.0/tests/test_workflow.py +997 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metalsurfer
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Adsorption on arbitrary materials
|
|
5
|
+
Author: metalsurfer contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: adsorption,screening,catalysis,ase,mlip
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: numpy<2.3,>=1.26
|
|
17
|
+
Requires-Dist: ase
|
|
18
|
+
Requires-Dist: spglib
|
|
19
|
+
Requires-Dist: pandas
|
|
20
|
+
Requires-Dist: rdkit<2026.3.3,>=2024.9
|
|
21
|
+
Requires-Dist: scipy
|
|
22
|
+
Requires-Dist: scikit-learn
|
|
23
|
+
Provides-Extra: mlip
|
|
24
|
+
Requires-Dist: torch; extra == "mlip"
|
|
25
|
+
Requires-Dist: torch-sim-atomistic>=0.5.2; extra == "mlip"
|
|
26
|
+
Requires-Dist: fairchem-core>=2.7; extra == "mlip"
|
|
27
|
+
Requires-Dist: fairchem-data-oc; extra == "mlip"
|
|
28
|
+
Requires-Dist: setuptools<82,>=65; extra == "mlip"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
Requires-Dist: coverage; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy; extra == "dev"
|
|
35
|
+
Requires-Dist: pandas-stubs; extra == "dev"
|
|
36
|
+
Requires-Dist: scipy-stubs; extra == "dev"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
39
|
+
Requires-Dist: furo; extra == "docs"
|
|
40
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
|
|
41
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# Metalsurfer
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Library for adsorption on arbitrary materials (slabs, nanoparticles, and periodic porous frameworks).
|
|
6
|
+
|
|
7
|
+
Metalsurfer is substrate-agnostic: pass any ASE `Atoms` object—periodic slab, fully periodic porous framework, or non-periodic cluster—after optional prep with `prepare_substrate` (equilibration, PBC, ASE `FixAtoms`). Supply adsorbates as SMILES; the library builds conformers, finds adsorption sites (orientation-aware Voronoi/topology hybrid, material-aware via `AdsorptionConfig.material_type`), deposits candidates with orientation/height sampling, relaxes with an MLIP, validates geometry, and ranks by adsorption energy. The four `run_*` campaign APIs orchestrate screening, Bayesian placement search, or sequential saturation on that pipeline.
|
|
8
|
+
|
|
9
|
+
**Documentation**: https://metalsurfer.readthedocs.io
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Requires **Python 3.12 or newer**.
|
|
14
|
+
|
|
15
|
+
Core dependencies only (library import and CPU-only workflow tests):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Running examples, scripts, or any `run_*` campaign requires the MLIP stack:**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e ".[mlip]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For TorchSim/FairChem-backed relaxation plus the developer toolchain:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e ".[mlip,dev]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `[dev]` extra includes **ruff**, **mypy**, type stubs, and pytest tooling.
|
|
34
|
+
|
|
35
|
+
## Quick Examples
|
|
36
|
+
|
|
37
|
+
Examples in `examples/` demonstrate basic usage and an advanced saturation workflow:
|
|
38
|
+
|
|
39
|
+
### Ethene Adsorption on Pt Nanocluster
|
|
40
|
+
```bash
|
|
41
|
+
# 12-atom Pt cluster with ethene adsorption
|
|
42
|
+
python examples/ethene_pt12_binding_energy.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### CO2 Adsorption in MOF
|
|
46
|
+
```bash
|
|
47
|
+
# Real MOF structure (RUBTAK01) with CO2 adsorption
|
|
48
|
+
python examples/co2_mof_binding_energy.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Ethene Adsorption on Ru(0001) Slab
|
|
52
|
+
```bash
|
|
53
|
+
# Ru(0001) slab with ethene adsorption
|
|
54
|
+
python examples/ethene_ru_slab_binding_energy.py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### H₂ Dissociative Adsorption on Ru(0001)
|
|
58
|
+
```bash
|
|
59
|
+
# Ru(0001) slab; skip_topology_check=True for H2 → 2H placements
|
|
60
|
+
python examples/h2_ru_slab_binding_energy.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Camphor on Cu(111) (Bayesian, GPU-heavy)
|
|
64
|
+
```bash
|
|
65
|
+
# BO placement search on a literature DFT slab (slab_relaxation_mode="none")
|
|
66
|
+
python examples/camphor_cu111_binding_energy.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### HPC / advanced
|
|
70
|
+
|
|
71
|
+
**Bipyridine saturation on defected Au(111)** — HPC-scale demo (1000 placements per step). Use the copy under `scripts/` for batch jobs:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python examples/bipyridine_au111_defects_saturation_raw.py
|
|
75
|
+
# or: python scripts/bipyridine_au111_defects_saturation_raw.py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
These examples span Pt, Ru, MOF, Au(111), and Cu(111); the same API accepts any ASE `Atoms` or prepared slab.
|
|
79
|
+
|
|
80
|
+
- Use pure ASE for receptor preparation (or `prepare_substrate` from a bulk id)
|
|
81
|
+
- Quick demos use modest explicit placement counts; omit `num_placements` to autotune to GPU parallel capacity (see `AdsorptionConfig`)
|
|
82
|
+
- Demonstrate different material types (`nanoparticle`, `porous`, and `slab`)
|
|
83
|
+
- Produce XYZ structures and CSV results (VASP inputs are opt-in via `write_vasp_inputs=True`)
|
|
84
|
+
|
|
85
|
+
## Python API
|
|
86
|
+
|
|
87
|
+
The library exposes four high-level entry points:
|
|
88
|
+
|
|
89
|
+
| Function | Role |
|
|
90
|
+
| -------- | ---- |
|
|
91
|
+
| `run_adsorption` | Standard screening: enumerate placements, relax, filter, rank. |
|
|
92
|
+
| `run_adsorption_bo` | Same pipeline with Bayesian optimization over placement candidates. |
|
|
93
|
+
| `run_saturation` | Sequential saturation: repeated adsorption onto an evolving slab. Returns `SaturationCampaignResult`. |
|
|
94
|
+
| `run_saturation_bo` | Saturation with BO-guided placement selection. Returns `SaturationCampaignResult`. |
|
|
95
|
+
|
|
96
|
+
Each accepts either an in-memory `list[tuple[str, str]]` of `(smiles, name)` pairs or a path to a SMILES CSV as `molecules`. `run_saturation` and `run_saturation_bo` require an explicit `molecules` argument (there is no default file). With `skip_existing=True` (default), binding campaigns skip molecules already in `adsorption_energies_detailed.csv`; saturation campaigns skip molecules already in `saturation_summary.csv` (both input forms).
|
|
97
|
+
|
|
98
|
+
### Surfaces: ASE Atoms, bulk prep, or containers
|
|
99
|
+
|
|
100
|
+
Surfaces are **not** tied to a specific element. Pass any `ase.Atoms` you already have (clusters, slabs, MOFs from CIF, saturated intermediates from XYZ), or build one with `prepare_substrate(bulk_id=...)`. Use `SlabContainer` only when you need its metadata helpers.
|
|
101
|
+
|
|
102
|
+
`AdsorptionConfig.material_type` (`slab`, `nanoparticle`, or `porous`) controls placement and validation geometry, not the chemical symbols in the structure.
|
|
103
|
+
|
|
104
|
+
All four `run_*` entry points accept plain `Atoms` or `SlabContainer`, but the substrate must be **campaign-ready** before the call: **equilibrated ionic positions** (via `prepare_substrate`, default `slab_relaxation_mode="ionic_only"`), correct PBC for `material_type`, bottom-anchored slab geometry when applicable, and ASE `FixAtoms` attached during prep (default: entire substrate frozen).
|
|
105
|
+
|
|
106
|
+
**Slab geometry:** For `material_type="slab"`, set the adsorption surface at `max(z)` with vacuum above. Alignment, PBC, freeze constraints, and in-plane sizing happen during **prep** (`prepare_substrate`, `create_slab_from_atoms`, `resize_substrate_for_molecule` from `metalsurfer.surface_prep`) — campaign APIs validate only. See the [surface engineering guide](https://metalsurfer.readthedocs.io/en/latest/guides/surface_engineering.html) for details.
|
|
107
|
+
|
|
108
|
+
**Prep vs adsorption:** `slab_relaxation_mode` equilibrates the substrate **before** campaigns. During placement relaxation, only adsorbate atoms and any substrate atoms **not** in ASE `FixAtoms` can move. With `relax_top_layer=True` on `prepare_substrate`, the free atoms depend on `material_type` (slab top layer, nanoparticle outer shell, porous pore boundary). See the [surface engineering guide](https://metalsurfer.readthedocs.io/en/latest/guides/surface_engineering.html). For custom patterns, attach your own ASE constraints during prep.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from ase.build import fcc111
|
|
114
|
+
|
|
115
|
+
from metalsurfer import AdsorptionConfig, run_adsorption
|
|
116
|
+
from metalsurfer.surface_prep import prepare_substrate
|
|
117
|
+
|
|
118
|
+
config = AdsorptionConfig(
|
|
119
|
+
material_type="slab", # "slab", "nanoparticle", or "porous"
|
|
120
|
+
seed=42,
|
|
121
|
+
)
|
|
122
|
+
slab = prepare_substrate(
|
|
123
|
+
slab=fcc111("Ru", size=(3, 3, 3), vacuum=12.0),
|
|
124
|
+
config=config,
|
|
125
|
+
results_dir="results_ru111_from_ase",
|
|
126
|
+
)
|
|
127
|
+
result = run_adsorption(
|
|
128
|
+
slab=slab,
|
|
129
|
+
molecules=[("O", "water")],
|
|
130
|
+
config=config,
|
|
131
|
+
surface_type="ru111_from_ase_atoms",
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
You may pass `SlabContainer` or bare `Atoms` to `run_*` once prep is complete.
|
|
136
|
+
|
|
137
|
+
### Slab sizing and PBC
|
|
138
|
+
|
|
139
|
+
Prepare substrates **outside** campaign APIs:
|
|
140
|
+
|
|
141
|
+
- Use a large enough `supercell` in `prepare_substrate`, or call `auto_resize_substrate_for_molecule` / `resize_substrate_for_molecule` (from `metalsurfer.surface_prep`) after conformer generation.
|
|
142
|
+
- `min_pbc_image_separation` (default 8 Å) controls the resize helper.
|
|
143
|
+
- Campaign entry validates PBC, slab anchoring, vacuum, and freeze constraints. Adsorbate-size / in-plane image-separation checks run after conformer generation (use the resize helpers during prep when needed).
|
|
144
|
+
|
|
145
|
+
### 1. Standard Screening
|
|
146
|
+
|
|
147
|
+
Use the campaign API when your driving script already has the molecule list in memory and you want a typed `BindingCampaignResult` back.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from metalsurfer import AdsorptionConfig, run_adsorption
|
|
151
|
+
from metalsurfer.surface_prep import prepare_substrate
|
|
152
|
+
|
|
153
|
+
config = AdsorptionConfig(
|
|
154
|
+
material_type="slab", # "slab", "nanoparticle", or "porous"
|
|
155
|
+
seed=42,
|
|
156
|
+
num_conformers=8,
|
|
157
|
+
num_placements=80, # or omit to autotune to GPU parallel capacity
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
slab = prepare_substrate(
|
|
161
|
+
bulk_id="mp-33",
|
|
162
|
+
miller_indices=(0, 0, 1),
|
|
163
|
+
config=config,
|
|
164
|
+
results_dir="results_Ru0001",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
molecules = [
|
|
168
|
+
("CC", "ethane"),
|
|
169
|
+
("C=C", "ethene"),
|
|
170
|
+
("C#C", "acetylene"),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
result = run_adsorption(
|
|
174
|
+
slab=slab,
|
|
175
|
+
molecules=molecules,
|
|
176
|
+
config=config,
|
|
177
|
+
surface_type="Ru0001",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
print(result.mode)
|
|
181
|
+
print(result.total_configurations)
|
|
182
|
+
for summary in result.molecule_summaries:
|
|
183
|
+
print(summary.molecule, summary.best_adsorption_energy)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Pass a CSV path to `run_adsorption` for file-driven batch screening (same XYZ/CSV outputs and `BindingCampaignResult` fields as an in-memory list). CSV files use two columns `(smiles, name)` and may include an optional header row (`smiles,molecule`):
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from metalsurfer import AdsorptionConfig, run_adsorption
|
|
190
|
+
from metalsurfer.surface_prep import prepare_substrate
|
|
191
|
+
|
|
192
|
+
config = AdsorptionConfig(
|
|
193
|
+
material_type="slab", # "slab", "nanoparticle", or "porous"
|
|
194
|
+
seed=42
|
|
195
|
+
)
|
|
196
|
+
slab = prepare_substrate(
|
|
197
|
+
bulk_id="mp-33",
|
|
198
|
+
miller_indices=(0, 0, 1),
|
|
199
|
+
config=config,
|
|
200
|
+
results_dir="results_Ru0001",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
result = run_adsorption(
|
|
204
|
+
slab=slab,
|
|
205
|
+
molecules="molecules.csv",
|
|
206
|
+
config=config,
|
|
207
|
+
surface_type="Ru0001",
|
|
208
|
+
skip_existing=True, # default: skip molecules already in adsorption_energies_detailed.csv
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Use `run_adsorption_bo` (not `bo_enabled=True` on `run_adsorption`) for Bayesian placement search; the non-BO entry point emits a warning if `bo_enabled=True` is set on the config.
|
|
213
|
+
|
|
214
|
+
### 2. Bayesian Screening
|
|
215
|
+
|
|
216
|
+
Bayesian mode keeps the same physical pipeline and output types, but replaces exhaustive placement evaluation with surrogate-guided candidate selection.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from metalsurfer import (
|
|
220
|
+
AdsorptionConfig,
|
|
221
|
+
run_adsorption_bo,
|
|
222
|
+
)
|
|
223
|
+
from metalsurfer.surface_prep import prepare_substrate
|
|
224
|
+
|
|
225
|
+
config = AdsorptionConfig(
|
|
226
|
+
material_type="slab", # "slab", "nanoparticle", or "porous"
|
|
227
|
+
seed=42,
|
|
228
|
+
# Defaults: ridge surrogate, EI acquisition, autotuned batch sizes
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
slab = prepare_substrate(
|
|
232
|
+
bulk_id="mp-33",
|
|
233
|
+
miller_indices=(0, 0, 1),
|
|
234
|
+
config=config,
|
|
235
|
+
results_dir="results_Ru0001_bo",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
result = run_adsorption_bo(
|
|
239
|
+
slab=slab,
|
|
240
|
+
molecules=[("O=C=O", "co2"), ("O", "water")],
|
|
241
|
+
config=config,
|
|
242
|
+
surface_type="Ru0001_bo",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
print(result.mode)
|
|
246
|
+
print(result.failure_summaries)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Relevant BO configuration fields live on `AdsorptionConfig`:
|
|
250
|
+
|
|
251
|
+
- `num_placements` (default `None`: autotune to GPU parallel capacity at runtime)
|
|
252
|
+
- `bo_initial_random`, `bo_batch_size` (default `None`: autotune to GPU parallel capacity)
|
|
253
|
+
- `bo_total_budget` (default `18`: acquisition batches after the initial random batch)
|
|
254
|
+
- Total evaluations once auto fields resolve: `bo_initial_random + bo_total_budget * bo_batch_size`
|
|
255
|
+
- `bo_acquisition` with `"lcb"`, `"ei"`, or `"pi"`
|
|
256
|
+
- `bo_surrogate` with `"random_forest"`, `"extra_trees"`, `"gradient_boost"`, `"ridge"`, or `"ensemble"` (default: `"ridge"`)
|
|
257
|
+
- `bo_transfer_*` for saturation transfer BO (default: weighted mode with 2-step memory window, recency and occupancy decay; `gradient_boost` does not support transfer sample weights)
|
|
258
|
+
- `bo_include_failure_negatives` and `bo_failure_penalty_*` for learning from failed placements
|
|
259
|
+
|
|
260
|
+
### 3. Sequential Saturation
|
|
261
|
+
|
|
262
|
+
Saturation mode repeatedly adsorbs the current best configuration onto the evolving slab until adsorption is no longer favorable or no valid placements remain.
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from metalsurfer import AdsorptionConfig, MultiMolSaturationRunResult, run_saturation
|
|
266
|
+
from metalsurfer.surface_prep import prepare_substrate
|
|
267
|
+
|
|
268
|
+
config = AdsorptionConfig(
|
|
269
|
+
material_type="slab", # "slab", "nanoparticle", or "porous"
|
|
270
|
+
seed=42,
|
|
271
|
+
num_conformers=6,
|
|
272
|
+
num_placements=60,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
slab = prepare_substrate(
|
|
276
|
+
bulk_id="mp-33",
|
|
277
|
+
miller_indices=(0, 0, 1),
|
|
278
|
+
config=config,
|
|
279
|
+
results_dir="results_Ru0001_sat",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Persists to results_Ru0001_sat/ when save_results=True (default), using the same
|
|
283
|
+
# config (per-step best slabs plus step_*_placements/ when saturation_save_all_placements is true).
|
|
284
|
+
campaign = run_saturation(
|
|
285
|
+
slab=slab,
|
|
286
|
+
molecules="molecules.csv",
|
|
287
|
+
config=config,
|
|
288
|
+
surface_type="Ru0001_sat",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
for entry in campaign.runs:
|
|
292
|
+
if isinstance(entry, MultiMolSaturationRunResult):
|
|
293
|
+
print(entry.molecules, entry.n_molecules_at_saturation)
|
|
294
|
+
else:
|
|
295
|
+
print(entry.molecule, entry.n_molecules_at_saturation)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Important saturation behaviors:
|
|
299
|
+
|
|
300
|
+
- **Prep vs adsorption relaxation:** `slab_relaxation_mode` (default `ionic_only`) equilibrates substrate ionic positions during `prepare_substrate`. Freeze policy is written to ASE `FixAtoms` via prep kwargs (default: entire substrate frozen). `relax_top_layer=True` is a material-aware shortcut (see [surface engineering guide](https://metalsurfer.readthedocs.io/en/latest/guides/surface_engineering.html)). Saturation pins `base_slab` at campaign start. Compare optimized structures to the matching prep snapshot (e.g. `clean_slab_Au20_*` after adatoms), not pre-adatom `clean_slab` files.
|
|
301
|
+
- In-plane supercell expansion must be done during prep (`auto_resize_substrate_for_molecule` / `resize_substrate_for_molecule` from `metalsurfer.surface_prep`) before calling campaign APIs.
|
|
302
|
+
- Call `run_saturation_bo` for Bayesian saturation; it forces BO on. The `bo_transfer_*` settings control cross-step observation reuse.
|
|
303
|
+
- When `multi_molecule_saturation=True` and multiple molecules are provided (in-memory list or CSV), the workflow switches to competitive saturation, where molecules compete for each step and the best overall adsorption wins.
|
|
304
|
+
- Competitive saturation with BO: call `run_saturation_bo`; each adsorbate trains and carries forward its own BO state independently (observations are not shared across adsorbates).
|
|
305
|
+
- By default, `saturation_save_all_placements=True` writes every validated placement per step under `xyz_structures/.../step_{NNN}_placements/`, plus `saturation_placements_detailed.csv`. Matching `vasp_inputs/...` trees are written only when `write_vasp_inputs=True`. Set `saturation_save_all_placements=False` to persist only the per-step best structures (smaller disk use).
|
|
306
|
+
- By default, `saturation_discard_topology_rearrangements=True` re-checks the full adsorbate pool on each candidate **before** choosing the step winner: adsorbates must form the expected number of connected fragments (connectivity-only guard). This catches inter-adsorbate coupling or unexpected splitting that per-placement filtering can miss while allowing strong adsorbate-material interactions that preserve adsorbate connectivity. Set `False` to rank only by `E_ads`; the guard is also skipped when `skip_topology_check=True`.
|
|
307
|
+
- When printing saturation completion summaries, pass `write_vasp_inputs=config.write_vasp_inputs` to `campaign.format_completion(...)` so the saved-files line matches actual output.
|
|
308
|
+
- Contributor test markers (`gpu`, `slow`): see the [development guide](https://metalsurfer.readthedocs.io/en/latest/guides/development.html).
|
|
309
|
+
|
|
310
|
+
### Surface setup and modifiers
|
|
311
|
+
|
|
312
|
+
Use [`metalsurfer.surface_prep`](https://metalsurfer.readthedocs.io/en/latest/api/surface_prep.html) as the single import path for substrate preparation. The orchestrator is [`prepare_substrate`](https://metalsurfer.readthedocs.io/en/latest/api/surface_prep.html#metalsurfer.surface_prep.prepare_substrate).
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from metalsurfer import AdsorptionConfig
|
|
316
|
+
from metalsurfer.surface_prep import prepare_substrate
|
|
317
|
+
|
|
318
|
+
config = AdsorptionConfig(material_type="slab", seed=42)
|
|
319
|
+
|
|
320
|
+
slab = prepare_substrate(
|
|
321
|
+
bulk_id="mp-33",
|
|
322
|
+
miller_indices=(0, 0, 1),
|
|
323
|
+
alloy_host="Ru",
|
|
324
|
+
alloy_guest="Cu",
|
|
325
|
+
alloy_fraction=0.25,
|
|
326
|
+
adatom_symbol="Sn",
|
|
327
|
+
adatom_coverage=0.20,
|
|
328
|
+
config=config,
|
|
329
|
+
results_dir="results_Ru0001",
|
|
330
|
+
adatom_relaxation_mode="ionic_only", # optional: full clean slab once, ionic-only after adatoms
|
|
331
|
+
)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
See the [Surface Engineering guide](https://metalsurfer.readthedocs.io/en/latest/guides/surface_engineering.html) for prep relaxation presets and substrate freeze behavior during adsorption.
|
|
335
|
+
|
|
336
|
+
For adatoms on an existing slab (e.g. frozen-base workflows), pass `slab=` after building the base:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
base_slab = prepare_substrate(bulk_id="mp-33", miller_indices=(0, 0, 1), config=config, results_dir=results_dir)
|
|
340
|
+
slab = prepare_substrate(
|
|
341
|
+
slab=base_slab,
|
|
342
|
+
adatom_symbol="Sn",
|
|
343
|
+
adatom_coverage=0.10,
|
|
344
|
+
config=config,
|
|
345
|
+
results_dir=results_dir,
|
|
346
|
+
)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Lower-level helpers (`create_slab_from_bulk`, `substitute_alloy`, `deposit_adatoms`) are available from `metalsurfer.surface_prep` for custom research loops; finalize with `prepare_substrate(slab=...)` or `finalize_substrate` after manual PBC + `apply_surface_constraints` before calling campaign APIs.
|
|
350
|
+
|
|
351
|
+
`AdsorptionConfig.material_type` must be chosen explicitly:
|
|
352
|
+
|
|
353
|
+
- `"slab"`: in-plane periodic surfaces.
|
|
354
|
+
- `"nanoparticle"`: non-periodic clusters.
|
|
355
|
+
- `"porous"`: fully periodic porous frameworks.
|
|
356
|
+
|
|
357
|
+
This choice affects site generation, adsorption validation, and distance handling throughout the workflow.
|
|
358
|
+
|
|
359
|
+
## What the core pipeline does
|
|
360
|
+
|
|
361
|
+
See the introduction above for the high-level mental model. Across all run modes, the library follows the same structure:
|
|
362
|
+
|
|
363
|
+
1. Build or accept a surface structure.
|
|
364
|
+
2. Generate and deduplicate molecular conformers.
|
|
365
|
+
3. Enumerate deterministic `PlacementSpec` candidates over conformer, site, orientation, tilt, azimuth, and height. Site detection is orientation-aware (slab normal, hybrid topology + Voronoi); BO features use materialized absolute geometry only (`x_abs`, `y_abs`, `z_abs`, quaternion)—not `site_index` or orientation labels.
|
|
366
|
+
4. Materialize placements into full adsorbate-slab structures.
|
|
367
|
+
5. Relax structures with the configured MLIP backend.
|
|
368
|
+
6. Validate adsorption geometry and filter decomposed, desorbed, or duplicate structures.
|
|
369
|
+
7. Rank surviving structures and persist structures, CSV summaries, and metadata.
|
|
370
|
+
|
|
371
|
+
Placement generation is **orientation-aware** (slab normal, hybrid topology + Voronoi) and works across slabs, nanoparticles, and porous materials. Bayesian mode changes candidate selection, not the downstream physics or filtering stack; surrogate inputs are resolved absolute poses, not discrete site IDs.
|
|
372
|
+
|
|
373
|
+
## Results and persistence
|
|
374
|
+
|
|
375
|
+
The output directory is `results_{surface_type}`. Depending on run mode, the library may write:
|
|
376
|
+
|
|
377
|
+
- `adsorption_energies_detailed.csv`
|
|
378
|
+
- `adsorption_energy_summary.csv`
|
|
379
|
+
- `saturation_details.csv`
|
|
380
|
+
- `saturation_placements_detailed.csv` (saturation runs when `saturation_save_all_placements` is true: one row per step × placement with paths and descriptor context)
|
|
381
|
+
- `saturation_summary.csv`
|
|
382
|
+
- `run_metadata.json` (when `write_settings=True` writes config snapshot; `write_metadata=True` adds timing/counts; both merge into the same file)
|
|
383
|
+
- `ml_dataset.csv`, `ml_dataset_metadata.json` (from `DatasetLogger` during binding campaigns and saturation)
|
|
384
|
+
- `xyz_structures/...`
|
|
385
|
+
- `vasp_inputs/...` (only when `write_vasp_inputs=True`)
|
|
386
|
+
|
|
387
|
+
Campaign APIs save XYZ structures and summary tables by default (`run_saturation` / `run_saturation_bo` call `save_saturation_results(..., config=config)` so placement-tree output follows `saturation_save_all_placements` and the rest of `AdsorptionConfig`). VASP bundles require `write_vasp_inputs=True` on `AdsorptionConfig`. Workflow APIs return typed results and can be paired with `save_summary_results(...)`, `save_saturation_results(...)`, `save_multi_mol_saturation_results(...)`, and `write_run_metadata(...)` / `write_run_settings(...)` (both merge into `run_metadata.json`) for explicit persistence control (for example after `save_results=False` or custom paths).
|
|
388
|
+
|
|
389
|
+
Use `results_dir(surface_type)` from `metalsurfer.io_results` (or `metalsurfer.results_dir` via lazy import) for the canonical `results_{surface_type}/` path.
|
|
390
|
+
|
|
391
|
+
## Logging
|
|
392
|
+
|
|
393
|
+
Call `configure_logging()` at the start of scripts (all `examples/` and `scripts/` already do). Workflows emit structured logs with optional context (`molecule`, `surface_type`, `placement_id`, `seed`) via `log_context`.
|
|
394
|
+
|
|
395
|
+
Environment overrides:
|
|
396
|
+
|
|
397
|
+
- `METALSURFER_LOG_LEVEL` (default: `INFO`)
|
|
398
|
+
- `TORCHSIM_LOG_LEVEL` (default: `WARNING`)
|
|
399
|
+
|
|
400
|
+
Logs go to **stdout** by default so HPC job `.out` files capture progress. TorchSim stdout/stderr is captured during relaxation (`torchsim_output_capture` in `src/metalsurfer/_logging.py`).
|
|
401
|
+
|
|
402
|
+
## Development
|
|
403
|
+
|
|
404
|
+
Pre-release GPU smoke (fresh runs, excludes bipyridine):
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
pip install -e ".[mlip,dev]"
|
|
408
|
+
./scripts/run_all_examples.sh
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Full local test parity (includes MLIP and GPU phases):
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
./scripts/run_all_tests.sh
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
CI-parity lint and fast tests:
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
pip install -e ".[dev]" # GPU stack: ".[mlip,dev]"
|
|
421
|
+
ruff check . && ruff format --check . && mypy src/metalsurfer
|
|
422
|
+
python -m pytest tests/ -m "not dependency_behavior and not mlip and not gpu and not slow" \
|
|
423
|
+
--cov=src/metalsurfer --cov-report=term-missing --tb=short -v
|
|
424
|
+
coverage report --fail-under=74
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
CI parity, coverage gates, GPU/slow test jobs: [development guide](https://metalsurfer.readthedocs.io/en/latest/guides/development.html). Architecture: [CORE_SYSTEM_EXPLANATION.md](CORE_SYSTEM_EXPLANATION.md).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "metalsurfer"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Adsorption on arbitrary materials"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "metalsurfer contributors" }]
|
|
12
|
+
keywords = ["adsorption", "screening", "catalysis", "ase", "mlip"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"numpy>=1.26,<2.3",
|
|
24
|
+
"ase",
|
|
25
|
+
"spglib",
|
|
26
|
+
"pandas",
|
|
27
|
+
"rdkit>=2024.9,<2026.3.3",
|
|
28
|
+
"scipy",
|
|
29
|
+
"scikit-learn",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
mlip = [
|
|
34
|
+
"torch",
|
|
35
|
+
"torch-sim-atomistic>=0.5.2",
|
|
36
|
+
"fairchem-core>=2.7",
|
|
37
|
+
"fairchem-data-oc",
|
|
38
|
+
"setuptools>=65,<82",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest",
|
|
42
|
+
"pytest-cov",
|
|
43
|
+
"coverage",
|
|
44
|
+
"ruff",
|
|
45
|
+
"mypy",
|
|
46
|
+
"pandas-stubs",
|
|
47
|
+
"scipy-stubs",
|
|
48
|
+
]
|
|
49
|
+
docs = [
|
|
50
|
+
"sphinx>=7.0",
|
|
51
|
+
"furo",
|
|
52
|
+
"sphinx-autodoc-typehints",
|
|
53
|
+
"sphinx-copybutton",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.package-data]
|
|
60
|
+
metalsurfer = ["py.typed"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
python_version = "3.12"
|
|
64
|
+
warn_redundant_casts = true
|
|
65
|
+
warn_unused_ignores = true
|
|
66
|
+
show_error_codes = true
|
|
67
|
+
|
|
68
|
+
[[tool.mypy.overrides]]
|
|
69
|
+
module = ["fairchem.*", "torch_sim.*", "torch.*", "sklearn.*"]
|
|
70
|
+
ignore_missing_imports = true
|
|
71
|
+
|
|
72
|
+
[[tool.mypy.overrides]]
|
|
73
|
+
module = ["rdkit", "rdkit.*"]
|
|
74
|
+
follow_imports = "skip"
|
|
75
|
+
ignore_missing_imports = true
|
|
76
|
+
|
|
77
|
+
[[tool.mypy.overrides]]
|
|
78
|
+
module = ["metalsurfer.placement.sites"]
|
|
79
|
+
disable_error_code = ["misc", "has-type", "union-attr", "arg-type", "attr-defined"]
|
|
80
|
+
|
|
81
|
+
[tool.ruff]
|
|
82
|
+
target-version = "py312"
|
|
83
|
+
line-length = 88
|
|
84
|
+
exclude = [".git", ".pytest_cache", "__pycache__", "*.egg-info", "results_*", "results", ".ruff_cache", "do_not_stage_tests"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
select = [
|
|
88
|
+
"E", # pycodestyle errors
|
|
89
|
+
"F", # pyflakes
|
|
90
|
+
"I", # isort (import sorting)
|
|
91
|
+
"B", # bugbear
|
|
92
|
+
"UP", # pyupgrade
|
|
93
|
+
"SIM", # simplify
|
|
94
|
+
"ERA", # commented-out code
|
|
95
|
+
"RET", # unnecessary return / else
|
|
96
|
+
"RUF100", # stale noqa
|
|
97
|
+
"FURB105", # print("") -> print()
|
|
98
|
+
"PGH003", # blanket type: ignore
|
|
99
|
+
]
|
|
100
|
+
ignore = ["E501"] # line length handled by formatter
|
|
101
|
+
|
|
102
|
+
[tool.ruff.lint.per-file-ignores]
|
|
103
|
+
"scripts/**/*.py" = ["T201", "EXE001", "E402"]
|
|
104
|
+
"examples/**/*.py" = ["T201", "EXE001"]
|
|
105
|
+
"tests/**/*.py" = ["S101"]
|
|
106
|
+
|
|
107
|
+
[tool.ruff.format]
|
|
108
|
+
quote-style = "double"
|