Skip to content

API Reference

Core Functions

parse_cdl

Parse a CDL string into a structured description.

from cdl_parser import parse_cdl

desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")

cdl_parser.parse_cdl(text)

Parse a CDL string to CrystalDescription or AmorphousDescription.

Parameters:

Name Type Description Default
text str

CDL string like "cubic[m3m]:{111}@1.0 + {100}@0.3" or "amorphous[opalescent]:{massive, botryoidal}"

required

Returns:

Type Description
CrystalDescription | AmorphousDescription

CrystalDescription or AmorphousDescription object

Raises:

Type Description
ParseError

If parsing fails due to syntax error

ValidationError

If validation fails (e.g., invalid point group)

Examples:

>>> desc = parse_cdl("cubic[m3m]:{111}")
>>> desc.system
'cubic'
>>> desc.forms[0].miller.as_tuple()
(1, 1, 1)
>>> desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")
>>> len(desc.forms)
2
>>> desc = parse_cdl("trigonal[-3m]:{10-10}@1.0 + {10-11}@0.8")
>>> desc.forms[0].miller.i
-1
Source code in src/cdl_parser/parser.py
def parse_cdl(text: str) -> CrystalDescription | AmorphousDescription:
    """Parse a CDL string to CrystalDescription or AmorphousDescription.

    Args:
        text: CDL string like "cubic[m3m]:{111}@1.0 + {100}@0.3"
             or "amorphous[opalescent]:{massive, botryoidal}"

    Returns:
        CrystalDescription or AmorphousDescription object

    Raises:
        ParseError: If parsing fails due to syntax error
        ValidationError: If validation fails (e.g., invalid point group)

    Examples:
        >>> desc = parse_cdl("cubic[m3m]:{111}")
        >>> desc.system
        'cubic'
        >>> desc.forms[0].miller.as_tuple()
        (1, 1, 1)

        >>> desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")
        >>> len(desc.forms)
        2

        >>> desc = parse_cdl("trigonal[-3m]:{10-10}@1.0 + {10-11}@0.8")
        >>> desc.forms[0].miller.i
        -1
    """
    cleaned, doc_comments = strip_comments(text)
    cleaned = cleaned.strip()
    if not cleaned:
        raise ParseError("Empty CDL string after stripping comments", position=0)

    # Pre-process definitions (@name = expression) and resolve $references
    body_text, raw_definitions = _preprocess_definitions(cleaned)
    body_text = body_text.strip()
    if not body_text:
        raise ParseError("Empty CDL string after extracting definitions", position=0)

    # Parse definition bodies into Definition objects
    definitions = _parse_definition_bodies(raw_definitions) if raw_definitions else None

    lexer = Lexer(body_text)
    tokens = lexer.tokenize()
    parser = Parser(tokens)
    desc = parser.parse()
    desc.doc_comments = doc_comments if doc_comments else None
    desc.definitions = definitions
    return desc

validate_cdl

Validate a CDL string without parsing.

from cdl_parser import validate_cdl

is_valid, error = validate_cdl("cubic[m3m]:{111}")
if not is_valid:
    print(f"Error: {error}")

cdl_parser.validate_cdl(text)

Validate a CDL string.

Parameters:

Name Type Description Default
text str

CDL string to validate

required

Returns:

Type Description
tuple[bool, str | None]

Tuple of (is_valid, error_message)

Examples:

>>> validate_cdl("cubic[m3m]:{111}")
(True, None)
>>> valid, error = validate_cdl("invalid{{{")
>>> valid
False
Source code in src/cdl_parser/parser.py
def validate_cdl(text: str) -> tuple[bool, str | None]:
    """Validate a CDL string.

    Args:
        text: CDL string to validate

    Returns:
        Tuple of (is_valid, error_message)

    Examples:
        >>> validate_cdl("cubic[m3m]:{111}")
        (True, None)

        >>> valid, error = validate_cdl("invalid{{{")
        >>> valid
        False
    """
    try:
        parse_cdl(text)
        return True, None
    except (ParseError, ValidationError) as e:
        return False, str(e)

Data Classes

CrystalDescription

Main output of CDL parsing.

@dataclass
class CrystalDescription:
    system: str                          # Crystal system
    point_group: str                     # Point group symbol
    forms: List[CrystalForm]             # Crystal forms
    modifications: List[Modification]    # Morphological mods
    twin: Optional[TwinSpec]             # Twin specification

cdl_parser.CrystalDescription dataclass

Complete crystal description parsed from CDL.

The main output of CDL parsing, containing all information needed to generate a crystal visualization.

Attributes:

Name Type Description
system str

Crystal system ('cubic', 'hexagonal', etc.)

point_group str

Hermann-Mauguin point group symbol ('m3m', '6/mmm', etc.)

forms list[FormNode]

List of form nodes (CrystalForm or FormGroup)

modifications list[Modification]

List of morphological modifications

twin TwinSpec | None

Optional twin specification

definitions list[Definition] | None

Optional list of named definitions

Examples:

>>> desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")
>>> desc.system
'cubic'
>>> len(desc.forms)
2
Source code in src/cdl_parser/models.py
@dataclass
class CrystalDescription:
    """Complete crystal description parsed from CDL.

    The main output of CDL parsing, containing all information needed
    to generate a crystal visualization.

    Attributes:
        system: Crystal system ('cubic', 'hexagonal', etc.)
        point_group: Hermann-Mauguin point group symbol ('m3m', '6/mmm', etc.)
        forms: List of form nodes (CrystalForm or FormGroup)
        modifications: List of morphological modifications
        twin: Optional twin specification
        definitions: Optional list of named definitions

    Examples:
        >>> desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")
        >>> desc.system
        'cubic'
        >>> len(desc.forms)
        2
    """

    system: str
    point_group: str
    forms: list[FormNode] = field(default_factory=list)
    modifications: list[Modification] = field(default_factory=list)
    twin: TwinSpec | None = None
    phenomenon: PhenomenonSpec | None = None
    doc_comments: list[str] | None = None
    definitions: list[Definition] | None = None

    def flat_forms(self) -> list[CrystalForm]:
        """Get a flat list of all CrystalForm objects (backwards compat).

        Recursively traverses FormGroup nodes to extract all CrystalForm leaves.
        Features from parent FormGroups are merged into child forms.
        """
        result: list[CrystalForm] = []
        for node in self.forms:
            result.extend(_flatten_node(node))
        return result

    def __str__(self) -> str:
        parts = [f"{self.system}[{self.point_group}]"]

        # Definitions
        if self.definitions:
            def_strs = [str(d) for d in self.definitions]
            parts = def_strs + parts

        # Forms (including features)
        form_strs = [str(f) for f in self.forms]
        parts.append(":" + " + ".join(form_strs))

        # Modifications
        if self.modifications:
            mod_strs = [str(m) for m in self.modifications]
            parts.append(" | " + ", ".join(mod_strs))

        # Twin
        if self.twin:
            parts.append(" | " + str(self.twin))

        # Phenomenon
        if self.phenomenon:
            parts.append(" | " + str(self.phenomenon))

        return "".join(parts)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary representation."""
        return {
            "system": self.system,
            "point_group": self.point_group,
            "forms": [_form_node_to_dict(f) for f in self.forms],
            "flat_forms": [
                {
                    "miller": f.miller.as_tuple(),
                    "scale": f.scale,
                    "name": f.name,
                    "label": f.label,
                    "features": [{"name": feat.name, "values": feat.values} for feat in f.features]
                    if f.features
                    else None,
                }
                for f in self.flat_forms()
            ],
            "modifications": [{"type": m.type, "params": m.params} for m in self.modifications],
            "twin": {
                "law": self.twin.law,
                "axis": self.twin.axis,
                "angle": self.twin.angle,
                "twin_type": self.twin.twin_type,
                "count": self.twin.count,
            }
            if self.twin
            else None,
            "phenomenon": {
                "type": self.phenomenon.type,
                "params": self.phenomenon.params,
            }
            if self.phenomenon
            else None,
            "doc_comments": self.doc_comments,
            "definitions": [
                {"name": d.name, "body": [_form_node_to_dict(f) for f in d.body]}
                for d in self.definitions
            ]
            if self.definitions
            else None,
        }

flat_forms()

Get a flat list of all CrystalForm objects (backwards compat).

Recursively traverses FormGroup nodes to extract all CrystalForm leaves. Features from parent FormGroups are merged into child forms.

Source code in src/cdl_parser/models.py
def flat_forms(self) -> list[CrystalForm]:
    """Get a flat list of all CrystalForm objects (backwards compat).

    Recursively traverses FormGroup nodes to extract all CrystalForm leaves.
    Features from parent FormGroups are merged into child forms.
    """
    result: list[CrystalForm] = []
    for node in self.forms:
        result.extend(_flatten_node(node))
    return result

to_dict()

Convert to dictionary representation.

Source code in src/cdl_parser/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary representation."""
    return {
        "system": self.system,
        "point_group": self.point_group,
        "forms": [_form_node_to_dict(f) for f in self.forms],
        "flat_forms": [
            {
                "miller": f.miller.as_tuple(),
                "scale": f.scale,
                "name": f.name,
                "label": f.label,
                "features": [{"name": feat.name, "values": feat.values} for feat in f.features]
                if f.features
                else None,
            }
            for f in self.flat_forms()
        ],
        "modifications": [{"type": m.type, "params": m.params} for m in self.modifications],
        "twin": {
            "law": self.twin.law,
            "axis": self.twin.axis,
            "angle": self.twin.angle,
            "twin_type": self.twin.twin_type,
            "count": self.twin.count,
        }
        if self.twin
        else None,
        "phenomenon": {
            "type": self.phenomenon.type,
            "params": self.phenomenon.params,
        }
        if self.phenomenon
        else None,
        "doc_comments": self.doc_comments,
        "definitions": [
            {"name": d.name, "body": [_form_node_to_dict(f) for f in d.body]}
            for d in self.definitions
        ]
        if self.definitions
        else None,
    }

MillerIndex

Miller index representation.

@dataclass
class MillerIndex:
    h: int
    k: int
    l: int
    i: Optional[int] = None  # For 4-index notation

    def as_tuple(self) -> tuple[int, ...]
    def as_3index(self) -> tuple[int, int, int]

cdl_parser.MillerIndex dataclass

Miller index representation.

Represents crystal face orientations using Miller or Miller-Bravais notation.

Attributes:

Name Type Description
h int

First Miller index

k int

Second Miller index

l int

Third Miller index (fourth in Miller-Bravais)

i int | None

Third index for Miller-Bravais notation (hexagonal/trigonal) Calculated as -(h+k), only used for 4-index notation

Examples:

>>> MillerIndex(1, 1, 1)  # Octahedron face
>>> MillerIndex(1, 0, 0)  # Cube face
>>> MillerIndex(1, 0, 1, i=-1)  # Hexagonal {10-11}
Source code in src/cdl_parser/models.py
@dataclass
class MillerIndex:
    """Miller index representation.

    Represents crystal face orientations using Miller or Miller-Bravais notation.

    Attributes:
        h: First Miller index
        k: Second Miller index
        l: Third Miller index (fourth in Miller-Bravais)
        i: Third index for Miller-Bravais notation (hexagonal/trigonal)
           Calculated as -(h+k), only used for 4-index notation

    Examples:
        >>> MillerIndex(1, 1, 1)  # Octahedron face
        >>> MillerIndex(1, 0, 0)  # Cube face
        >>> MillerIndex(1, 0, 1, i=-1)  # Hexagonal {10-11}
    """

    h: int
    k: int
    l: int  # noqa: E741 - standard crystallographic notation
    i: int | None = None  # For Miller-Bravais (hexagonal/trigonal)

    def __post_init__(self) -> None:
        # Validate Miller-Bravais constraint: i = -(h+k)
        if self.i is not None:
            expected_i = -(self.h + self.k)
            if self.i != expected_i:
                raise ValueError(
                    f"Invalid Miller-Bravais index: i should be {expected_i}, got {self.i}"
                )

    def as_tuple(self) -> tuple[int, ...]:
        """Return as tuple (3 or 4 elements)."""
        if self.i is not None:
            return (self.h, self.k, self.i, self.l)
        return (self.h, self.k, self.l)

    def as_3index(self) -> tuple[int, int, int]:
        """Return as 3-index tuple (for calculations)."""
        return (self.h, self.k, self.l)

    def __str__(self) -> str:
        if self.i is not None:
            return f"{{{self.h}{self.k}{self.i}{self.l}}}"
        return f"{{{self.h}{self.k}{self.l}}}"

    def __repr__(self) -> str:
        if self.i is not None:
            return f"MillerIndex({self.h}, {self.k}, {self.l}, i={self.i})"
        return f"MillerIndex({self.h}, {self.k}, {self.l})"

as_3index()

Return as 3-index tuple (for calculations).

Source code in src/cdl_parser/models.py
def as_3index(self) -> tuple[int, int, int]:
    """Return as 3-index tuple (for calculations)."""
    return (self.h, self.k, self.l)

as_tuple()

Return as tuple (3 or 4 elements).

Source code in src/cdl_parser/models.py
def as_tuple(self) -> tuple[int, ...]:
    """Return as tuple (3 or 4 elements)."""
    if self.i is not None:
        return (self.h, self.k, self.i, self.l)
    return (self.h, self.k, self.l)

CrystalForm

A crystal form with scale.

@dataclass
class CrystalForm:
    miller: MillerIndex
    scale: float = 1.0
    name: Optional[str] = None

cdl_parser.CrystalForm dataclass

A crystal form with Miller index and scale.

Represents a single crystal form (set of symmetry-equivalent faces) with an optional distance scale for truncation.

Attributes:

Name Type Description
miller MillerIndex

The Miller index defining the form

scale float

Distance scale (default 1.0, larger = more truncated)

name str | None

Original name if using named form (e.g., 'octahedron')

features list[Feature] | None

Optional list of feature annotations

label str | None

Optional label for the form (e.g., 'prism' in prism:{10-10})

Examples:

>>> CrystalForm(MillerIndex(1, 1, 1), scale=1.0)
>>> CrystalForm(MillerIndex(1, 0, 0), scale=1.3, name='cube')
Source code in src/cdl_parser/models.py
@dataclass
class CrystalForm:
    """A crystal form with Miller index and scale.

    Represents a single crystal form (set of symmetry-equivalent faces)
    with an optional distance scale for truncation.

    Attributes:
        miller: The Miller index defining the form
        scale: Distance scale (default 1.0, larger = more truncated)
        name: Original name if using named form (e.g., 'octahedron')
        features: Optional list of feature annotations
        label: Optional label for the form (e.g., 'prism' in prism:{10-10})

    Examples:
        >>> CrystalForm(MillerIndex(1, 1, 1), scale=1.0)
        >>> CrystalForm(MillerIndex(1, 0, 0), scale=1.3, name='cube')
    """

    miller: MillerIndex
    scale: float = 1.0
    name: str | None = None  # Original name if using named form
    features: list[Feature] | None = None  # Per-form features [phantom:3]
    label: str | None = None  # Form label (v1.3)

    def __str__(self) -> str:
        s = str(self.miller)
        if self.name:
            s = f"{self.name}={s}"
        if self.label:
            s = f"{self.label}:{s}"
        if self.scale != 1.0:
            s += f"@{self.scale}"
        if self.features:
            feat_str = ", ".join(str(f) for f in self.features)
            s += f"[{feat_str}]"
        return s

Constants

from cdl_parser import (
    CRYSTAL_SYSTEMS,      # Set of system names
    POINT_GROUPS,         # Dict[system, Set[groups]]
    DEFAULT_POINT_GROUPS, # Dict[system, default_group]
    NAMED_FORMS,          # Dict[name, (h, k, l)]
    TWIN_LAWS,            # Set of twin law names
)

CRYSTAL_SYSTEMS

Set of valid crystal system names:

  • cubic
  • tetragonal
  • orthorhombic
  • hexagonal
  • trigonal
  • monoclinic
  • triclinic

POINT_GROUPS

Dictionary mapping each crystal system to its valid point groups.

DEFAULT_POINT_GROUPS

Dictionary mapping each crystal system to its default (highest symmetry) point group.

NAMED_FORMS

Dictionary mapping common form names to their Miller indices:

Name Miller Index
octahedron {111}
cube {100}
dodecahedron {110}
trapezohedron {211}
prism {10-10}
basal {0001}
rhombohedron {10-11}

TWIN_LAWS

Set of recognized twin law names:

  • spinel - Spinel law (111) twin
  • brazil - Brazil law quartz twin
  • japan - Japan law quartz twin
  • fluorite - Fluorite interpenetration twin
  • iron_cross - Iron cross pyrite twin

Exceptions

ParseError

Raised when parsing fails due to syntax errors.

from cdl_parser import ParseError

try:
    desc = parse_cdl("invalid{{{")
except ParseError as e:
    print(f"Syntax error at position {e.position}: {e.message}")

cdl_parser.ParseError

Bases: CDLError

Raised when CDL parsing fails.

Attributes:

Name Type Description
message

Human-readable error description

position

Character position in the input string where error occurred

line

Optional line number (for multi-line inputs)

column

Optional column number

Source code in src/cdl_parser/exceptions.py
class ParseError(CDLError):
    """Raised when CDL parsing fails.

    Attributes:
        message: Human-readable error description
        position: Character position in the input string where error occurred
        line: Optional line number (for multi-line inputs)
        column: Optional column number
    """

    def __init__(self, message: str, position: int = -1, line: int = -1, column: int = -1):
        self.message = message
        self.position = position
        self.line = line
        self.column = column
        super().__init__(self._format_message())

    def _format_message(self) -> str:
        parts = [self.message]
        if self.position >= 0:
            parts.append(f"at position {self.position}")
        if self.line >= 0:
            parts.append(f"(line {self.line}")
            if self.column >= 0:
                parts.append(f", column {self.column})")
            else:
                parts.append(")")
        return " ".join(parts)

ValidationError

Raised when validation fails due to invalid values.

from cdl_parser import ValidationError

try:
    desc = parse_cdl("invalid[xxx]:{111}")
except ValidationError as e:
    print(f"Validation error: {e.message}")

cdl_parser.ValidationError

Bases: CDLError

Raised when CDL validation fails.

This is raised when the CDL syntax is correct but the content is semantically invalid (e.g., invalid point group for system).

Attributes:

Name Type Description
message

Human-readable error description

field

The field or component that failed validation

value

The invalid value

Source code in src/cdl_parser/exceptions.py
class ValidationError(CDLError):
    """Raised when CDL validation fails.

    This is raised when the CDL syntax is correct but the content
    is semantically invalid (e.g., invalid point group for system).

    Attributes:
        message: Human-readable error description
        field: The field or component that failed validation
        value: The invalid value
    """

    def __init__(self, message: str, field: str = "", value: str = ""):
        self.message = message
        self.field = field
        self.value = value
        super().__init__(self._format_message())

    def _format_message(self) -> str:
        if self.field and self.value:
            return f"{self.message}: {self.field}='{self.value}'"
        elif self.field:
            return f"{self.message}: {self.field}"
        return self.message