Source code for hofmann.model.polyhedron_spec

from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from hofmann.model._util import _field_defaults
from hofmann.model.colour import Colour, normalise_colour


[docs] @dataclass class PolyhedronSpec: """Declarative rule for rendering coordination polyhedra. A polyhedron is drawn around each atom whose species matches *centre*, using its bonded neighbours as vertices of a convex hull. Species names support fnmatch-style wildcards. Attributes: centre: Species pattern for the centre atom (e.g. ``"Ti"``). colour: Face colour, or ``None`` to inherit from the centre atom's style colour. alpha: Face transparency (0 = fully transparent, 1 = opaque). edge_colour: Colour for face wireframe edges. edge_width: Line width for face wireframe edges (points). hide_centre: Whether to hide the centre atom circle when a polyhedron is drawn. hide_bonds: Whether to hide bonds from the centre atom to its coordinating neighbours when a polyhedron is drawn. hide_vertices: Whether to hide the vertex atom circles. An atom is only hidden if *every* polyhedron it participates in has ``hide_vertices=True``. min_vertices: Minimum number of bonded neighbours required to draw a polyhedron. Centre atoms with fewer neighbours are skipped. ``None`` uses the default minimum of 3. """ centre: str colour: Colour | None = None alpha: float = 0.4 edge_colour: Colour = (0.15, 0.15, 0.15) edge_width: float = 1.0 hide_centre: bool = False hide_bonds: bool = False hide_vertices: bool = False min_vertices: int | None = None def __post_init__(self) -> None: if not 0.0 <= self.alpha <= 1.0: raise ValueError( f"alpha must be between 0.0 and 1.0, got {self.alpha}" ) if self.edge_width < 0: raise ValueError( f"edge_width must be non-negative, got {self.edge_width}" ) if self.min_vertices is not None and self.min_vertices < 3: raise ValueError( f"min_vertices must be >= 3, got {self.min_vertices}" )
[docs] def to_dict(self) -> dict: """Serialise to a JSON-compatible dictionary. Fields at their default values are omitted. Colours are normalised to ``[r, g, b]`` lists. """ # centre and colour are handled separately above. defaults = _field_defaults( type(self), exclude=frozenset({"centre", "colour"}), ) d: dict = {"centre": self.centre} if self.colour is not None: d["colour"] = list(normalise_colour(self.colour)) for field_name, default in defaults.items(): val = getattr(self, field_name) if field_name == "edge_colour": if normalise_colour(val) != normalise_colour(default): d[field_name] = list(normalise_colour(val)) else: if val != default: d[field_name] = val return d
[docs] @classmethod def from_dict(cls, d: dict) -> PolyhedronSpec: """Deserialise from a dictionary.""" defaults = _field_defaults(cls, exclude=frozenset({"centre", "colour"})) kwargs: dict = {"centre": d["centre"]} if "colour" in d: val = d["colour"] kwargs["colour"] = tuple(val) if isinstance(val, list) else val for field_name in defaults: if field_name in d: val = d[field_name] if field_name == "edge_colour" and isinstance(val, list): val = tuple(val) kwargs[field_name] = val return cls(**kwargs)
[docs] @dataclass(frozen=True) class Polyhedron: """A computed coordination polyhedron. Attributes: centre_index: Index of the centre atom. neighbour_indices: Indices of the coordinating atoms. faces: List of faces, each a 1-D array of vertex indices into *neighbour_indices*. Triangular faces have length 3; merged coplanar faces may have 4 or more vertices. spec: The PolyhedronSpec that produced this polyhedron. """ centre_index: int neighbour_indices: tuple[int, ...] faces: list[np.ndarray] spec: PolyhedronSpec