Source code for scgo.runner_params

"""Param merging, allowlists, coherence checks, and run-context dataclasses.

These helpers resolve/merge ``go_params`` / ``ts_params`` against preset
defaults, validate consistency between run-level ``system_type`` /
``surface_config`` and the params dicts, and build the frozen context
dataclasses consumed by :mod:`scgo.runner_api`'s public run functions.
"""

from __future__ import annotations

import copy
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from ase import Atoms
from ase.calculators.calculator import Calculator

from scgo.exceptions import SCGOValidationError
from scgo.param_presets import get_default_params
from scgo.runner_composition import (
    CompositionInput,
    _as_composition,
    _as_composition_list,
)
from scgo.runner_go import select_scgo_minima_algorithm
from scgo.surface.config import SurfaceSystemConfig
from scgo.system_types import (
    AdsorbateDefinition,
    AdsorbatesInput,
    SystemType,
    extract_adsorbate_definition_from_params,
    get_system_policy,
    resolve_adsorbate_run_composition,
    validate_system_type_settings,
)
from scgo.utils.helpers import get_cluster_formula
from scgo.utils.logging import get_logger
from scgo.utils.output_paths import resolve_go_searches_dir
from scgo.utils.run_helpers import initialize_params, initialize_ts_params
from scgo.utils.ts_runner_kwargs import coerce_ts_params_to_runner_kwargs

_ALGO_KEYS = ("simple", "bh", "ga")
_LOGGER = get_logger(__name__)
_VALIDATION_LOGGER = get_logger("scgo.validation")
_DEFAULT_GO_PARAMS: dict[str, Any] | None = None


def _log_validation_error(exc: SCGOValidationError) -> None:
    """Emit user-facing ERROR for validation failures at the runner API boundary."""
    _VALIDATION_LOGGER.error("Validation error: %s", exc)


@dataclass(frozen=True)
class RunGOContext:
    composition: list[str]
    system_type: SystemType
    params: dict[str, Any]
    seed: int | None
    run_id: str | None
    clean: bool
    output_dir: Path | None
    verbosity: int
    calculator_for_global_optimization: Calculator | None
    output_summary_dir: str


@dataclass(frozen=True)
class RunGOCampaignContext:
    compositions: list[list[str]]
    system_type: SystemType
    params: dict[str, Any]
    seed: int | None
    run_id: str | None
    clean: bool
    output_dir: Path | None
    verbosity: int
    output_summary_dir: str


@dataclass(frozen=True)
class RunGOTSContext:
    composition: list[str]
    system_type: SystemType
    go_params: dict[str, Any]
    ts_kwargs: dict[str, Any]
    seed: int | None
    verbosity: int
    output_dir: Path
    adsorbate_definition: AdsorbateDefinition | None


@dataclass(frozen=True)
class RunTSContext:
    composition: list[str]
    system_type: SystemType
    ts_params: dict[str, Any]
    ts_base: dict[str, Any]
    ts_kwargs: dict[str, Any]
    seed: int | None
    verbosity: int
    output_dir: Path | None
    searches_dir: Path | None
    adsorbate_definition: AdsorbateDefinition | None


def _optimizer_write_timing_json_enabled(params: dict[str, Any]) -> bool:
    """Return True when any GO optimizer slot requests ``write_timing_json``."""
    opt = params.get("optimizer_params") or {}
    for algo in _ALGO_KEYS:
        slot = opt.get(algo)
        if isinstance(slot, dict) and slot.get("write_timing_json"):
            return True
    return False


def _default_optimizer_system_type(algo: str) -> SystemType | None:
    global _DEFAULT_GO_PARAMS
    if _DEFAULT_GO_PARAMS is None:
        _DEFAULT_GO_PARAMS = get_default_params()
    slot = _DEFAULT_GO_PARAMS.get("optimizer_params", {}).get(algo, {})
    if isinstance(slot, dict):
        return slot.get("system_type")
    return None


def _resolved_path(path: str | Path | None) -> Path | None:
    return Path(path).expanduser().resolve() if path is not None else None


def _require_system_type(system_type: SystemType | None, fn_name: str) -> SystemType:
    if system_type is None:
        raise SCGOValidationError(f"system_type is required for {fn_name}.")
    return system_type


def _prepare_run_context(
    composition: CompositionInput,
    *,
    system_type: SystemType | None,
    surface_config: SurfaceSystemConfig | None,
    params: dict[str, Any] | None,
    adsorbates: AdsorbatesInput | None,
    context: str,
) -> tuple[
    SystemType,
    dict[str, Any] | None,
    AdsorbateDefinition | None,
    Atoms | None,
    list[str],
]:
    st = _require_system_type(system_type, context)
    validate_system_type_settings(system_type=st, surface_config=surface_config)
    if params is not None:
        _reject_system_keys(params, context=context, kind="go")
    comp = _as_composition(composition)
    preset_ads = (
        extract_adsorbate_definition_from_params(params)
        if adsorbates is None and params is not None
        else None
    )
    ads_def, ads_template, full_comp = resolve_adsorbate_run_composition(
        system_type=st,
        composition=comp,
        adsorbates=adsorbates,
        preset_adsorbate_definition=preset_ads,
        context=context,
    )
    params_prep = params or {}
    if params:
        params_prep = _with_surface_in_optimizers(params, surface_config=surface_config)
    if params_prep is not None:
        params_prep = _with_adsorbate_in_optimizers(
            params_prep,
            adsorbate_definition=ads_def,
            adsorbate_fragment_template=ads_template,
        )
    return st, params_prep, ads_def, ads_template, full_comp


def _validate_go_ts_surface_config(
    go_prepared: dict[str, Any],
    *,
    system_type: SystemType,
    surface_config: SurfaceSystemConfig | None,
    adsorbate_composition: list[str],
) -> None:
    """For surface system types, ensure active GO slot does not conflict."""
    if not get_system_policy(system_type).uses_surface:
        return
    if not isinstance(surface_config, SurfaceSystemConfig):
        raise SCGOValidationError(
            f"system_type={system_type!r} requires the run surface_config argument "
            "to be a SurfaceSystemConfig."
        )
    chosen = select_scgo_minima_algorithm(len(adsorbate_composition), system_type)
    op = go_prepared.get("optimizer_params") or {}
    go_slot = op.get(chosen)
    if not isinstance(go_slot, dict):
        go_slot = {}
    go_sc = go_slot.get("surface_config")
    if go_sc is not None and go_sc != surface_config:
        raise SCGOValidationError(
            "run surface_config and go_params['optimizer_params']["
            f"'{chosen}']['surface_config'] disagree."
        )


def _validate_go_ts_param_coherence(
    *,
    go_prepared: dict[str, Any],
    ts_params: dict[str, Any],
    system_type: SystemType,
    surface_config: SurfaceSystemConfig | None,
) -> None:
    """Validate GO/TS params coherence against run-level system definition."""
    policy = get_system_policy(system_type)
    go_surface_config = go_prepared.get("surface_config") or surface_config
    if policy.uses_surface:
        if not isinstance(go_surface_config, SurfaceSystemConfig):
            raise SCGOValidationError(
                "GO/TS coherence error: surface system types require "
                "go_params['surface_config'] or run surface_config=."
            )
        if (
            surface_config is not None
            and go_prepared.get("surface_config") is not None
            and go_prepared.get("surface_config") != surface_config
        ):
            raise SCGOValidationError(
                "GO/TS coherence error: go_params['surface_config'] disagrees with "
                "run surface_config."
            )
    elif go_surface_config is not None:
        raise SCGOValidationError(
            "GO/TS coherence error: go_params['surface_config'] is set but "
            f"run system_type={system_type!r} is non-surface."
        )

    optimizer_params = go_prepared.get("optimizer_params") or {}
    for algo in _ALGO_KEYS:
        slot = optimizer_params.get(algo)
        if slot is None:
            continue
        if not isinstance(slot, dict):
            raise SCGOValidationError(
                f"go_params['optimizer_params']['{algo}'] must be a dict."
            )
        slot_system_type = slot.get("system_type")
        default_slot_st = _default_optimizer_system_type(algo)
        if (
            slot_system_type is not None
            and slot_system_type != system_type
            and slot_system_type != default_slot_st
        ):
            raise SCGOValidationError(
                "GO/TS coherence error: "
                f"go_params['optimizer_params']['{algo}']['system_type']="
                f"{slot_system_type!r} disagrees with run system_type={system_type!r}."
            )
        slot_surface_config = slot.get("surface_config")
        if policy.uses_surface:
            if (
                slot_surface_config is not None
                and surface_config is not None
                and slot_surface_config != surface_config
            ):
                raise SCGOValidationError(
                    "GO/TS coherence error: "
                    f"go_params['optimizer_params']['{algo}']['surface_config'] "
                    "disagrees with run surface_config."
                )
        elif slot_surface_config is not None:
            raise SCGOValidationError(
                "GO/TS coherence error: go_params surface_config is set but "
                f"run system_type={system_type!r} is non-surface."
            )

    ts_surface_config = ts_params.get("surface_config") or surface_config
    if policy.uses_surface:
        if not isinstance(ts_surface_config, SurfaceSystemConfig):
            raise SCGOValidationError(
                "GO/TS coherence error: surface system types require "
                "ts_params['surface_config'] or run surface_config=."
            )
        if (
            surface_config is not None
            and ts_params.get("surface_config") is not None
            and ts_params.get("surface_config") != surface_config
        ):
            raise SCGOValidationError(
                "GO/TS coherence error: ts_params['surface_config'] disagrees with "
                "run surface_config."
            )
    elif ts_surface_config is not None:
        raise SCGOValidationError(
            "GO/TS coherence error: ts_params['surface_config'] is set but "
            f"run system_type={system_type!r} is non-surface."
        )


def _merge_adsorbate_context_into_params(
    base: dict[str, Any] | None,
    **kwargs: Any,
) -> dict[str, Any]:
    """Attach adsorbate/surface init context for :func:`_run_go_trials` / GA."""
    out = copy.deepcopy(base) if base is not None else {}
    out.update({k: v for k, v in kwargs.items() if v is not None})
    return out


def _with_system_type_in_optimizer_params(
    params: dict[str, Any] | None,
    *,
    system_type: SystemType,
) -> dict[str, Any]:
    """Attach ``system_type`` (and fan-out ``surface_config``) to optimizer slots."""
    out = copy.deepcopy(params or {})
    op = out.setdefault("optimizer_params", {})
    for algo in _ALGO_KEYS:
        cfg = op.setdefault(algo, {})
        cfg["system_type"] = system_type
    # Add surface_config to all optimizer slots if it's in params
    if "surface_config" in out:
        for algo in _ALGO_KEYS:
            op.setdefault(algo, {})["surface_config"] = out["surface_config"]
    return out


def _coerce_ts_for_runner(
    ts_params: dict[str, Any] | None,
    *,
    fn_name: str,
    system_type: SystemType,
    surface_config: SurfaceSystemConfig | None,
) -> dict[str, Any]:
    if not ts_params:
        raise SCGOValidationError(
            f"ts_params is required for {fn_name}. Build with get_ts_search_params(...)."
        )
    _reject_system_keys(ts_params, context=fn_name, kind="ts")
    return coerce_ts_params_to_runner_kwargs(
        ts_params, system_type=system_type, surface_config=surface_config
    )


def _resolve_go_params(
    go_params: dict[str, Any] | None,
    *,
    surface_config: SurfaceSystemConfig | None = None,
) -> dict[str, Any]:
    """Merge GO params with defaults and inject run-level ``surface_config`` when missing."""
    merged = initialize_params(go_params)
    if surface_config is not None and merged.get("surface_config") is None:
        merged = copy.deepcopy(merged)
        merged["surface_config"] = surface_config
    return merged


def _resolve_ts_params(
    ts_params: dict[str, Any] | None,
    *,
    system_type: SystemType,
    surface_config: SurfaceSystemConfig | None = None,
    go_params: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Merge TS params with defaults; align calculator with merged GO when provided."""
    merged_go = initialize_params(go_params) if go_params is not None else None
    merged = initialize_ts_params(
        ts_params,
        system_type=system_type,
        surface_config=surface_config,
        go_params=merged_go,
    )
    if surface_config is not None and merged.get("surface_config") is None:
        merged = copy.deepcopy(merged)
        merged["surface_config"] = surface_config
    return merged


def _resolve_go_ts_params(
    *,
    system_type: SystemType,
    surface_config: SurfaceSystemConfig | None,
    go_params: dict[str, Any] | None,
    ts_params: dict[str, Any] | None,
) -> tuple[dict[str, Any], dict[str, Any]]:
    """Return merged GO and TS param dicts using canonical preset defaults."""
    effective_go = _resolve_go_params(go_params, surface_config=surface_config)
    effective_ts = _resolve_ts_params(
        ts_params,
        system_type=system_type,
        surface_config=surface_config,
        go_params=effective_go,
    )
    return effective_go, effective_ts


def _calculator_slug_from_go_params(go_params: dict[str, Any] | None) -> str:
    c = str((go_params or {}).get("calculator", "MACE")).strip().upper()
    if c in ("MACE", "UMA"):
        return c.lower()
    return c.lower() or "calc"


def _default_go_ts_output_path(
    composition: list[str],
    *,
    go_params: dict[str, Any],
    output_stem: str | None,
    output_root: str | Path | None,
) -> Path:
    root = output_root if output_root is not None else Path.cwd() / "scgo_runs"
    p = Path(root).expanduser().resolve()
    stem = output_stem or get_cluster_formula(composition)
    path = (p / f"{stem}_{_calculator_slug_from_go_params(go_params)}").resolve()
    if output_root is None:
        _LOGGER.info("No output_dir provided; using default campaign root %s", path)
    return path


def _log_completion(kind: str, *, elapsed_s: float, details: str) -> None:
    _LOGGER.info("%s completed in %.2f s (%s)", kind, elapsed_s, details)


def _as_int_seed(label: str, value: Any) -> int:
    try:
        return int(value)
    except (TypeError, ValueError) as e:
        raise SCGOValidationError(f"{label} must be int-like, got {value!r}") from e


[docs] def resolve_workflow_seed( *, seed_kw: int | None = None, go_params: dict[str, Any] | None = None, ts_params: dict[str, Any] | None = None, ) -> int | None: """Unify run ``seed=...``, ``go_params['seed']``, and ``ts_params['seed']``; all non-null must agree.""" parts: list[tuple[str, int]] = [] if seed_kw is not None: parts.append(("run_kwd(seed=...)", _as_int_seed("run seed", seed_kw))) if go_params is not None and go_params.get("seed") is not None: parts.append( ( "go_params['seed']", _as_int_seed("go_params['seed']", go_params.get("seed")), ) ) if ts_params is not None and ts_params.get("seed") is not None: parts.append( ( "ts_params['seed']", _as_int_seed("ts_params['seed']", ts_params.get("seed")), ) ) if not parts: return None values = {v for _, v in parts} if len(values) > 1: desc = ", ".join(f"{name}={v}" for name, v in parts) raise SCGOValidationError(f"Inconsistent random seeds: {desc}") return next(iter(values))
def _with_surface_in_optimizers( go_params: dict[str, Any], *, surface_config: SurfaceSystemConfig | None ) -> dict[str, Any]: """Copy ``go_params``; fan out explicit run ``surface_config`` to optimizer slots.""" out = copy.deepcopy(go_params) if surface_config is not None: if out.get("surface_config") is None: out["surface_config"] = surface_config elif out.get("surface_config") != surface_config: raise SCGOValidationError( "run argument surface_config must match go_params['surface_config'] " "when both are set." ) op = out.setdefault("optimizer_params", {}) for key in _ALGO_KEYS: if key not in op: continue slot = op[key] if not isinstance(slot, dict): raise SCGOValidationError( f"optimizer_params['{key}'] must be a dict when using go_params['surface_config']" ) ex = slot.get("surface_config") if ex is None: slot["surface_config"] = surface_config elif ex != surface_config: raise SCGOValidationError( f"run argument surface_config must match " f"go_params['optimizer_params']['{key}']['surface_config'] when both are set." ) return out def _with_adsorbate_in_optimizers( go_params: dict[str, Any] | None, *, adsorbate_definition: Any | None = None, adsorbate_fragment_template: Any | None = None, ) -> dict[str, Any]: """Copy ``go_params``; fan out derived adsorbate context to optimizer slots.""" out = copy.deepcopy(go_params) if go_params is not None else {} cluster_adsorbate_config = out.get("cluster_adsorbate_config") # If any adsorbate param is set, distribute to all optimizer slots if ( adsorbate_definition is not None or adsorbate_fragment_template is not None or cluster_adsorbate_config is not None ): op = out.setdefault("optimizer_params", {}) for key in _ALGO_KEYS: slot = op.setdefault(key, {}) if not isinstance(slot, dict): raise SCGOValidationError( f"optimizer_params['{key}'] must be a dict when using adsorbate parameters" ) if adsorbate_definition is not None: ex = slot.get("adsorbate_definition") if ex is None: slot["adsorbate_definition"] = adsorbate_definition # Don't check for match, multiple definitions may be equivalent if adsorbate_fragment_template is not None: ex = slot.get("adsorbate_fragment_template") if ex is None: slot["adsorbate_fragment_template"] = adsorbate_fragment_template if cluster_adsorbate_config is not None: ex = slot.get("cluster_adsorbate_config") if ex is None: slot["cluster_adsorbate_config"] = cluster_adsorbate_config return out def _reject_system_keys( params: dict[str, Any], *, context: str, kind: str = "go" ) -> None: """Reject ``system_type`` in go/ts params (use the run-function argument). Top-level ``surface_config`` in params is allowed and fanned into optimizer slots; only ``system_type`` remains forbidden here. """ if params.get("system_type") is not None: guidance = "Use the run function argument instead." if kind == "ts": guidance = ( "Use the run function system_type argument; " "ts_params['surface_config'] is allowed." ) raise SCGOValidationError( f"{context} does not allow top-level {kind}_params['system_type']. " f"{guidance}" ) def _prepare_run_go_context( composition: CompositionInput, *, params: dict[str, Any] | None, seed: int | None, verbosity: int, run_id: str | None, clean: bool, output_dir: str | Path | None, calculator_for_global_optimization: Calculator | None, surface_config: SurfaceSystemConfig | None, system_type: SystemType | None, adsorbates: AdsorbatesInput | None, ) -> RunGOContext: st, params_prep, ads_def, ads_temp, comp = _prepare_run_context( composition, system_type=system_type, surface_config=surface_config, params=params, adsorbates=adsorbates, context="run_go", ) eff_seed = resolve_workflow_seed(seed_kw=seed, go_params=params) eff_params = _with_system_type_in_optimizer_params(params_prep, system_type=st) eff_params = _merge_adsorbate_context_into_params( eff_params, adsorbate_definition=ads_def, adsorbate_fragment_template=ads_temp, ) out_path = _resolved_path(output_dir) searches_dir = str(resolve_go_searches_dir(output_dir, get_cluster_formula(comp))) return RunGOContext( composition=comp, system_type=st, params=eff_params, seed=eff_seed, run_id=run_id, clean=clean, output_dir=out_path, verbosity=verbosity, calculator_for_global_optimization=calculator_for_global_optimization, output_summary_dir=searches_dir, ) def _prepare_run_go_campaign_context( compositions: Iterable[CompositionInput], *, params: dict[str, Any] | None, seed: int | None, verbosity: int, run_id: str | None, clean: bool, output_dir: str | Path | None, surface_config: SurfaceSystemConfig | None, system_type: SystemType | None, adsorbates: AdsorbatesInput | None, ) -> RunGOCampaignContext: st = _require_system_type(system_type, "run_go_campaign") validate_system_type_settings(system_type=st, surface_config=surface_config) if params is not None: _reject_system_keys(params, context="run_go_campaign") params_prep = ( _with_surface_in_optimizers(params, surface_config=surface_config) if params else None ) eff_seed = resolve_workflow_seed(seed_kw=seed, go_params=params) eff_params = _with_system_type_in_optimizer_params(params_prep, system_type=st) preset_ads_def = ( extract_adsorbate_definition_from_params(eff_params) if adsorbates is None else None ) full_compositions: list[list[str]] = [] ads_def: AdsorbateDefinition | None = None ads_temp: list[Atoms] | Atoms | None = None for composition_item in _as_composition_list(compositions): comp = _as_composition(composition_item) ads_def, ads_temp, full_comp = resolve_adsorbate_run_composition( system_type=st, composition=comp, adsorbates=adsorbates, preset_adsorbate_definition=preset_ads_def, context="run_go_campaign", ) full_compositions.append(full_comp) if ads_def is not None: eff_params["adsorbate_definition"] = ads_def if ads_temp is not None: eff_params["adsorbate_fragment_template"] = ads_temp if adsorbates is not None or preset_ads_def is not None: eff_params = _with_adsorbate_in_optimizers( eff_params, adsorbate_definition=eff_params.get("adsorbate_definition"), adsorbate_fragment_template=eff_params.get("adsorbate_fragment_template"), ) out_path = _resolved_path(output_dir) campaign_root = ( str(Path(out_path).expanduser().resolve()) if out_path is not None else str( resolve_go_searches_dir( None, get_cluster_formula(full_compositions[0]) ).parent ) ) return RunGOCampaignContext( compositions=full_compositions, system_type=st, params=eff_params, seed=eff_seed, run_id=run_id, clean=clean, output_dir=out_path, verbosity=verbosity, output_summary_dir=campaign_root, ) def _prepare_run_go_ts_context( composition: CompositionInput, *, go_params: dict[str, Any] | None, ts_params: dict[str, Any] | None, seed: int | None, verbosity: int, output_dir: str | Path | None, output_root: str | Path | None, output_stem: str | None, surface_config: SurfaceSystemConfig | None, system_type: SystemType | None, adsorbates: AdsorbatesInput | None, ) -> RunGOTSContext: context_name = "run_go_ts" st = _require_system_type(system_type, context_name) validate_system_type_settings(system_type=st, surface_config=surface_config) if go_params is not None: _reject_system_keys(go_params, context=context_name) if ts_params is not None: _reject_system_keys(ts_params, context=context_name, kind="ts") go_mat, ts_mat = _resolve_go_ts_params( system_type=st, surface_config=surface_config, go_params=go_params, ts_params=ts_params, ) eff_seed = resolve_workflow_seed(seed_kw=seed, go_params=go_mat, ts_params=ts_mat) go_prep = _with_surface_in_optimizers(go_mat, surface_config=surface_config) core_comp = _as_composition(composition) preset_ads = ( extract_adsorbate_definition_from_params(go_mat) if adsorbates is None else None ) ads_def, ads_temp, comp = resolve_adsorbate_run_composition( system_type=st, composition=core_comp, adsorbates=adsorbates, preset_adsorbate_definition=preset_ads, context=context_name, ) _validate_go_ts_param_coherence( go_prepared=go_prep, ts_params=ts_mat, system_type=st, surface_config=surface_config, ) _validate_go_ts_surface_config( go_prep, system_type=st, surface_config=surface_config, adsorbate_composition=comp, ) go_prep = _with_system_type_in_optimizer_params(go_prep, system_type=st) go_local = _merge_adsorbate_context_into_params( go_prep, adsorbate_definition=ads_def, adsorbate_fragment_template=ads_temp, ) ts_kwargs = _coerce_ts_for_runner( ts_mat, fn_name=context_name, system_type=st, surface_config=surface_config ) out_path = _resolved_path(output_dir) or _default_go_ts_output_path( comp, go_params=go_mat, output_stem=output_stem, output_root=output_root ) return RunGOTSContext( composition=comp, system_type=st, go_params=go_local, ts_kwargs=ts_kwargs, seed=eff_seed, verbosity=verbosity, output_dir=out_path, adsorbate_definition=ads_def, ) def _prepare_run_ts_search_context( composition: CompositionInput, *, ts_params: dict[str, Any] | None, output_dir: str | Path | None, searches_dir: str | Path | None, seed: int | None, verbosity: int, surface_config: SurfaceSystemConfig | None, system_type: SystemType | None, adsorbates: AdsorbatesInput | None, ) -> RunTSContext: context_name = "run_ts_search" st, _, ads_def, _, comp = _prepare_run_context( composition, system_type=system_type, surface_config=surface_config, params=None, adsorbates=adsorbates, context=context_name, ) if ts_params is not None: _reject_system_keys(ts_params, context=context_name, kind="ts") ts_mat = _resolve_ts_params( ts_params, system_type=st, surface_config=surface_config ) ts_base = initialize_ts_params(None, system_type=st, surface_config=surface_config) eff_seed = resolve_workflow_seed(seed_kw=seed, ts_params=ts_mat) ts_kwargs = _coerce_ts_for_runner( ts_mat, fn_name=context_name, system_type=st, surface_config=surface_config ) ts_kwargs.pop("system_type", None) return RunTSContext( composition=comp, system_type=st, ts_params=ts_mat, ts_base=ts_base, ts_kwargs=ts_kwargs, seed=eff_seed, verbosity=verbosity, output_dir=_resolved_path(output_dir), searches_dir=_resolved_path(searches_dir), adsorbate_definition=ads_def, )