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
|
|
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,
}
|
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
|
|
k |
int
|
|
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)
|
A crystal form with scale.
@dataclass
class CrystalForm:
miller: MillerIndex
scale: float = 1.0
name: Optional[str] = None
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.
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 |
|
|
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 |
|
|
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
|