Quick Start

SCGO helps you find the lowest-energy atomic structures using global optimization. This guide shows how to use all supported workflows.

System Types

You must specify one of four system types:

Type

Use when

gas_cluster

Optimizing a cluster in vacuum

surface_cluster

Optimizing a cluster on a surface

gas_cluster_adsorbate

Optimizing a cluster with adsorbates in vacuum

surface_cluster_adsorbate

Optimizing a cluster with adsorbates on a surface

For surface types, you need a surface_config. For adsorbate types, you need adsorbates.

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 (bimetallics, oxides), atom order follows the composition list you pass in so GA crossover can pair structures safely. Initialization favours placing heavier elements first while keeping diversity across the population. 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 relax

  • n_fix_bottom_slab_layers=1: Freeze bottom layer only

  • Both 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, etc.) 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 GA 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 validate 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,
)

Transition States

Find transition states between optimized structures.

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["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,
)

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[formula, list[(energy, Atoms)]]

Failed compositions (e.g. initialization ValueError on extreme stoichiometries) are logged, recorded as empty lists in the returned dict, and skipped so the rest of the campaign continues. See Cluster initialization for multi-element atom ordering and placement behaviour.

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 formula needs a prior {formula}_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 directories

output_dir means different things depending on the runner. See also API Reference.

Runner

output_dir is

Default when omitted

run_go

The {formula}_searches/ directory itself

{formula}_searches/ in the current working directory

run_go_campaign

Campaign parent; each composition → {parent}/{formula}_searches/

Each composition → {formula}_searches/ in CWD (no shared parent)

run_go_ts

Campaign root → {root}/{formula}_searches/ and {root}/{formula}_ts_results/

scgo_runs/{formula}_{calculator_slug}/ (see output_root / output_stem below)

run_go_ts_campaign

Campaign parent; each composition → {parent}/{formula}_campaign/…

scgo_runs/go_ts_campaign_{calc}/

run_ts_search

Campaign root (or an existing *_searches/ path — parent is inferred)

CWD; minima from {formula}_searches/, TS to {formula}_ts_results/

run_ts_campaign

Shared campaign root for all compositions

CWD per composition

output_root and output_stem (run_go_ts / run_go_ts_campaign only): when output_dir is omitted, the default root is {output_root or ./scgo_runs}/{output_stem or formula}_{calculator_slug}/ (for example examples/results/pt5_gas_mace/).

searches_dir (run_ts_search only): explicit path to a GO searches directory; the campaign root becomes searches_dir.parent.

Example — ``run_go_ts`` with ``output_root`` / ``output_stem``:

GO and TS use the same campaign layout: {formula}_searches/ and {formula}_ts_results/ are siblings; each holds run_* directories, campaign-level summaries, and deduplicated exports. GO databases and TS pair artifacts live directly under each run_* (TS uses pair_<i>_<j>/ subdirs).

results/pt5_gas_mace/
├── Pt5_searches/
│   ├── run_20260703_120000_123456/
│   │   ├── metadata.json
│   │   ├── timing.json              # optional (write_timing_json=True)
│   │   └── ga_go.db
│   ├── results_summary.json
│   └── final_unique_minima/
└── Pt5_ts_results/
    ├── run_20260703_130000_654321/
    │   ├── metadata.json
    │   ├── timing.json              # optional (write_timing_json=True)
    │   └── pair_0_1/
    │       └── neb_0_1_metadata.json
    ├── results_summary.json
    ├── ts_network_metadata.json
    └── final_unique_ts/
        └── final_unique_ts_summary.json

Example — ``run_go_campaign`` with ``output_dir=”benchmark/results”``:

benchmark/results/
├── Pt4_searches/
├── Pt5_searches/
└── Pt6_searches/

Example — gas-phase MLIP benchmark default (benchmark_Pt.py):

benchmark/results/
└── pt5_mace_mace_matpes_0/
    └── Pt5_searches/
        └── run_<timestamp>_<microseconds>/

Output Files

Global optimization and transition-state search use analogous directory hierarchies under sibling {formula}_searches/ and {formula}_ts_results/ trees (location depends on the runner — see Output directories above). Each tree has:

  • One run_<timestamp>_<microseconds>/ directory per invocation

  • Campaign-level results_summary.json and a deduplicated export directory

  • Per-run metadata.json, optional timing.json, and optimizer DB at the run root

  • TS pair work units under pair_<i>_<j>/ inside each run

Global optimization ({formula}_searches/):

Pt5_searches/
├── run_<timestamp>_<microseconds>/
│   ├── metadata.json
│   ├── timing.json                 # optional (write_timing_json=True)
│   └── ga_go.db                    # or bh_go.db / simple_go.db
├── results_summary.json
└── final_unique_minima/            # XYZ files of best structures

Each invocation creates one datetime-tagged run_* directory. Repeat run_go (or a campaign loop) for additional independent runs; results merge across runs via database discovery and deduplication.

Run-level ``timing.json`` — when write_timing_json=True, a flat payload is written at {run_dir}/timing.json. See scgo.utils.timing_report.

Transition state search ({formula}_ts_results/):

Pt5_ts_results/
├── run_<timestamp>_<microseconds>/
│   ├── metadata.json
│   ├── timing.json                 # optional (write_timing_json=True)
│   └── pair_<i>_<j>/
│       └── neb_{i}_{j}_metadata.json   # + trajectory, TS/endpoints
├── results_summary.json
├── ts_network_metadata.json        # connectivity graph between minima
└── final_unique_ts/
    └── final_unique_ts_summary.json

Run IDs and provenance

Each GO or TS invocation creates a datetime-tagged directory named run_YYYYMMDD_HHMMSS_ffffff (microsecond granularity). This tag is the primary key for tracing artifacts across combined campaigns:

  • ``run_go`` / ``run_trials`` — one new run_* per invocation under {formula}_searches/.

  • ``run_go_campaign`` — a single shared run_id for all compositions in the campaign (generated once at campaign start unless you pass run_id=).

  • ``run_ts_search`` / ``run_go_ts`` — TS always mints a fresh run_* under {formula}_ts_results/ (independent of any GO run_id).

Combining multiple runs: repeat run_go (or a campaign loop) to add sibling run_* directories. SCGO discovers prior databases, merges minima, deduplicates, and writes final_unique_minima/ exports. Only directories matching the datetime run_* pattern are listed by scgo.utils.run_tracking.get_run_directories(); custom run_id strings still work at runtime but are skipped by that helper.

TS minima provenance: each NEB pair records endpoint lineage in minima_provenance inside results_summary.json, neb_{i}_{j}_metadata.json, and ts_network_metadata.json. Each entry links back to the GO minimum via:

Field

Meaning

run_id

GO run that produced the endpoint minimum

source_db / source_db_relpath

Optimizer database path (basename and campaign-relative path)

confid / gaid / systems_row_id

Row identifiers inside the GO database

unique_id / final_id

Dedup and final-export identifiers when present

energy

Endpoint energy at pairing time (eV)

Timing artifacts:

  • Per GO/TS run: {run_dir}/timing.json when write_timing_json=True.

  • GO+TS pipeline rollup: go_ts_timing.json at the campaign root when timing JSON is enabled in go_params and/or ts_params (see scgo.utils.timing_report).

Reading prior results

To reload minima from completed searches without re-running GO:

from scgo import load_previous_run_results, SCGODatabaseManager

minima = load_previous_run_results("Pt5_searches")
# Or browse databases with context-manager cleanup:
with SCGODatabaseManager(base_dir="Pt5_searches") as manager:
    refs = manager.load_reference_structures("**/*.db", composition=["Pt"] * 5)

See scgo.database for HPC-oriented database access patterns.

Parameters

Quick parameter selection:

Preset

Use for

get_testing_params()

Fast tests (EMT calculator)

get_default_params()

Default production (MACE)

get_torchsim_ga_params(...)

GPU-accelerated with TorchSim

get_ts_search_params(...)

Transition state search

See All Parameters for all options and Parameter Presets for details.

Examples

Working examples in the repository:

  • examples/example_pt5_gas.py: Pt5 in gas phase

  • examples/example_pt5_graphite.py: Pt5 on graphite

  • examples/example_pt5_oh_gas.py: Pt5 + OH in gas phase

  • examples/example_pt5_2oh_graphite.py: Pt5 + 2OH on graphite