vibe-sketcherDocumentation
Source code for vibesketcher.document.crystal_templates
"""A library of standard crystals given by space group and asymmetric unit (#231).
Every template is a space-group setting, conventional-cell parameters and an
asymmetric unit, expanded by :func:`vibesketcher.document.crystal.from_space_group`.
The library is therefore also a test of the space-group machinery: each entry
records the composition and Z it must produce.
The lattice constants are representative room-temperature experimental values.
``reference`` names the source of the structure and its parameters; values a
study depends on should be checked against the primary literature for that
study's conditions (temperature, pressure, composition).
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Any, Literal
from vibesketcher.document.crystal import LatticeParameters, from_space_group
from vibesketcher.document.model import PeriodicStructure
Category = Literal[
"elements",
"halides",
"oxides",
"semiconductors",
"perovskites",
"spinels",
"layered",
"minerals",
"carbides and nitrides",
]
WYCKOFF_1 = "R. W. G. Wyckoff, Crystal Structures, 2nd ed., Vol. 1 (Interscience, New York, 1963)"
WYCKOFF_2 = "R. W. G. Wyckoff, Crystal Structures, 2nd ed., Vol. 2 (Interscience, New York, 1964)"
WYCKOFF_3 = "R. W. G. Wyckoff, Crystal Structures, 2nd ed., Vol. 3 (Interscience, New York, 1965)"
_T = 1 / 3
_TT = 2 / 3
Site = tuple[str, tuple[float, float, float]]
[docs]
@dataclass(frozen=True)
class Surface:
"""A commonly studied surface of a template, for one-step slabs (#232)."""
miller: tuple[int, int, int]
note: str = ""
[docs]
@dataclass(frozen=True)
class Template:
name: str
title: str
formula: str
category: Category
space_group: str
a: float
sites: tuple[Site, ...]
z: int
reference: str
c: float | None = None
b: float | None = None
beta: float | None = None
aliases: tuple[str, ...] = ()
structure_type: str = ""
surfaces: tuple[Surface, ...] = ()
#: Experimental density in g/cm^3 where checked, for the library test.
density: float | None = None
extra: dict[str, Any] = field(default_factory=dict)
[docs]
def parameters(self) -> LatticeParameters:
return LatticeParameters(self.a, b=self.b, c=self.c, beta=self.beta)
_ROCKSALT = "Fm-3m"
_TEMPLATES: list[Template] = []
def _add(*templates: Template) -> None:
_TEMPLATES.extend(templates)
def _rocksalt(
name: str,
a: str,
b: str,
lattice: float,
category: Category = "halides",
title: str = "",
aliases: tuple[str, ...] = (),
density: float | None = None,
surfaces: tuple[Surface, ...] = (Surface((1, 0, 0)),),
) -> Template:
return Template(
name,
title or f"{a}{b} (rocksalt)",
f"{a}{b}",
category,
_ROCKSALT,
lattice,
((a, (0, 0, 0)), (b, (0.5, 0.5, 0.5))),
4,
WYCKOFF_1,
aliases=aliases,
structure_type="rocksalt (B1)",
surfaces=surfaces,
density=density,
)
def _metal(
name: str,
element: str,
kind: str,
a: float,
c: float | None = None,
density: float | None = None,
) -> Template:
if kind == "fcc":
return Template(
name,
f"{element} (fcc)",
element,
"elements",
"Fm-3m",
a,
((element, (0, 0, 0)),),
4,
WYCKOFF_1,
structure_type="fcc (A1)",
surfaces=(Surface((1, 1, 1)), Surface((1, 0, 0)), Surface((1, 1, 0))),
density=density,
)
if kind == "bcc":
return Template(
name,
f"{element} (bcc)",
element,
"elements",
"Im-3m",
a,
((element, (0, 0, 0)),),
2,
WYCKOFF_1,
structure_type="bcc (A2)",
surfaces=(Surface((1, 1, 0)), Surface((1, 0, 0))),
density=density,
)
return Template(
name,
f"{element} (hcp)",
element,
"elements",
"P6_3/mmc",
a,
((element, (_T, _TT, 0.25)),),
2,
WYCKOFF_1,
c=c,
structure_type="hcp (A3)",
surfaces=(Surface((0, 0, 1)),),
density=density,
)
def _zincblende(
name: str,
a: str,
b: str,
lattice: float,
category: Category = "semiconductors",
aliases: tuple[str, ...] = (),
density: float | None = None,
) -> Template:
return Template(
name,
f"{a}{b} (zincblende)",
f"{a}{b}",
category,
"F-43m",
lattice,
((a, (0, 0, 0)), (b, (0.25, 0.25, 0.25))),
4,
WYCKOFF_1,
aliases=aliases,
structure_type="zincblende (B3)",
surfaces=(Surface((1, 1, 0)), Surface((1, 0, 0)), Surface((1, 1, 1))),
density=density,
)
def _wurtzite(
name: str,
a: str,
b: str,
lattice: float,
c: float,
u: float,
category: Category = "semiconductors",
aliases: tuple[str, ...] = (),
density: float | None = None,
) -> Template:
return Template(
name,
f"{a}{b} (wurtzite)",
f"{a}{b}",
category,
"P6_3mc",
lattice,
((a, (_T, _TT, 0)), (b, (_T, _TT, u))),
2,
WYCKOFF_1,
c=c,
aliases=aliases,
structure_type="wurtzite (B4)",
surfaces=(
Surface((1, 0, 0), "the nonpolar (10-10) surface"),
Surface((0, 0, 1), "the polar (0001) surface"),
),
density=density,
)
def _fluorite(
name: str,
cation: str,
anion: str,
lattice: float,
category: Category = "oxides",
anti: bool = False,
aliases: tuple[str, ...] = (),
density: float | None = None,
) -> Template:
if anti: # anion on 4a, two cations on 8c
sites: tuple[Site, ...] = ((anion, (0, 0, 0)), (cation, (0.25, 0.25, 0.25)))
formula, kind = f"{cation}2{anion}", "antifluorite"
else:
sites = ((cation, (0, 0, 0)), (anion, (0.25, 0.25, 0.25)))
formula, kind = f"{cation}{anion}2", "fluorite (C1)"
return Template(
name,
f"{formula} ({kind.split()[0]})",
formula,
category,
"Fm-3m",
lattice,
sites,
4,
WYCKOFF_1,
aliases=aliases,
structure_type=kind,
surfaces=(Surface((1, 1, 1)), Surface((1, 1, 0))),
density=density,
)
def _rutile(
name: str,
cation: str,
anion: str,
a: float,
c: float,
x: float,
category: Category = "oxides",
aliases: tuple[str, ...] = (),
density: float | None = None,
) -> Template:
return Template(
name,
f"{cation}{anion}2 (rutile)",
f"{cation}{anion}2",
category,
"P4_2/mnm",
a,
((cation, (0, 0, 0)), (anion, (x, x, 0))),
2,
WYCKOFF_1,
c=c,
aliases=aliases,
structure_type="rutile (C4)",
surfaces=(Surface((1, 1, 0)), Surface((1, 0, 0)), Surface((1, 0, 1))),
density=density,
)
# --- elements -------------------------------------------------------------------
_add(
_metal("cu", "Cu", "fcc", 3.615, density=8.96),
_metal("ag", "Ag", "fcc", 4.086, density=10.50),
_metal("au", "Au", "fcc", 4.078, density=19.3),
_metal("al", "Al", "fcc", 4.050, density=2.70),
_metal("ni", "Ni", "fcc", 3.524, density=8.91),
_metal("pd", "Pd", "fcc", 3.891),
_metal("pt", "Pt", "fcc", 3.924, density=21.45),
_metal("pb", "Pb", "fcc", 4.950),
_metal("rh", "Rh", "fcc", 3.803),
_metal("ir", "Ir", "fcc", 3.839),
_metal("ca", "Ca", "fcc", 5.588),
_metal("fe", "Fe", "bcc", 2.866, density=7.87),
_metal("cr", "Cr", "bcc", 2.885),
_metal("mo", "Mo", "bcc", 3.147, density=10.22),
_metal("w", "W", "bcc", 3.165, density=19.3),
_metal("v", "V", "bcc", 3.030),
_metal("nb", "Nb", "bcc", 3.300),
_metal("ta", "Ta", "bcc", 3.303),
_metal("li", "Li", "bcc", 3.509),
_metal("na", "Na", "bcc", 4.291),
_metal("k", "K", "bcc", 5.328),
_metal("ba", "Ba", "bcc", 5.025),
_metal("mg", "Mg", "hcp", 3.209, 5.211, density=1.74),
_metal("ti", "Ti", "hcp", 2.951, 4.686, density=4.51),
_metal("zn", "Zn", "hcp", 2.665, 4.947),
_metal("co", "Co", "hcp", 2.507, 4.069),
_metal("zr", "Zr", "hcp", 3.232, 5.147),
_metal("be", "Be", "hcp", 2.286, 3.584),
_metal("cd", "Cd", "hcp", 2.979, 5.619),
_metal("ru", "Ru", "hcp", 2.706, 4.282),
_metal("hf", "Hf", "hcp", 3.195, 5.051),
_metal("re", "Re", "hcp", 2.761, 4.456),
_metal("os", "Os", "hcp", 2.734, 4.317),
_metal("sc", "Sc", "hcp", 3.309, 5.273),
_metal("y", "Y", "hcp", 3.647, 5.731),
)
for _name, _element, _a, _rho in (
("diamond", "C", 3.567, 3.515),
("si", "Si", 5.431, 2.329),
("ge", "Ge", 5.658, 5.32),
("alpha-sn", "Sn", 6.489, None),
):
_add(
Template(
_name,
f"{_element} (diamond structure)",
_element,
"elements",
"Fd-3m",
_a,
((_element, (0.125, 0.125, 0.125)),),
8,
WYCKOFF_1,
aliases=("c-diamond",) if _element == "C" else ("silicon",) if _element == "Si" else (),
structure_type="diamond (A4)",
surfaces=(Surface((1, 1, 1)), Surface((1, 0, 0)), Surface((1, 1, 0))),
density=_rho,
)
)
# --- rocksalt halides, oxides and others ----------------------------------------
_add(
_rocksalt(
"mgo", "Mg", "O", 4.212, "oxides", "MgO (periclase)", ("periclase", "magnesia"), 3.58
),
_rocksalt("cao", "Ca", "O", 4.811, "oxides", "CaO (lime)", ("lime",)),
_rocksalt("sro", "Sr", "O", 5.160, "oxides"),
_rocksalt("bao", "Ba", "O", 5.523, "oxides"),
_rocksalt("nio", "Ni", "O", 4.177, "oxides", "NiO (bunsenite)"),
_rocksalt("mno", "Mn", "O", 4.445, "oxides"),
_rocksalt("coo", "Co", "O", 4.260, "oxides"),
_rocksalt(
"nacl",
"Na",
"Cl",
5.640,
title="NaCl (halite)",
aliases=("halite", "rocksalt-nacl"),
density=2.165,
),
_rocksalt("lif", "Li", "F", 4.027),
_rocksalt("licl", "Li", "Cl", 5.130),
_rocksalt("naf", "Na", "F", 4.634),
_rocksalt("nabr", "Na", "Br", 5.977),
_rocksalt("kcl", "K", "Cl", 6.293, title="KCl (sylvite)", aliases=("sylvite",), density=1.98),
_rocksalt("kbr", "K", "Br", 6.600),
_rocksalt("agcl", "Ag", "Cl", 5.549),
_rocksalt("pbs", "Pb", "S", 5.936, "semiconductors", "PbS (galena)", ("galena",)),
_rocksalt("tin", "Ti", "N", 4.240, "carbides and nitrides", "TiN (osbornite)"),
_rocksalt("tic", "Ti", "C", 4.328, "carbides and nitrides"),
)
for _name, _ca, _an, _lattice in (
("cscl", "Cs", "Cl", 4.123),
("csbr", "Cs", "Br", 4.286),
("csi", "Cs", "I", 4.567),
("nial", "Ni", "Al", 2.887),
):
_add(
Template(
_name,
f"{_ca}{_an} (CsCl structure)",
f"{_ca}{_an}",
"halides" if _an != "Al" else "elements",
"Pm-3m",
_lattice,
((_ca, (0, 0, 0)), (_an, (0.5, 0.5, 0.5))),
1,
WYCKOFF_1,
structure_type="CsCl (B2)",
surfaces=(Surface((1, 1, 0)), Surface((1, 0, 0))),
)
)
# --- fluorite and antifluorite --------------------------------------------------
_add(
_fluorite("caf2", "Ca", "F", 5.463, "halides", aliases=("fluorite",), density=3.18),
_fluorite("srf2", "Sr", "F", 5.800, "halides"),
_fluorite("baf2", "Ba", "F", 6.200, "halides"),
_fluorite("ceo2", "Ce", "O", 5.411, aliases=("ceria",), density=7.22),
_fluorite("tho2", "Th", "O", 5.597),
_fluorite("uo2", "U", "O", 5.470),
_fluorite("li2o", "Li", "O", 4.611, anti=True),
)
# --- zincblende and wurtzite semiconductors -------------------------------------
_add(
_zincblende("gaas", "Ga", "As", 5.653, density=5.32),
_zincblende("gap", "Ga", "P", 5.451),
_zincblende("inp", "In", "P", 5.869),
_zincblende("inas", "In", "As", 6.058),
_zincblende("alas", "Al", "As", 5.661),
_zincblende("alp", "Al", "P", 5.463),
_zincblende("zns", "Zn", "S", 5.409, aliases=("sphalerite", "zns-zincblende"), density=4.09),
_zincblende("znse", "Zn", "Se", 5.668),
_zincblende("cdte", "Cd", "Te", 6.481),
_zincblende(
"sic-3c", "Si", "C", 4.360, "carbides and nitrides", aliases=("beta-sic", "3c-sic")
),
_zincblende("c-bn", "B", "N", 3.615, "carbides and nitrides", aliases=("cbn", "cubic-bn")),
_wurtzite("zno", "Zn", "O", 3.250, 5.207, 0.382, "oxides", ("zincite",)),
_wurtzite("gan", "Ga", "N", 3.189, 5.185, 0.377, density=6.15),
_wurtzite("aln", "Al", "N", 3.112, 4.982, 0.382, "carbides and nitrides"),
_wurtzite("zns-wurtzite", "Zn", "S", 3.821, 6.257, 0.375),
_wurtzite("cds", "Cd", "S", 4.136, 6.714, 0.377, aliases=("greenockite",)),
)
# --- rutile, anatase and other dioxides -----------------------------------------
_add(
_rutile("rutile", "Ti", "O", 4.594, 2.959, 0.3048, aliases=("tio2-rutile",), density=4.25),
_rutile("sno2", "Sn", "O", 4.737, 3.186, 0.307, aliases=("cassiterite",), density=6.99),
_rutile("stishovite", "Si", "O", 4.180, 2.666, 0.3062),
_rutile("ruo2", "Ru", "O", 4.492, 3.107, 0.306),
_rutile("mgf2", "Mg", "F", 4.621, 3.052, 0.303, "halides", ("sellaite",)),
Template(
"anatase",
"TiO2 (anatase)",
"TiO2",
"oxides",
"I4_1/amd:1",
3.785,
(("Ti", (0, 0, 0)), ("O", (0, 0, 0.2081))),
4,
"M. Horn, C. F. Schwerdtfeger, E. P. Meagher, Z. Kristallogr. 136, 273 (1972)",
c=9.514,
aliases=("tio2-anatase",),
structure_type="anatase (C5)",
surfaces=(Surface((1, 0, 1)), Surface((0, 0, 1))),
density=3.89,
),
Template(
"alpha-quartz",
"SiO2 (alpha-quartz)",
"SiO2",
"minerals",
"P3_221",
4.9134,
(("Si", (0.4697, 0, _TT)), ("O", (0.4135, 0.2669, 0.1191 + _TT))),
3,
"Y. Le Page, G. Donnay, Acta Cryst. B32, 2456 (1976); origin moved by 2c/3 "
"to the ITA setting of P3_221",
c=5.4052,
aliases=("quartz",),
structure_type="alpha-quartz",
surfaces=(Surface((0, 0, 1)), Surface((1, 0, 1))),
density=2.65,
),
Template(
"zro2-monoclinic",
"ZrO2 (baddeleyite)",
"ZrO2",
"oxides",
"P2_1/c",
5.1507,
(
("Zr", (0.2754, 0.0395, 0.2083)),
("O", (0.0700, 0.3317, 0.3447)),
("O", (0.4496, 0.7569, 0.4792)),
),
4,
"C. J. Howard, R. J. Hill, B. E. Reichert, Acta Cryst. B44, 116 (1988)",
b=5.2028,
c=5.3156,
beta=99.194,
aliases=("baddeleyite",),
structure_type="baddeleyite",
surfaces=(Surface((1, 1, 1)), Surface((-1, 1, 1), "the (-111) surface")),
),
Template(
"cu2o",
"Cu2O (cuprite)",
"Cu2O",
"oxides",
"Pn-3m:1",
4.269,
(("O", (0, 0, 0)), ("Cu", (0.25, 0.25, 0.25))),
2,
WYCKOFF_1,
aliases=("cuprite",),
structure_type="cuprite (C3)",
surfaces=(Surface((1, 1, 1)), Surface((1, 0, 0))),
),
)
# --- corundum-type sesquioxides --------------------------------------------------
_add(
Template(
"al2o3",
"Al2O3 (corundum)",
"Al2O3",
"oxides",
"R-3c",
4.7602,
(("Al", (0, 0, 0.35216)), ("O", (0.30624, 0, 0.25))),
6,
"J. Lewis, D. Schwarzenbach, H. D. Flack, Acta Cryst. A38, 733 (1982)",
c=12.9933,
aliases=("corundum", "alpha-alumina", "sapphire"),
structure_type="corundum (D5_1)",
surfaces=(
Surface((0, 0, 1), "the (0001) basal plane"),
Surface((1, 0, 2), "the r plane (1-102)"),
),
density=3.987,
),
Template(
"cr2o3",
"Cr2O3 (eskolaite)",
"Cr2O3",
"oxides",
"R-3c",
4.959,
(("Cr", (0, 0, 0.3475)), ("O", (0.3058, 0, 0.25))),
6,
WYCKOFF_2,
c=13.594,
aliases=("eskolaite",),
structure_type="corundum (D5_1)",
surfaces=(Surface((0, 0, 1)),),
),
Template(
"fe2o3",
"Fe2O3 (hematite)",
"Fe2O3",
"oxides",
"R-3c",
5.035,
(("Fe", (0, 0, 0.3553)), ("O", (0.3059, 0, 0.25))),
6,
WYCKOFF_2,
c=13.747,
aliases=("hematite", "alpha-fe2o3"),
structure_type="corundum (D5_1)",
surfaces=(Surface((0, 0, 1)),),
density=5.26,
),
)
# --- perovskites and spinels ----------------------------------------------------
for _name, _a_site, _b_site, _lattice, _rho in (
("srtio3", "Sr", "Ti", 3.905, 5.12),
("ktao3", "K", "Ta", 3.989, None),
("bazro3", "Ba", "Zr", 4.193, None),
):
_add(
Template(
_name,
f"{_a_site}{_b_site}O3 (cubic perovskite)",
f"{_a_site}{_b_site}O3",
"perovskites",
"Pm-3m",
_lattice,
((_b_site, (0, 0, 0)), (_a_site, (0.5, 0.5, 0.5)), ("O", (0.5, 0, 0))),
1,
WYCKOFF_2,
structure_type="cubic perovskite (E2_1)",
surfaces=(Surface((1, 0, 0)), Surface((1, 1, 0))),
density=_rho,
)
)
for _name, _formula, _tet, _oct, _lattice, _x, _rho, _aliases in (
("mgal2o4", "MgAl2O4", "Mg", "Al", 8.083, 0.2624, 3.58, ("spinel",)),
("fe3o4", "Fe3O4", "Fe", "Fe", 8.396, 0.2549, 5.17, ("magnetite",)),
("znal2o4", "ZnAl2O4", "Zn", "Al", 8.086, 0.264, None, ("gahnite",)),
):
_add(
Template(
_name,
f"{_formula} (spinel)",
_formula,
"spinels",
"Fd-3m",
_lattice,
((_tet, (0.125, 0.125, 0.125)), (_oct, (0.5, 0.5, 0.5)), ("O", (_x, _x, _x))),
8,
WYCKOFF_3,
aliases=_aliases,
structure_type="spinel (H1_1)",
surfaces=(Surface((1, 0, 0)), Surface((1, 1, 1))),
density=_rho,
)
)
# --- layered and carbon ---------------------------------------------------------
_add(
Template(
"graphite",
"C (hexagonal graphite)",
"C",
"layered",
"P6_3/mmc",
2.464,
(("C", (0, 0, 0.25)), ("C", (_T, _TT, 0.25))),
4,
"R. Trucano, R. Chen, Nature 258, 136 (1975)",
c=6.711,
aliases=("graphite-2h",),
structure_type="graphite (A9)",
surfaces=(Surface((0, 0, 1), "the basal plane; one layer is graphene"),),
density=2.26,
),
Template(
"h-bn",
"BN (hexagonal)",
"BN",
"layered",
"P6_3/mmc",
2.504,
(("B", (_T, _TT, 0.25)), ("N", (_T, _TT, 0.75))),
2,
WYCKOFF_1,
c=6.661,
aliases=("hbn-bulk", "hexagonal-bn"),
structure_type="h-BN",
surfaces=(Surface((0, 0, 1)),),
density=2.28,
),
Template(
"mos2",
"MoS2 (2H, molybdenite)",
"MoS2",
"layered",
"P6_3/mmc",
3.160,
(("Mo", (_T, _TT, 0.25)), ("S", (_T, _TT, 0.621))),
2,
WYCKOFF_1,
c=12.295,
aliases=("molybdenite", "2h-mos2"),
structure_type="2H-MoS2 (C7)",
surfaces=(Surface((0, 0, 1)),),
),
Template(
"ws2",
"WS2 (2H, tungstenite)",
"WS2",
"layered",
"P6_3/mmc",
3.153,
(("W", (_T, _TT, 0.25)), ("S", (_T, _TT, 0.622))),
2,
WYCKOFF_1,
c=12.323,
aliases=("tungstenite", "2h-ws2"),
structure_type="2H-MoS2 (C7)",
surfaces=(Surface((0, 0, 1)),),
),
Template(
"licoo2",
"LiCoO2 (layered)",
"LiCoO2",
"layered",
"R-3m",
2.816,
(("Co", (0, 0, 0)), ("Li", (0, 0, 0.5)), ("O", (0, 0, 0.2396))),
3,
WYCKOFF_2,
c=14.05,
structure_type="alpha-NaFeO2",
surfaces=(Surface((0, 0, 1)), Surface((1, 0, 4))),
),
Template(
"calcite",
"CaCO3 (calcite)",
"CaCO3",
"minerals",
"R-3c",
4.990,
(("Ca", (0, 0, 0)), ("C", (0, 0, 0.25)), ("O", (0.257, 0, 0.25))),
6,
WYCKOFF_2,
c=17.061,
aliases=("caco3",),
structure_type="calcite (G0_1)",
surfaces=(Surface((1, 0, 4), "the cleavage plane (10-14)"),),
density=2.71,
),
)
# --- lookup -----------------------------------------------------------------------
_BY_NAME = {t.name: t for t in _TEMPLATES}
if len(_BY_NAME) != len(_TEMPLATES): # pragma: no cover - guards the data above
raise RuntimeError("duplicate crystal template names")
def _key(text: str) -> str:
return re.sub(r"[\s_]+", "-", text.strip().lower())
[docs]
def find(name: str) -> Template:
"""A template by name, alias or (when unambiguous) formula."""
key = _key(name)
if key in _BY_NAME:
return _BY_NAME[key]
hits = [t for t in _TEMPLATES if key in {_key(a) for a in t.aliases}]
if not hits:
hits = [t for t in _TEMPLATES if _key(t.formula) == key]
if len(hits) == 1:
return hits[0]
if hits:
names = ", ".join(t.name for t in hits)
raise ValueError(f"{name!r} matches several templates ({names}); choose one")
raise ValueError(f"unknown crystal template {name!r}; see 'vibe-sketcher solid templates'")
[docs]
def search(query: str | None = None, category: str | None = None) -> list[Template]:
"""Templates whose name, title, formula, alias or structure type contains ``query``."""
key = _key(query) if query else ""
out = []
for t in _TEMPLATES:
if category and t.category != category:
continue
haystack = [
t.name,
_key(t.title),
_key(t.formula),
_key(t.structure_type),
_key(t.category),
*(_key(a) for a in t.aliases),
]
if key and not any(key in h for h in haystack):
continue
out.append(t)
return out
[docs]
def describe(template: Template) -> dict[str, Any]:
return {
"name": template.name,
"title": template.title,
"formula": template.formula,
"category": template.category,
"structure_type": template.structure_type,
"space_group": template.space_group,
"parameters": {
k: v
for k, v in (
("a", template.a),
("b", template.b),
("c", template.c),
("beta", template.beta),
)
if v is not None
},
"asymmetric_unit": [{"element": e, "fractional": list(f)} for e, f in template.sites],
"z": template.z,
"aliases": list(template.aliases),
"surfaces": [{"miller": list(s.miller), "note": s.note} for s in template.surfaces],
"reference": template.reference,
}
[docs]
def build(
name: str,
*,
identifier: str,
cell: Literal["primitive", "conventional"] = "primitive",
a: float | None = None,
c: float | None = None,
species_map: dict[str, str] | None = None,
) -> PeriodicStructure:
"""Expand a template into a crystal, optionally overriding ``a`` and ``c``."""
template = find(name)
parameters = template.parameters()
if a is not None or c is not None:
if a is not None and a <= 0 or c is not None and c <= 0:
raise ValueError("lattice lengths must be positive")
scale_b = (a / template.a) if a is not None and template.b is not None else 1.0
parameters = LatticeParameters(
a if a is not None else template.a,
b=template.b * scale_b if template.b is not None else None,
c=c if c is not None else template.c,
beta=template.beta,
)
structure = from_space_group(
template.space_group,
parameters,
list(template.sites),
identifier=identifier,
cell=cell,
species_map=species_map,
)
builder = dict(structure.provenance.get("builder", {})) # type: ignore[arg-type]
builder["template"] = {
"name": template.name,
"title": template.title,
"formula": template.formula,
"reference": template.reference,
"overrides": {k: v for k, v in (("a", a), ("c", c)) if v is not None},
"species_map": dict(species_map or {}),
}
provenance = dict(structure.provenance)
provenance["builder"] = builder
return structure.model_copy(update={"provenance": provenance})
[docs]
def markdown_page() -> str:
"""The manual page listing every template, generated from the data above."""
def cell(t: Template) -> str:
parts = [f"a = {t.a:g}"]
parts += [f"{k} = {v:g}" for k, v in (("b", t.b), ("c", t.c), ("β", t.beta)) if v]
return ", ".join(parts)
lines = [
"# Crystal templates",
"",
"<!-- Generated by scripts/generate_template_docs.py from",
" src/vibesketcher/document/crystal_templates.py; do not edit by hand. -->",
"",
"Each template is a space group, a conventional cell in angstrom and an",
"asymmetric unit. `vibe-sketcher solid crystal --template NAME` expands it",
"(see [Crystal templates](solid_state_inputs.md#crystal-templates)). The",
"lattice constants are representative room-temperature experimental values.",
"Check the values your study depends on against the primary literature.",
"",
]
references: dict[str, int] = {}
for category in dict.fromkeys(t.category for t in _TEMPLATES):
lines += [f"## {category.capitalize()}", "",
"| Name | Crystal | Space group | Cell (Å, °) | Z | Surfaces | Ref. |",
"|---|---|---|---|---|---|---|"] # fmt: skip
for t in _TEMPLATES:
if t.category != category:
continue
number = references.setdefault(t.reference, len(references) + 1)
surfaces = ", ".join("(" + "".join(str(i) for i in s.miller) + ")" for s in t.surfaces)
lines.append(
f"| `{t.name}` | {t.title} | `{t.space_group}` | {cell(t)} | {t.z} | {surfaces} | "
f"[{number}] |"
)
lines.append("")
lines += ["## References", ""]
lines += [f"{n}. {ref}" for ref, n in references.items()]
return "\n".join(lines) + "\n"