Source code for scgo.cluster_adsorbate.sites
"""Convex-hull adsorption site discovery for cluster and slab placement."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
import numpy as np
from ase import Atoms
from scgo.initialization.geometry_helpers import try_convex_hull
from scgo.utils.logging import get_logger
logger = get_logger(__name__)
SiteType = Literal["vertex", "edge", "facet"]
[docs]
@dataclass(frozen=True)
class SurfaceSiteCandidate:
site_type: SiteType
anchor: np.ndarray
normal: np.ndarray
# Call-stack / placement-session cache: identical site-core geometries reuse Qhull.
_SITE_CANDIDATE_CACHE: dict[int, dict[SiteType, list[SurfaceSiteCandidate]]] = {}
_SITE_CACHE_MAX = 64
def _safe_normalize(v: np.ndarray) -> np.ndarray:
vn = float(np.linalg.norm(v))
if vn < 1e-12:
return np.array([0.0, 0.0, 1.0], dtype=float)
return v / vn
def _site_core_positions_key(core: Atoms) -> int:
pos = np.ascontiguousarray(core.get_positions(), dtype=np.float64)
return hash(pos.tobytes())
def clear_surface_site_cache() -> None:
"""Drop cached hull site candidates (e.g. between independent placement stacks)."""
_SITE_CANDIDATE_CACHE.clear()
def get_or_compute_surface_site_candidates(
core: Atoms,
) -> dict[SiteType, list[SurfaceSiteCandidate]]:
"""Return surface sites for ``core``, caching by positions hash."""
key = _site_core_positions_key(core)
cached = _SITE_CANDIDATE_CACHE.get(key)
if cached is not None:
return cached
result = compute_surface_site_candidates(core)
if len(_SITE_CANDIDATE_CACHE) >= _SITE_CACHE_MAX:
# Drop an arbitrary old entry (insertion order in CPython 3.7+).
_SITE_CANDIDATE_CACHE.pop(next(iter(_SITE_CANDIDATE_CACHE)))
_SITE_CANDIDATE_CACHE[key] = result
return result
[docs]
def compute_surface_site_candidates(
core: Atoms,
) -> dict[SiteType, list[SurfaceSiteCandidate]]:
"""Build explicit vertex/edge/facet adsorption sites from a convex hull."""
out: dict[SiteType, list[SurfaceSiteCandidate]] = {
"vertex": [],
"edge": [],
"facet": [],
}
if len(core) < 4:
return out
pos = core.get_positions()
com = np.mean(pos, axis=0)
hull = try_convex_hull(pos)
if hull is None:
logger.debug(
"Convex hull site discovery unavailable for %d core atoms", len(core)
)
return out
vertices = np.asarray(hull.vertices, dtype=np.intp)
for vidx in vertices:
anchor = pos[int(vidx)]
normal = _safe_normalize(anchor - com)
out["vertex"].append(
SurfaceSiteCandidate(site_type="vertex", anchor=anchor, normal=normal)
)
edge_pairs: set[tuple[int, int]] = set()
for simplex in hull.simplices:
i, j, k = int(simplex[0]), int(simplex[1]), int(simplex[2])
edge_pairs.add(tuple(sorted((i, j))))
edge_pairs.add(tuple(sorted((j, k))))
edge_pairs.add(tuple(sorted((i, k))))
for i, j in sorted(edge_pairs):
midpoint = 0.5 * (pos[i] + pos[j])
normal = _safe_normalize(midpoint - com)
out["edge"].append(
SurfaceSiteCandidate(site_type="edge", anchor=midpoint, normal=normal)
)
for simplex in hull.simplices:
tri = pos[np.asarray(simplex, dtype=np.intp)]
v1 = tri[1] - tri[0]
v2 = tri[2] - tri[0]
centroid = np.mean(tri, axis=0)
normal = _safe_normalize(np.cross(v1, v2))
if float(np.dot(normal, centroid - com)) < 0.0:
normal = -normal
out["facet"].append(
SurfaceSiteCandidate(site_type="facet", anchor=centroid, normal=normal)
)
return out