Source code for scgo.cluster_adsorbate.reposition
"""Re-place adsorbate fragments on fresh core-hull sites during GA evolution."""
from __future__ import annotations
import numpy as np
from ase import Atoms
from ase_ga.offspring_creator import OffspringCreator
from ase_ga.utilities import atoms_too_close_two_sets
from numpy.random import Generator
from scgo.ase_ga_patches.standardmutations import _ensure_rng
from scgo.cluster_adsorbate.config import ClusterAdsorbateConfig
from scgo.cluster_adsorbate.helpers import (
parse_positive_fragment_lengths,
resolve_fragment_anchor_and_bond_axis,
)
from scgo.cluster_adsorbate.placement import place_fragment_on_cluster
from scgo.system_types import (
AdsorbateDefinition,
AdsorbateFragmentInput,
SystemType,
get_system_policy,
resolve_adsorbate_fragments,
)
[docs]
class FragmentRepositionMutation(OffspringCreator):
"""Rigidly re-place one adsorbate fragment on a new core surface site.
Core atoms (tag 0) define the convex-hull site pool; other adsorbate
fragments remain fixed as clash obstacles. Preserves fragment internal
geometry by using the current fragment pose (or an input template) as
the rigid body.
"""
def __init__(
self,
blmin: dict,
n_top: int,
system_type: SystemType,
adsorbate_definition: AdsorbateDefinition,
fragment_templates: AdsorbateFragmentInput | None = None,
cluster_adsorbate_config: ClusterAdsorbateConfig | None = None,
*,
rng: Generator | None = None,
verbose: bool = False,
) -> None:
rng = _ensure_rng(rng)
OffspringCreator.__init__(self, verbose, rng=rng)
self.blmin = blmin
self.n_top = n_top
self.system_type = system_type
self._policy = get_system_policy(system_type)
self.adsorbate_definition = adsorbate_definition
self.fragment_templates = fragment_templates
self.cluster_adsorbate_config = cluster_adsorbate_config
self.descriptor = "FragmentRepositionMutation"
self.min_inputs = 1
[docs]
def get_new_individual(self, parents):
parent = parents[0]
mutant = self.mutate(parent)
if mutant is None:
return mutant, "mutation: fragment_reposition"
mutant = self.initialize_individual(parent, mutant)
mutant.info["data"]["parents"] = [parent.info.get("confid")]
return self.finalize_individual(mutant), "mutation: fragment_reposition"
def _fragment_template_for_tag(
self, mobile: Atoms, tag: int, frag_index: int
) -> Atoms:
if self.fragment_templates is not None:
fragments = resolve_adsorbate_fragments(
self.fragment_templates,
self.adsorbate_definition,
context="FragmentRepositionMutation",
)
if 0 <= frag_index < len(fragments):
return fragments[frag_index].copy()
mask = mobile.get_tags() == tag
return mobile[mask].copy()
[docs]
def mutate(self, atoms: Atoms) -> Atoms | None:
n_top = int(self.n_top)
slab = atoms[: len(atoms) - n_top]
mobile = atoms[-n_top:].copy()
tags = mobile.get_tags()
ads_tags = sorted({int(t) for t in tags if int(t) > 0})
if not ads_tags:
return None
lengths = parse_positive_fragment_lengths(
self.adsorbate_definition.get("adsorbate_fragment_lengths", [])
)
target_tag = int(self.rng.choice(ads_tags))
frag_index = target_tag - 1
if lengths and not (0 <= frag_index < len(lengths)):
return None
core_mask = tags == 0
ads_mask = tags == target_tag
if not np.any(core_mask) or not np.any(ads_mask):
return None
metal_core = mobile[core_mask]
clash_mobile = mobile[~ads_mask]
ca = self.cluster_adsorbate_config or ClusterAdsorbateConfig()
anchor, bond_axis = resolve_fragment_anchor_and_bond_axis(
self.adsorbate_definition
)
fragment_tmpl = self._fragment_template_for_tag(mobile, target_tag, frag_index)
placed = place_fragment_on_cluster(
metal_core,
fragment_tmpl,
self.rng,
ca,
anchor_index=anchor,
bond_axis=bond_axis,
site_core=metal_core,
clash_atoms=clash_mobile,
)
if placed is None:
return None
new_mobile = mobile.copy()
new_mobile.positions[ads_mask] = placed.get_positions()
if len(slab) > 0 and atoms_too_close_two_sets(new_mobile, slab, self.blmin):
return None
if not self._policy.uses_surface:
new_mobile.center()
return slab + new_mobile