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",
),
)