Source code for optmat.materials

# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: MIT

"""The materials the curated records name.

Every dataset belongs to one material: a gas, a glass or a crystal. The core knows only
what a material *is* (its id, name, kind, formula and optical symmetry), read from
``data/materials.toml``; what a medium needs beyond that (a gas's electron count and
equation of state, a solid's models) lives in that medium's subpackage (TC-318).

A material's id is the directory of its extracted files,
``data/values/<family>/<id>/``. An anisotropic crystal lists the axes a dataset's
``parameters.axis`` may take (``o`` and ``e`` for a uniaxial crystal).
"""

import functools
import tomllib
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Literal

from optmat.exceptions import OptmatError

__all__ = [
    "KINDS",
    "MATERIALS_FILE",
    "Material",
    "as_material",
    "material",
    "materials",
]

MATERIALS_FILE: Final = Path(__file__).parent / "data" / "materials.toml"
"""The registry of materials."""

KINDS: Final[tuple[str, ...]] = ("gas", "ion", "glass", "crystal", "metal")
"""The kinds of material."""

_AXES: Final[dict[str, int]] = {"isotropic": 0, "uniaxial": 2, "biaxial": 3}


[docs] @dataclass(frozen=True) class Material: """What a material is. Attributes ---------- id : str The id records use, and the directory of the material's files. name : str A descriptive name. kind : {"gas", "ion", "glass", "crystal", "metal"} The kind of medium; an ``ion`` is a gas-phase ion, whose data (its polarizability, for the ionisation rates) never enter a gas's models. A ``metal`` is a solid whose index is complex throughout (TC-543). formula : str The chemical formula, or an empty string (a multicomponent glass). optics : {"isotropic", "uniaxial", "biaxial"} The optical symmetry. axes : tuple of str The axes a dataset's ``parameters.axis`` may name; empty when isotropic. aliases : tuple of str Other names that :func:`as_material` accepts. """ id: str name: str kind: Literal["gas", "ion", "glass", "crystal", "metal"] formula: str optics: Literal["isotropic", "uniaxial", "biaxial"] axes: tuple[str, ...] aliases: tuple[str, ...] def __post_init__(self) -> None: """Check the kind, the symmetry and the axes. Raises ------ OptmatError If the kind or symmetry is unknown, or the axes do not match the symmetry. """ if self.kind not in KINDS: msg = f"material {self.id}: unknown kind {self.kind!r}" raise OptmatError(msg) if self.optics not in _AXES: msg = f"material {self.id}: unknown optics {self.optics!r}" raise OptmatError(msg) if len(self.axes) != _AXES[self.optics]: msg = f"material {self.id}: a {self.optics} material has " msg += f"{_AXES[self.optics]} axes, not {len(self.axes)}" raise OptmatError(msg) @property def is_solid(self) -> bool: """Whether the material is a glass, a crystal or a metal. Returns ------- bool True for a glass, a crystal or a metal. """ return self.kind in {"glass", "crystal", "metal"}
def _material(material_id: str, entry: Mapping[str, object]) -> Material: """Build one material from its registry entry. Parameters ---------- material_id : str The id (the entry's table name). entry : mapping The entry. Returns ------- Material The material. Raises ------ OptmatError If the entry has unknown keys. """ unknown = set(entry) - {"name", "kind", "formula", "optics", "axes", "aliases"} if unknown: msg = f"material {material_id}: unknown keys {sorted(unknown)}" raise OptmatError(msg) axes = entry.get("axes", []) aliases = entry.get("aliases", []) return Material( id=material_id, name=str(entry["name"]), kind=str(entry["kind"]), # pyright: ignore[reportArgumentType] - checked formula=str( entry.get("formula", material_id if entry["kind"] in {"gas", "ion"} else "") ), optics=str(entry.get("optics", "isotropic")), # pyright: ignore[reportArgumentType] axes=tuple(str(a) for a in axes), # pyright: ignore[reportGeneralTypeIssues] aliases=tuple(str(a) for a in aliases), # pyright: ignore[reportGeneralTypeIssues] )
[docs] @functools.cache def materials() -> dict[str, Material]: """Return every registered material, by id. Returns ------- dict of str to Material The materials, in registry order. Examples -------- >>> materials()["SiO2"].kind 'glass' """ with MATERIALS_FILE.open("rb") as handle: registry = tomllib.load(handle) return {key: _material(key, value) for key, value in registry.items()}
@functools.cache def _names() -> dict[str, str]: """Map every id and alias to its id. Returns ------- dict of str to str Names to ids. Raises ------ OptmatError If a name is claimed twice. """ names: dict[str, str] = {} for entry in materials().values(): for name in (entry.id, *entry.aliases): if names.setdefault(name, entry.id) != entry.id: msg = ( f"material name {name!r} is claimed by {names[name]} and {entry.id}" ) raise OptmatError(msg) return names
[docs] def as_material(name: str) -> str: """Return the id of the material ``name`` names. Parameters ---------- name : str An id or an alias, e.g. ``"SiO2"`` or ``"fused silica"``. Returns ------- str The material's id. Raises ------ OptmatError If ``name`` names no registered material. Examples -------- >>> as_material("fused silica") 'SiO2' """ try: return _names()[str(name)] except KeyError as exc: msg = ( f"unknown material {name!r}; it must be registered in {MATERIALS_FILE.name}" ) raise OptmatError(msg) from exc
[docs] def material(name: str) -> Material: """Return the registered material ``name`` names. Parameters ---------- name : str An id or an alias. Returns ------- Material The material. Examples -------- >>> material("sapphire").axes ('o', 'e') """ return materials()[as_material(name)]