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.
Files changed (70) hide show
  1. metalsurfer-0.3.0/PKG-INFO +41 -0
  2. metalsurfer-0.3.0/README.md +427 -0
  3. metalsurfer-0.3.0/pyproject.toml +108 -0
  4. metalsurfer-0.3.0/setup.cfg +4 -0
  5. metalsurfer-0.3.0/src/metalsurfer/__init__.py +147 -0
  6. metalsurfer-0.3.0/src/metalsurfer/_logging.py +325 -0
  7. metalsurfer-0.3.0/src/metalsurfer/_utils.py +13 -0
  8. metalsurfer-0.3.0/src/metalsurfer/campaigns.py +583 -0
  9. metalsurfer-0.3.0/src/metalsurfer/config.py +695 -0
  10. metalsurfer-0.3.0/src/metalsurfer/conformers.py +249 -0
  11. metalsurfer-0.3.0/src/metalsurfer/exceptions.py +29 -0
  12. metalsurfer-0.3.0/src/metalsurfer/filters.py +604 -0
  13. metalsurfer-0.3.0/src/metalsurfer/io_results.py +751 -0
  14. metalsurfer-0.3.0/src/metalsurfer/ml/__init__.py +11 -0
  15. metalsurfer-0.3.0/src/metalsurfer/ml/bayesian.py +927 -0
  16. metalsurfer-0.3.0/src/metalsurfer/ml/dataset.py +217 -0
  17. metalsurfer-0.3.0/src/metalsurfer/ml/features.py +147 -0
  18. metalsurfer-0.3.0/src/metalsurfer/ml/predict.py +123 -0
  19. metalsurfer-0.3.0/src/metalsurfer/ml/regression.py +307 -0
  20. metalsurfer-0.3.0/src/metalsurfer/ml/reproduce.py +158 -0
  21. metalsurfer-0.3.0/src/metalsurfer/ml/schema.py +579 -0
  22. metalsurfer-0.3.0/src/metalsurfer/models.py +773 -0
  23. metalsurfer-0.3.0/src/metalsurfer/optimization.py +1300 -0
  24. metalsurfer-0.3.0/src/metalsurfer/placement/__init__.py +64 -0
  25. metalsurfer-0.3.0/src/metalsurfer/placement/_constants.py +211 -0
  26. metalsurfer-0.3.0/src/metalsurfer/placement/_material.py +59 -0
  27. metalsurfer-0.3.0/src/metalsurfer/placement/generators.py +1435 -0
  28. metalsurfer-0.3.0/src/metalsurfer/placement/geometry.py +856 -0
  29. metalsurfer-0.3.0/src/metalsurfer/placement/policy.py +206 -0
  30. metalsurfer-0.3.0/src/metalsurfer/placement/sites.py +1815 -0
  31. metalsurfer-0.3.0/src/metalsurfer/py.typed +0 -0
  32. metalsurfer-0.3.0/src/metalsurfer/surface_prep/__init__.py +69 -0
  33. metalsurfer-0.3.0/src/metalsurfer/surface_prep/prep.py +372 -0
  34. metalsurfer-0.3.0/src/metalsurfer/surfaces.py +1004 -0
  35. metalsurfer-0.3.0/src/metalsurfer/symmetry.py +460 -0
  36. metalsurfer-0.3.0/src/metalsurfer/workflow/__init__.py +15 -0
  37. metalsurfer-0.3.0/src/metalsurfer/workflow/bayesian.py +601 -0
  38. metalsurfer-0.3.0/src/metalsurfer/workflow/core.py +498 -0
  39. metalsurfer-0.3.0/src/metalsurfer/workflow/reference.py +92 -0
  40. metalsurfer-0.3.0/src/metalsurfer/workflow/saturation.py +913 -0
  41. metalsurfer-0.3.0/src/metalsurfer/workflow/shared.py +1070 -0
  42. metalsurfer-0.3.0/src/metalsurfer.egg-info/PKG-INFO +41 -0
  43. metalsurfer-0.3.0/src/metalsurfer.egg-info/SOURCES.txt +68 -0
  44. metalsurfer-0.3.0/src/metalsurfer.egg-info/dependency_links.txt +1 -0
  45. metalsurfer-0.3.0/src/metalsurfer.egg-info/requires.txt +29 -0
  46. metalsurfer-0.3.0/src/metalsurfer.egg-info/top_level.txt +1 -0
  47. metalsurfer-0.3.0/tests/test_bayesian.py +606 -0
  48. metalsurfer-0.3.0/tests/test_campaigns.py +589 -0
  49. metalsurfer-0.3.0/tests/test_config_validation.py +632 -0
  50. metalsurfer-0.3.0/tests/test_conformers.py +281 -0
  51. metalsurfer-0.3.0/tests/test_constraints.py +173 -0
  52. metalsurfer-0.3.0/tests/test_dependency_behavior.py +229 -0
  53. metalsurfer-0.3.0/tests/test_exceptions.py +36 -0
  54. metalsurfer-0.3.0/tests/test_filters.py +1244 -0
  55. metalsurfer-0.3.0/tests/test_import.py +226 -0
  56. metalsurfer-0.3.0/tests/test_integration_co2_mof.py +125 -0
  57. metalsurfer-0.3.0/tests/test_integration_ethene_ru.py +106 -0
  58. metalsurfer-0.3.0/tests/test_integration_h2_pt12.py +148 -0
  59. metalsurfer-0.3.0/tests/test_integration_h2_ru_slab.py +108 -0
  60. metalsurfer-0.3.0/tests/test_integration_seeded.py +177 -0
  61. metalsurfer-0.3.0/tests/test_ml.py +684 -0
  62. metalsurfer-0.3.0/tests/test_models.py +442 -0
  63. metalsurfer-0.3.0/tests/test_observability.py +518 -0
  64. metalsurfer-0.3.0/tests/test_optimization.py +322 -0
  65. metalsurfer-0.3.0/tests/test_placement.py +1915 -0
  66. metalsurfer-0.3.0/tests/test_saturation.py +1595 -0
  67. metalsurfer-0.3.0/tests/test_surface_prep.py +260 -0
  68. metalsurfer-0.3.0/tests/test_surfaces.py +864 -0
  69. metalsurfer-0.3.0/tests/test_symmetry.py +378 -0
  70. 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
+ ![Metalsurfer Logo](docs/_static/logo_metalsurfer.svg)
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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+