vibe-sketcherDocumentation

Source code for vibesketcher.chem.nanotube_cells

"""Periodic carbon nanotube cells from chiral indices (n, m).

One or more translation periods of a single- or multi-walled tube along z,
rolled with the chiral-vector construction of
:mod:`vibesketcher.chem.nanostructures` (Saito, Dresselhaus and Dresselhaus,
*Physical Properties of Carbon Nanotubes*, Imperial College Press 1998, ch. 3;
Hamada, Sawada and Oshiyama, Phys. Rev. Lett. 68, 1579, 1992). The x and y
cell vectors hold the outer diameter plus a stated vacuum gap, and are not
periodic. Walls of a multi-walled cell must have commensurate periods; an
incommensurate pair is refused, never strained.
"""

from __future__ import annotations

from typing import Literal, Self

import numpy as np
from pydantic import BaseModel, ConfigDict, Field, model_validator

from vibesketcher.chem.periodic_geometry import (
    BuildRefused,
    _check_collisions,
    _provenance,
    _structure,
    neighbour_pairs,
)
from vibesketcher.document.model import Chirality, PeriodicStructure
from vibesketcher.limits import MAX_ATOMS

__all__ = ["NanotubeRecipe", "build_nanotube_cell"]

_ID = Field(pattern=r"^[A-Za-z][A-Za-z0-9_-]{0,63}$")


[docs] class NanotubeRecipe(BaseModel): """A periodic carbon nanotube: armchair (n,n), zigzag (n,0) or chiral (n,m).""" model_config = ConfigDict(extra="forbid") kind: Literal["nanotube"] = "nanotube" identifier: str = _ID n: int = Field(ge=1, le=100) m: int | None = Field(default=None, ge=0, le=100) type: Literal["chiral", "armchair", "zigzag"] = "chiral" outer_walls: list[Chirality] = Field(default_factory=list, max_length=9) periods: int = Field(default=1, ge=1, le=1000) vacuum: float = Field(default=10.0, ge=3, le=200, allow_inf_nan=False) @model_validator(mode="after") def _indices(self) -> Self: if self.type == "armchair" and self.m not in (None, self.n): raise ValueError("an armchair tube is (n,n); leave m out or equal to n") if self.type == "zigzag" and self.m not in (None, 0): raise ValueError("a zigzag tube is (n,0); leave m out or 0") if self.type == "chiral" and self.m is None: raise ValueError("a chiral tube needs both n and m (or choose armchair or zigzag)") self.chirality() return self
[docs] def chirality(self) -> Chirality: if self.type == "armchair": return Chirality(n=self.n, m=self.n) if self.type == "zigzag": return Chirality(n=self.n, m=0) assert self.m is not None if self.m > self.n: raise ValueError(f"({self.n},{self.m}): write the indices with m <= n") return Chirality(n=self.n, m=self.m)
[docs] def build_nanotube_cell(p: NanotubeRecipe) -> PeriodicStructure: """The periodic tube cell the recipe describes, or a refusal with the reason.""" identifier = p.identifier from vibesketcher.chem import nanostructures as nano walls = [p.chirality(), *p.outer_walls] for wall in walls: if nano.diameter_nm(wall) < nano._MIN_DIAMETER_NM: raise BuildRefused( f"({wall.n},{wall.m}) is {nano.diameter_nm(wall):.3f} nm across; " f"tubes thinner than {nano._MIN_DIAMETER_NM} nm are not built" ) for inner, outer in zip(walls, walls[1:], strict=False): gap = (nano.diameter_nm(outer) - nano.diameter_nm(inner)) / 2 low, high = nano._WALL_GAP_NM if not low <= gap <= high: raise BuildRefused( f"walls ({inner.n},{inner.m}) and ({outer.n},{outer.m}) are {gap:.3f} nm apart; " f"concentric walls must be {low}-{high} nm apart" ) period = nano.translation_angstrom(walls[0]) length = p.periods * period periods: list[int] = [] for wall in walls: ratio = length / nano.translation_angstrom(wall) if abs(ratio - round(ratio)) > 1e-6 or round(ratio) < 1: raise BuildRefused( f"wall ({wall.n},{wall.m}) is not commensurate with ({walls[0].n},{walls[0].m}) " f"over {p.periods} period(s); a periodic multi-walled cell needs translation " "periods in a whole-number ratio (all armchair or all zigzag walls)" ) periods.append(round(ratio)) count = sum(2 * nano.hexagons_per_period(w) * k for w, k in zip(walls, periods, strict=True)) if count > MAX_ATOMS: raise BuildRefused(f"{count} carbons exceed the {MAX_ATOMS}-atom limit") points = np.vstack([nano._wall(w, k) for w, k in zip(walls, periods, strict=True)]) outer_diameter = nano.diameter_nm(walls[-1]) * 10 side = outer_diameter + p.vacuum lattice = np.diag([side, side, length]) pbc = (False, False, True) cartesian = points + np.array([side / 2, side / 2, 0.0]) pairs = neighbour_pairs(cartesian, lattice, pbc, nano._CC_CUTOFF) degree = [0] * len(cartesian) for i, j, _, _ in pairs: degree[i] += 1 degree[j] += 1 if any(d != 3 for d in degree): raise BuildRefused("the rolled lattice did not give three neighbours per carbon") _check_collisions(cartesian, lattice, pbc) summaries = [nano.wall_properties(w) for w in walls] return _structure( identifier, lattice, pbc, ["C"] * len(cartesian), cartesian, [(i, j, 1.5) for i, j, _, _ in pairs], cell_setting="unspecified", charge=0, multiplicity=1, provenance=_provenance( "nanotube", p, walls=summaries, periods_per_wall=periods, axis="z", spin="lowest multiplicity allowed by electron parity; not a ground-state prediction", ), )