vibe-sketcherDocumentation

Source code for vibesketcher.render.schematic

"""Schematic aids for periodic publication figures (#233).

Every function here reads the 3D model and returns drawing data; none of them
changes a structure. The figure in :mod:`vibesketcher.render.periodic` uses
them for heights, coordination polyhedra and slab layer schematics.
"""

from __future__ import annotations

import itertools
from fractions import Fraction

import numpy as np

from vibesketcher.document import crystal
from vibesketcher.document.model import PeriodicStructure

_GLYPHS = {Fraction(1, 2): "½", Fraction(1, 4): "¼", Fraction(3, 4): "¾", Fraction(1, 3): "⅓",
           Fraction(2, 3): "⅔", Fraction(1, 8): "⅛", Fraction(3, 8): "⅜", Fraction(5, 8): "⅝",
           Fraction(7, 8): "⅞", Fraction(1, 6): "⅙", Fraction(5, 6): "⅚"}  # fmt: skip


[docs] def view_period(structure: PeriodicStructure, direction: np.ndarray) -> np.ndarray | None: """The shortest lattice vector along the view direction, if one exists (indices up to 6).""" rows = crystal.lattice_rows(structure) d = direction / np.linalg.norm(direction) periodic = [i for i in range(3) if structure.pbc[i]] best: np.ndarray | None = None for coefficients in itertools.product(range(-6, 7), repeat=len(periodic)): if not any(coefficients): continue vector = sum((c * rows[i] for c, i in zip(coefficients, periodic, strict=True)), np.zeros(3)) # fmt: skip length = float(np.linalg.norm(vector)) if abs(abs(float(vector @ d)) - length) < 1e-6 * max(length, 1.0) and ( best is None or length < float(np.linalg.norm(best)) - 1e-9 ): best = vector if float(vector @ d) > 0 else -vector return best
[docs] def fraction_text(value: float) -> str: """0, ½, ¼, 0.37: a height in the projection, as crystallographers write it.""" value %= 1.0 if value > 1 - 1e-3: value = 0.0 exact = Fraction(value).limit_denominator(8) if abs(float(exact) - value) < 2e-3: if exact == 0: return "0" return _GLYPHS.get(exact, f"{exact.numerator}/{exact.denominator}") return f"{value:.2f}".lstrip("0")
[docs] def heights( structure: PeriodicStructure, positions: np.ndarray, toward: np.ndarray ) -> tuple[list[str], str]: """Height labels of the atoms toward the viewer, and what they measure. Along a lattice direction the heights are fractions of that period; otherwise (for example across a slab's vacuum) they are in angstrom above the lowest atom. """ period = view_period(structure, toward) if period is not None: length = float(np.linalg.norm(period)) unit = period / length values = positions @ unit / length return [fraction_text(v) for v in values], f"heights in units of {length:.3f} Å" depth = positions @ (toward / np.linalg.norm(toward)) return [f"{v:.1f}" for v in depth - depth.min()], "heights in Å above the lowest atom"
[docs] def hull_faces(points: np.ndarray, tolerance: float = 1e-3) -> list[list[int]]: """Faces (vertex indices, ordered around the face) of the convex hull of a few points.""" n = len(points) if n < 3: return [] centre = points.mean(axis=0) if n == 3 or np.linalg.matrix_rank(points - centre, tol=1e-6) < 3: normal = np.cross(points[1] - points[0], points[2] - points[0]) return [_ordered(list(range(n)), points, normal)] if np.linalg.norm(normal) > 1e-9 else [] faces: dict[tuple[int, ...], list[int]] = {} for i, j, k in itertools.combinations(range(n), 3): normal = np.cross(points[j] - points[i], points[k] - points[i]) if np.linalg.norm(normal) < 1e-9: continue normal /= np.linalg.norm(normal) side = (points - points[i]) @ normal if np.all(side <= tolerance) or np.all(side >= -tolerance): members = tuple(int(m) for m in np.nonzero(np.abs(side) <= tolerance)[0]) if members not in faces: faces[members] = _ordered(list(members), points, normal) return list(faces.values())
def _ordered(members: list[int], points: np.ndarray, normal: np.ndarray) -> list[int]: face = points[members] centre = face.mean(axis=0) e1 = face[0] - centre e1 /= np.linalg.norm(e1) e2 = np.cross(normal / np.linalg.norm(normal), e1) angles = [float(np.arctan2((p - centre) @ e2, (p - centre) @ e1)) for p in face] return [members[i] for i in np.argsort(angles)]
[docs] def coordination( structure: PeriodicStructure, centre: np.ndarray, centre_symbol: str, radius: dict[str, float], scale: float = 1.15, ) -> np.ndarray: """Cartesian positions of the atoms bonded to an atom at ``centre`` (all images).""" rows = crystal.lattice_rows(structure) shifts = list(itertools.product(*((-1, 0, 1) if p else (0,) for p in structure.pbc))) found = [] for site in structure.sites: if site.symbol == centre_symbol: continue base = np.asarray(site.fractional, dtype=float) @ rows limit = scale * (radius[centre_symbol] + radius[site.symbol]) for shift in shifts: p = base + np.asarray(shift) @ rows distance = float(np.linalg.norm(p - centre)) if 0.4 < distance < limit: found.append(p) return np.asarray(found) if found else np.zeros((0, 3))
[docs] def stacking_letters(structure: PeriodicStructure, tolerance: float = 0.3) -> dict[str, str]: """Stacking letter (A, B, C, ...) of each site's atomic plane in a slab. Planes are grouped by height along the normal; two planes get the same letter when their in-plane fractional positions coincide modulo the surface lattice. """ rows = crystal.lattice_rows(structure) normal = np.cross(rows[0], rows[1]) normal /= np.linalg.norm(normal) xyz = crystal.cartesian(structure) z = xyz @ normal order = np.argsort(z) planes: list[list[int]] = [] for index in order: if planes and z[index] - z[planes[-1][-1]] <= tolerance: planes[-1].append(int(index)) else: planes.append([int(index)]) in_plane = np.linalg.inv(rows) letters: dict[str, str] = {} registry: list[tuple[frozenset[tuple[str, int, int]], str]] = [] for plane in planes: key = frozenset( (structure.sites[i].symbol, int(round(((xyz[i] @ in_plane)[0] % 1.0) * 50)) % 50, int(round(((xyz[i] @ in_plane)[1] % 1.0) * 50)) % 50) for i in plane ) # fmt: skip letter = next((name for k, name in registry if k == key), None) if letter is None: letter = chr(ord("A") + len(registry)) if len(registry) < 26 else "?" registry.append((key, letter)) for i in plane: letters[structure.sites[i].id] = letter return letters