Source code for scgo.cluster_adsorbate.rigid
"""Rigid-body restoration for adsorbate fragments with frozen internal geometry."""
from __future__ import annotations
from collections.abc import Sequence
import numpy as np
from ase import Atoms
from scgo.cluster_adsorbate.constraints import (
attach_adsorbate_internal_geometry_constraints,
)
from scgo.cluster_adsorbate.helpers import parse_positive_fragment_lengths
from scgo.system_types import (
AdsorbateDefinition,
AdsorbateFragmentInput,
resolve_adsorbate_fragments,
)
def _kabsch_place_template(template: np.ndarray, target: np.ndarray) -> np.ndarray:
"""Place ``template`` onto ``target`` preserving target centroid and orientation."""
if len(template) != len(target):
raise ValueError("template and target must have the same length")
if len(template) == 1:
return target.copy()
template_com = template.mean(axis=0)
target_com = target.mean(axis=0)
template_centered = template - template_com
target_centered = target - target_com
covariance = template_centered.T @ target_centered
u, _singular, vt = np.linalg.svd(covariance)
rotation = vt.T @ u.T
if np.linalg.det(rotation) < 0.0:
vt[-1, :] *= -1.0
rotation = vt.T @ u.T
return (template_centered @ rotation.T) + target_com
[docs]
def restore_rigid_adsorbate_fragments(
atoms: Atoms,
*,
n_slab: int,
adsorbate_definition: AdsorbateDefinition,
fragment_templates: Sequence[Atoms],
) -> None:
"""Reset adsorbate internal coordinates to templates in-place.
Each fragment keeps its current center and best-fit rigid orientation.
"""
core_symbols = adsorbate_definition.get("core_symbols", [])
if not isinstance(core_symbols, list):
return
lengths = parse_positive_fragment_lengths(
adsorbate_definition.get("adsorbate_fragment_lengths", [])
)
if len(lengths) != len(fragment_templates):
return
positions = atoms.get_positions()
ads_start = int(n_slab) + len(core_symbols)
offset = 0
for frag_len, template in zip(lengths, fragment_templates, strict=True):
start = ads_start + offset
end = start + frag_len
template_pos = np.asarray(template.get_positions(), dtype=float)
current = np.asarray(positions[start:end], dtype=float)
positions[start:end] = _kabsch_place_template(template_pos, current)
offset += frag_len
atoms.set_positions(positions)
[docs]
def enforce_frozen_adsorbate_geometry(
atoms: Atoms,
*,
n_slab: int,
adsorbate_definition: AdsorbateDefinition | None,
fragment_templates: AdsorbateFragmentInput | None,
reattach_constraints: bool = False,
) -> None:
"""Snap adsorbate fragments back to templates and optionally reattach constraints."""
if adsorbate_definition is None or fragment_templates is None:
return
fragments = resolve_adsorbate_fragments(
fragment_templates,
adsorbate_definition,
context="enforce_frozen_adsorbate_geometry",
)
restore_rigid_adsorbate_fragments(
atoms,
n_slab=n_slab,
adsorbate_definition=adsorbate_definition,
fragment_templates=fragments,
)
if reattach_constraints:
atoms.set_constraint([])
attach_adsorbate_internal_geometry_constraints(
atoms,
n_slab=n_slab,
adsorbate_definition=adsorbate_definition,
)