Quick Start¶
SCGO finds low-energy atomic structures with global optimization. This guide shows how to use each supported workflow.
A cluster (also called the core when molecules are present) is the metal nanoparticle whose shape you are searching. An adsorbate is a small molecule or fragment (for example OH or CO) attached to the cluster or the surface. A slab is a periodic surface the cluster or adsorbate sits on. You can also search the top layers of the slab itself, with the bottom layers held still.
System Types¶
You must specify one of six system types:
Type |
What you search |
|---|---|
|
Cluster in vacuum |
|
Cluster on a slab |
|
Cluster plus adsorbates in vacuum |
|
Cluster plus adsorbates on a slab |
|
Top layers of a bare slab |
|
Top slab layers plus adsorbates, no cluster |
For surface types, pass a surface_config. For adsorbate types, pass
adsorbates. When the slab itself is the search target
(surface / surface_adsorbate), also set fix_all_slab_atoms=False
and choose n_relax_top_slab_layers or n_fix_bottom_slab_layers.
Gas Cluster¶
Optimize a simple cluster in vacuum.
Fast test with EMT:
from scgo import run_go
from scgo.param_presets import get_testing_params
results = run_go(
["Pt"] * 4,
params=get_testing_params(),
seed=42,
system_type="gas_cluster",
)
for energy, atoms in results:
print(f"Energy: {energy:.4f} eV, Formula: {atoms.get_chemical_formula()}")
For multi-element clusters, atom order follows the composition list you pass in so genetic-algorithm crossover can pair structures safely. Heavier elements are favoured during initialization while the population stays diverse. Details: Cluster initialization.
Production run with MACE:
from scgo import run_go
from scgo.param_presets import get_default_params
params = get_default_params()
params["calculator_kwargs"]["model_name"] = "mace_mp_small"
results = run_go(
"Pt5",
params=params,
seed=42,
system_type="gas_cluster",
)
On a Surface¶
Use the built-in graphite surface or create your own slab.
Using graphite preset:
from scgo import run_go, make_graphite_surface_config
from scgo.param_presets import get_default_params
surface_config = make_graphite_surface_config(slab_layers=3)
results = run_go(
"Pt5",
params=get_default_params(),
seed=42,
surface_config=surface_config,
system_type="surface_cluster",
)
Use adsorption_energy() to compare adsorption energies on a slab.
Using a custom slab:
from ase.build import fcc111
from scgo import run_go, make_surface_config
from scgo.param_presets import get_default_params
slab = fcc111("Pt", size=(3, 3, 3), vacuum=10.0)
surface_config = make_surface_config(slab)
results = run_go(
"Pt4",
params=get_default_params(),
seed=42,
surface_config=surface_config,
system_type="surface_cluster",
)
Defining Custom Surfaces¶
Use SurfaceSystemConfig or the simpler make_surface_config() helper.
Slab motion options:
fix_all_slab_atoms=True: entire slab stays frozen (default)n_relax_top_slab_layers=2: allow top 2 layers to relaxn_fix_bottom_slab_layers=1: freeze bottom layer onlyBoth layer counts =
None: entire slab can relax
Do not use n_relax_top_slab_layers together with n_fix_bottom_slab_layers.
Full example:
from ase.build import fcc111
from scgo import run_go
from scgo.param_presets import get_default_params
from scgo.surface import SurfaceSystemConfig
slab = fcc111("Fe", size=(4, 4, 3), vacuum=12.0)
surface_config = SurfaceSystemConfig(
slab=slab,
adsorption_height_min=1.2,
adsorption_height_max=2.5,
fix_all_slab_atoms=False,
n_relax_top_slab_layers=2,
)
results = run_go(
"Pt5",
params=get_default_params(),
seed=42,
surface_config=surface_config,
system_type="surface_cluster",
)
With Adsorbates¶
Add adsorbate molecules (OH, CO, and so on) to your cluster.
Gas phase with adsorbate:
from ase import Atoms
from scgo import run_go
from scgo.param_presets import get_default_params
oh = Atoms("OH", positions=[[0, 0, 0], [0, 0, 0.96]])
results = run_go(
["Pt"] * 5,
params=get_default_params(),
seed=42,
system_type="gas_cluster_adsorbate",
adsorbates=oh,
)
Multiple adsorbates:
from ase import Atoms
from scgo import run_go
from scgo.param_presets import get_default_params
oh1 = Atoms("OH", positions=[[0, 0, 0], [0, 0, 0.96]])
oh2 = Atoms("OH", positions=[[0, 0, 0], [0, 0, 0.96]])
results = run_go(
"Pt5",
params=get_default_params(),
seed=42,
system_type="gas_cluster_adsorbate",
adsorbates=[oh1, oh2],
)
Defining Custom Adsorbates¶
Any ASE Atoms object is a valid adsorbate fragment. The genetic algorithm
will:
Place fragments on cluster surface sites
Keep fragments rigid (bonds stay intact)
Optionally reposition fragments during optimization
Tuning placement:
from ase import Atoms
from scgo import run_go
from scgo.param_presets import get_default_params
from scgo.cluster_adsorbate import ClusterAdsorbateConfig
oh = Atoms("OH", positions=[[0, 0, 0], [0, 0, 0.96]])
params = get_default_params()
params["cluster_adsorbate_config"] = ClusterAdsorbateConfig(
height_min=0.9,
height_max=2.2,
max_placement_attempts=200,
)
params["freeze_adsorbate_internal_geometry"] = True # Keep fragment rigid
results = run_go(
"Pt5",
params=params,
seed=42,
system_type="gas_cluster_adsorbate",
adsorbates=oh,
)
Use is_true_minimum() or perform_local_relaxation() to
check or re-relax candidates outside a full GO run.
Surface + Adsorbates¶
Combine surface and adsorbates.
from ase import Atoms
from scgo import run_go, make_graphite_surface_config
from scgo.param_presets import get_default_params
surface_config = make_graphite_surface_config(slab_layers=3)
oh = Atoms("OH", positions=[[0, 0, 0], [0, 0, 0.96]])
results = run_go(
"Pt5",
params=get_default_params(),
seed=42,
surface_config=surface_config,
system_type="surface_cluster_adsorbate",
adsorbates=oh,
)
Slab as search target¶
Use surface / surface_adsorbate when the search should move the
top slab layers rather than a deposited nanoparticle. Pass an empty composition
[] for the nanoparticle core. Presets such as
make_defected_graphite_surface_config() and
make_n_doped_graphite_surface_config() set
fix_all_slab_atoms=False, n_relax_top_slab_layers=1, and a filesystem
name for path keys.
Bare defected graphite:
from scgo import run_go, make_defected_graphite_surface_config
from scgo.param_presets import get_default_params
surface_config = make_defected_graphite_surface_config(
n_vacancies=1, seed=42
)
results = run_go(
[],
params=get_default_params(),
seed=42,
surface_config=surface_config,
system_type="surface",
)
OH on N-doped graphite:
from ase import Atoms
from scgo import run_go, make_n_doped_graphite_surface_config
from scgo.param_presets import get_default_params
surface_config = make_n_doped_graphite_surface_config(
n_dopants=2, seed=42
)
oh = Atoms("OH", positions=[[0, 0, 0], [0, 0, 0.97]])
results = run_go(
[],
params=get_default_params(),
seed=42,
surface_config=surface_config,
system_type="surface_adsorbate",
adsorbates=oh,
)
Transition States¶
Find transition states between optimized structures. SCGO builds a short list
of minima pairs, ranks them, and runs NEB only on the top candidates. The
budget is max_pairs.
How candidate pairs are chosen¶
SCGO does not run NEB on every possible combination. It builds and ranks candidates before spending any NEB force evaluations:
Load and dedupe minima. GO minima are loaded and filtered with the uniqueness rule (see Uniqueness (de-duplication)). For TS, the distance gate is tighter (
0.1Å instead of0.7Å).Enumerate pairs. Every remaining pair is considered.
Hard gates — drop bad pairs. A pair is skipped if its energy gap exceeds
energy_gap_threshold(2.0 eV for bare systems, 0.75 eV with adsorbates), if its shape difference exceedsmax_endpoint_mismatch, or, for adsorbate plus metal core, if core RMS exceedspair_core_rms_max.Soft rank. Survivors are scored with
pair_score_*weights. The score favors a mid-range energy gap, similar cores, and some adsorbate site motion. Bare systems favor distinct fingerprints instead.Budget and oversampling. Keep the top N pairs where N comes from the budget. Bare systems use
max_pairsdirectly. Adsorbate systems withmax_endpoint_mismatchset oversample tomin(max_pairs * 10, max(max_pairs, 50))and re-rank by IDPP path quality before keepingmax_pairsfor NEB. When TorchSim or IDPP screening is not available, the oversampled list is still truncated tomax_pairsbefore NEB.Run NEB. Only those N pairs run NEB. Bare
surface_clusterandsurfaceuse the same budget rule as bare gas (no oversampling even thoughmax_endpoint_mismatchis set).
Hard gates always apply. Soft scores only matter when the pool is larger than the budget. Pair selection and NEB endpoint setup share the same core overlay: gas cores are fingerprint-matched then Kabsch-aligned (including 1-atom translation); slab cores stay in the lab frame. Defaults by system type and full knob tables: All Parameters (Pair selection) and Validation and Constraints.
TS from existing minima (after a prior run_go or manual GO output):
from scgo import run_ts_search
from scgo.param_presets import get_ts_search_params
ts_params = get_ts_search_params(system_type="gas_cluster", seed=42)
ts_params["max_pairs"] = 10
# Campaign root: reads Pt5_searches/, writes Pt5_ts_results/ as sibling
results = run_ts_search(
"Pt5",
ts_params=ts_params,
seed=42,
output_dir="results/pt5_gas_mace",
system_type="gas_cluster",
)
# Or pass the searches directory directly (parent becomes campaign root)
results = run_ts_search(
"Pt5",
ts_params=ts_params,
seed=42,
searches_dir="results/pt5_gas_mace/Pt5_searches",
system_type="gas_cluster",
)
GO + TS combined:
from scgo import run_go_ts
from scgo.param_presets import get_torchsim_ga_params, get_ts_search_params
go_params = get_torchsim_ga_params(system_type="gas_cluster", seed=42)
go_params["n_jobs"] = -2 # all but one CPU, across every parallel stage
go_params["optimizer_params"]["ga"].update(
niter=10,
population_size=50,
)
ts_params = get_ts_search_params(system_type="gas_cluster", seed=42)
ts_params["max_pairs"] = 15
summary = run_go_ts(
"Pt5",
go_params=go_params,
ts_params=ts_params,
seed=42,
system_type="gas_cluster",
)
On a surface:
from scgo import run_go_ts, make_graphite_surface_config
from scgo.param_presets import get_torchsim_ga_params, get_ts_search_params
surface_config = make_graphite_surface_config(slab_layers=3)
go_params = get_torchsim_ga_params(
system_type="surface_cluster",
surface_config=surface_config,
seed=42,
)
go_params["n_jobs"] = -2 # all but one CPU, across every parallel stage
ts_params = get_ts_search_params(
system_type="surface_cluster",
surface_config=surface_config,
seed=42,
)
ts_params["max_pairs"] = 10
summary = run_go_ts(
"Pt5",
go_params=go_params,
ts_params=ts_params,
seed=42,
surface_config=surface_config,
system_type="surface_cluster",
)
Campaigns¶
Run multiple compositions in one call. Composition builders
(build_one_element_compositions, build_two_element_compositions) live in
scgo.runner_api, not the top-level scgo package.
Global optimization:
from scgo import run_go_campaign
from scgo.param_presets import get_testing_params
from scgo.runner_api import build_one_element_compositions
# Pt2, Pt3, Pt4, Pt5, Pt6
compositions = build_one_element_compositions("Pt", min_atoms=2, max_atoms=6)
results = run_go_campaign(
compositions,
params=get_testing_params(),
seed=42,
system_type="gas_cluster",
)
# results is dict[path_key, list[(energy, Atoms)]]
# (for gas_cluster, path_key matches the formula, e.g. "Pt5")
Failed compositions are logged, recorded as empty lists under their path_key,
and skipped so the rest of the campaign continues. See Cluster initialization
for multi-element atom ordering and placement.
Binary compositions:
from scgo import run_go_campaign
from scgo.param_presets import get_testing_params
from scgo.runner_api import build_two_element_compositions
# All Au/Pt combinations with 2-4 total atoms
compositions = build_two_element_compositions("Au", "Pt", min_atoms=2, max_atoms=4)
results = run_go_campaign(
compositions,
params=get_testing_params(),
seed=42,
system_type="gas_cluster",
)
TS from existing minima (each composition needs a prior {path_key}_searches/ tree):
from scgo import run_ts_campaign
from scgo.param_presets import get_ts_search_params
from scgo.runner_api import build_one_element_compositions
compositions = build_one_element_compositions("Pt", min_atoms=4, max_atoms=6)
results = run_ts_campaign(
compositions,
ts_params=get_ts_search_params(system_type="gas_cluster", seed=42),
seed=42,
output_dir="benchmark/results", # shared campaign root
system_type="gas_cluster",
)
GO + TS for multiple compositions:
from scgo import run_go_ts_campaign
from scgo.param_presets import get_testing_params, get_ts_search_params
from scgo.runner_api import build_one_element_compositions
compositions = build_one_element_compositions("Pt", min_atoms=4, max_atoms=5)
results = run_go_ts_campaign(
compositions,
go_params=get_testing_params(),
ts_params=get_ts_search_params(system_type="gas_cluster", seed=42),
seed=42,
output_dir="benchmark/results",
system_type="gas_cluster",
)
See API Reference for full signatures.
Output¶
See Output Layout for directory structure, path keys, run IDs, and file
formats. In brief: run_go writes {path_key}_searches/ with run_*/
subdirectories, and GO+TS creates a sibling {path_key}_ts_results/ tree.
Parameters¶
Quick parameter selection:
Preset |
Use for |
|---|---|
|
Fast tests (EMT calculator) |
|
Default production (MACE) |
|
GPU-accelerated with TorchSim |
|
Transition state search |
See All Parameters for all options and Parameter Presets for details.
Examples¶
Working examples live in the repository. See
examples/README.md
for the full list and usage notes. Each example enables write_timing_json
so per-run timing.json and campaign go_ts_timing.json are written.