Skip to content

Commit cb5a65b

Browse files
committed
feat!: add amorphous, nested growth, and aggregate syntax (CDL v2.0)
- Add amorphous system type for non-crystalline materials (opal, glass, pearl) - Add nested growth expressions for phantom and zoned crystals - Add aggregate syntax for clusters, geodes, and massive habits - Add group-level twin specifications - Bump version to 2.0.0 (breaking: CrystalDescription model changes) Refs: CDL v2.0 Phase 4
1 parent 5100138 commit cb5a65b

8 files changed

Lines changed: 1099 additions & 38 deletions

File tree

README.md

Lines changed: 160 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Python](https://img.shields.io/pypi/pyversions/cdl-parser.svg)](https://pypi.org/project/cdl-parser/)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66

7-
**Crystal Description Language (CDL) Parser** - A Python library for parsing compact string notation describing crystal morphology for gemmological and mineralogical visualization.
7+
**Crystal Description Language (CDL) Parser** - A Python library for parsing compact string notation describing crystal morphology for gemmological and mineralogical visualization. CDL v2.0 adds support for amorphous materials, nested growth, and crystal aggregates.
88

99
Part of the [Gemmology Project](https://gemmology.dev).
1010

@@ -31,19 +31,40 @@ print(len(desc.forms)) # 2
3131
# Parse with twin specification
3232
desc = parse_cdl("cubic[m3m]:{111} | twin(spinel)")
3333
print(desc.twin.law) # 'spinel'
34+
35+
# Parse an amorphous material (v2.0)
36+
desc = parse_cdl("amorphous[opalescent]:{botryoidal}")
37+
print(desc.system) # 'amorphous'
38+
print(desc.subtype) # 'opalescent'
39+
40+
# Parse nested growth (v2.0)
41+
desc = parse_cdl("trigonal[32]:({10-10}@1.0 + {10-11}@0.8) > ({10-10}@0.5 + {10-11}@0.4)")
42+
43+
# Parse an aggregate (v2.0)
44+
desc = parse_cdl("trigonal[32]:{10-10}@1.0 + {10-11}@0.8 ~ cluster[12]")
3445
```
3546

3647
## CDL Specification
3748

3849
### Syntax Overview
3950

4051
```
52+
# Crystalline materials
4153
system[point_group]:{form}@scale + {form}@scale | modification | twin(law)
54+
55+
# Amorphous materials (v2.0)
56+
amorphous[subtype]:{shape1, shape2}[features] | phenomenon[type]
57+
58+
# Nested growth (v2.0)
59+
system[point_group]:(forms) > (overgrowth_forms)
60+
61+
# Aggregates (v2.0)
62+
system[point_group]:forms ~ arrangement[count]
4263
```
4364

4465
### Crystal Systems
4566

46-
All 7 crystal systems are supported with their standard point groups:
67+
All 7 crystal systems are supported with their standard point groups, plus amorphous materials:
4768

4869
| System | Default Point Group | All Point Groups |
4970
|--------|---------------------|------------------|
@@ -54,6 +75,59 @@ All 7 crystal systems are supported with their standard point groups:
5475
| orthorhombic | mmm | mmm, 222, mm2 |
5576
| monoclinic | 2/m | 2/m, m, 2 |
5677
| triclinic | -1 | -1, 1 |
78+
| amorphous | none | (n/a) |
79+
80+
### Amorphous Materials (v2.0)
81+
82+
Materials without crystalline structure use the `amorphous` keyword instead of a crystal system:
83+
84+
```python
85+
# Syntax: amorphous[subtype]:{shape1, shape2}[features]
86+
"amorphous[opalescent]:{botryoidal}"
87+
"amorphous[cryptocrystalline]:{massive, nodular}[colour:blue]"
88+
"amorphous[waxy]:{mammillary}[banding:concentric]"
89+
```
90+
91+
**Subtypes:** `opalescent`, `glassy`, `waxy`, `resinous`, `cryptocrystalline`
92+
93+
**Shapes:** `massive`, `botryoidal`, `reniform`, `stalactitic`, `mammillary`, `nodular`, `conchoidal`
94+
95+
### Nested Growth (v2.0)
96+
97+
The `>` operator describes overgrowth relationships (base > overgrowth), where an outer crystal grows on an inner one. Right-associative: `a > b > c` = `a > (b > c)`.
98+
99+
```python
100+
# Scepter quartz — enlarged head on thin stem
101+
"trigonal[32]:({10-10}@1.0 + {10-11}@0.8) > ({10-10}@0.5 + {10-11}@0.4)"
102+
103+
# Diamond phantom
104+
"cubic[m3m]:{111}@1.0 > {111}@1.0"
105+
```
106+
107+
### Aggregates (v2.0)
108+
109+
The `~` operator describes crystal aggregates — multiple copies of a form arranged in a spatial pattern:
110+
111+
```python
112+
# Syntax: forms ~ arrangement[count]
113+
"trigonal[32]:{10-10}@1.0 + {10-11}@0.8 ~ cluster[12]" # Quartz cluster
114+
"trigonal[32]:{10-10}@1.0 + {10-11}@0.8 ~ druse[50]" # Amethyst geode
115+
"cubic[m3m]:{100} ~ cluster[5]" # Pyrite cluster
116+
"trigonal[-3m]:rhombohedron ~ parallel[3]" # Calcite parallel growth
117+
```
118+
119+
**Arrangements:** `parallel`, `random`, `radial`, `epitaxial`, `druse`, `cluster`
120+
121+
**Orientations** (optional): `aligned`, `random`, `planar`, `spherical`
122+
123+
### Group-Level Twins (v2.0)
124+
125+
Twin specifications can be applied to form groups, allowing twinning of composite morphologies:
126+
127+
```python
128+
# Twin a group of forms
129+
"cubic[m3m]:({111}@1.0 + {100}@0.3) | twin(spinel)"
130+
```
57131

58132
### Miller Indices
59133

@@ -133,20 +207,39 @@ Named twin laws for common crystal twins:
133207

134208
# Fluorite cube
135209
"cubic[m3m]:{100}"
210+
211+
# Opal — amorphous with play of color (v2.0)
212+
"amorphous[opalescent]:{botryoidal} | phenomenon[play_of_color:intense]"
213+
214+
# Turquoise — cryptocrystalline massive (v2.0)
215+
"amorphous[cryptocrystalline]:{massive, nodular}[colour:blue]"
216+
217+
# Scepter quartz — nested growth (v2.0)
218+
"trigonal[32]:({10-10}@1.0 + {10-11}@0.8) > ({10-10}@0.5 + {10-11}@0.4)"
219+
220+
# Quartz cluster aggregate (v2.0)
221+
"trigonal[32]:{10-10}@1.0 + {10-11}@0.8 ~ cluster[12]"
222+
223+
# Amethyst geode druse (v2.0)
224+
"trigonal[32]:{10-10}@1.0 + {10-11}@0.8 ~ druse[50]"
136225
```
137226

138227
## API Reference
139228

140229
### Core Functions
141230

142-
#### `parse_cdl(text: str) -> CrystalDescription`
231+
#### `parse_cdl(text: str) -> CrystalDescription | AmorphousDescription`
143232

144-
Parse a CDL string into a structured description.
233+
Parse a CDL string into a structured description. Returns `CrystalDescription` for crystalline materials or `AmorphousDescription` for amorphous materials.
145234

146235
```python
147236
from cdl_parser import parse_cdl
148237

238+
# Crystalline — returns CrystalDescription
149239
desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")
240+
241+
# Amorphous — returns AmorphousDescription (v2.0)
242+
desc = parse_cdl("amorphous[opalescent]:{botryoidal}")
150243
```
151244

152245
#### `validate_cdl(text: str) -> tuple[bool, str | None]`
@@ -165,16 +258,66 @@ if not is_valid:
165258

166259
#### `CrystalDescription`
167260

168-
Main output of CDL parsing.
261+
Main output of CDL parsing for crystalline materials.
169262

170263
```python
171264
@dataclass
172265
class CrystalDescription:
173266
system: str # Crystal system
174267
point_group: str # Point group symbol
175-
forms: List[CrystalForm] # Crystal forms
176-
modifications: List[Modification] # Morphological mods
177-
twin: Optional[TwinSpec] # Twin specification
268+
forms: list[FormNode] # Form tree (CrystalForm | FormGroup | NestedGrowth | AggregateSpec)
269+
modifications: list[Modification] # Morphological mods
270+
twin: TwinSpec | None # Twin specification
271+
phenomenon: PhenomenonSpec | None # Optical phenomenon
272+
doc_comments: list[str] | None # Doc comments (#!)
273+
definitions: list[Definition] | None # Named definitions (@name = ...)
274+
275+
def flat_forms(self) -> list[CrystalForm]:
276+
"""Flatten form tree into a list of CrystalForm leaves."""
277+
```
278+
279+
#### `AmorphousDescription` (v2.0)
280+
281+
Output of CDL parsing for amorphous (non-crystalline) materials.
282+
283+
```python
284+
@dataclass
285+
class AmorphousDescription:
286+
subtype: str # 'opalescent', 'glassy', 'waxy', etc.
287+
shapes: list[str] # 'massive', 'botryoidal', etc.
288+
features: list[Feature] | None # Feature annotations
289+
phenomenon: PhenomenonSpec | None # Optical phenomenon
290+
doc_comments: list[str] | None # Doc comments (#!)
291+
definitions: list[Definition] | None # Named definitions
292+
293+
@property
294+
def system(self) -> str: # Always returns 'amorphous'
295+
```
296+
297+
#### `NestedGrowth` (v2.0)
298+
299+
Represents a base crystal with an overgrowth (the `>` operator).
300+
301+
```python
302+
@dataclass
303+
class NestedGrowth:
304+
base: FormNode # Base form node
305+
overgrowth: FormNode # Overgrowth form node
306+
```
307+
308+
#### `AggregateSpec` (v2.0)
309+
310+
Represents a crystal aggregate (the `~` operator).
311+
312+
```python
313+
@dataclass
314+
class AggregateSpec:
315+
form: FormNode # Form to aggregate
316+
arrangement: str # 'parallel', 'random', 'radial', etc.
317+
count: int # Number of individuals
318+
spacing: str | None # Optional spacing value
319+
orientation: str | None # Optional orientation mode
320+
orientation_param: float | None # Optional orientation parameter
178321
```
179322

180323
#### `MillerIndex`
@@ -209,11 +352,15 @@ class CrystalForm:
209352

210353
```python
211354
from cdl_parser import (
212-
CRYSTAL_SYSTEMS, # Set of system names
213-
POINT_GROUPS, # Dict[system, Set[groups]]
214-
DEFAULT_POINT_GROUPS, # Dict[system, default_group]
215-
NAMED_FORMS, # Dict[name, (h, k, l)]
216-
TWIN_LAWS, # Set of twin law names
355+
CRYSTAL_SYSTEMS, # Set of system names
356+
POINT_GROUPS, # Dict[system, Set[groups]]
357+
DEFAULT_POINT_GROUPS, # Dict[system, default_group]
358+
NAMED_FORMS, # Dict[name, (h, k, l)]
359+
TWIN_LAWS, # Set of twin law names
360+
AMORPHOUS_SUBTYPES, # Set: opalescent, glassy, waxy, resinous, cryptocrystalline
361+
AMORPHOUS_SHAPES, # Set: massive, botryoidal, reniform, stalactitic, ...
362+
AGGREGATE_ARRANGEMENTS, # Set: parallel, random, radial, epitaxial, druse, cluster
363+
AGGREGATE_ORIENTATIONS, # Set: aligned, random, planar, spherical
217364
)
218365
```
219366

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "gemmology-cdl-parser"
3-
version = "1.3.0"
3+
version = "2.0.0"
44
description = "Crystal Description Language (CDL) parser for crystallographic visualization"
55
readme = "README.md"
66
license = { text = "MIT" }

src/cdl_parser/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@
2222
cubic[m3m]:{111} | twin(spinel) # Spinel-law twin
2323
"""
2424

25-
__version__ = "1.3.0"
25+
__version__ = "2.0.0"
2626
__author__ = "Fabian Schuh"
2727
__email__ = "fabian@gemmology.dev"
2828

2929
# Core parsing functions
3030
# Constants
3131
from .constants import (
32+
AGGREGATE_ARRANGEMENTS,
33+
AGGREGATE_ORIENTATIONS,
3234
ALL_POINT_GROUPS,
35+
AMORPHOUS_SHAPES,
36+
AMORPHOUS_SUBTYPES,
3337
CRYSTAL_SYSTEMS,
3438
DEFAULT_POINT_GROUPS,
3539
FEATURE_NAMES,
@@ -46,6 +50,8 @@
4650

4751
# Data classes
4852
from .models import (
53+
AggregateSpec,
54+
AmorphousDescription,
4955
CrystalDescription,
5056
CrystalForm,
5157
Definition,
@@ -54,6 +60,7 @@
5460
FormNode,
5561
MillerIndex,
5662
Modification,
63+
NestedGrowth,
5764
PhenomenonSpec,
5865
TwinSpec,
5966
)
@@ -68,6 +75,8 @@
6875
"parse_cdl",
6976
"validate_cdl",
7077
# Data classes
78+
"AggregateSpec",
79+
"AmorphousDescription",
7180
"CrystalDescription",
7281
"CrystalForm",
7382
"Definition",
@@ -76,14 +85,19 @@
7685
"FormNode",
7786
"MillerIndex",
7887
"Modification",
88+
"NestedGrowth",
7989
"PhenomenonSpec",
8090
"TwinSpec",
8191
# Exceptions
8292
"CDLError",
8393
"ParseError",
8494
"ValidationError",
8595
# Constants
96+
"AGGREGATE_ARRANGEMENTS",
97+
"AGGREGATE_ORIENTATIONS",
8698
"ALL_POINT_GROUPS",
99+
"AMORPHOUS_SHAPES",
100+
"AMORPHOUS_SUBTYPES",
87101
"CRYSTAL_SYSTEMS",
88102
"DEFAULT_POINT_GROUPS",
89103
"FEATURE_NAMES",

src/cdl_parser/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
POINT_GROUPS,
1616
TWIN_LAWS,
1717
)
18+
from .models import AmorphousDescription
1819
from .parser import parse_cdl, validate_cdl
1920

2021

@@ -105,6 +106,14 @@ def main(args: list[str] | None = None) -> int:
105106
import json
106107

107108
print(json.dumps(desc.to_dict(), indent=2))
109+
elif isinstance(desc, AmorphousDescription):
110+
print("Parsed successfully!")
111+
print(f" System: {desc.system}")
112+
print(f" Subtype: {desc.subtype}")
113+
print(f" Shapes: {', '.join(desc.shapes)}")
114+
if desc.phenomenon:
115+
print(f" Phenomenon: {desc.phenomenon}")
116+
print(f"\nReconstructed: {desc}")
108117
else:
109118
print("Parsed successfully!")
110119
print(f" System: {desc.system}")

src/cdl_parser/constants.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,45 @@
178178
"aventurescence",
179179
"iridescence",
180180
}
181+
182+
# =============================================================================
183+
# Amorphous Constants (CDL v2.0)
184+
# =============================================================================
185+
186+
AMORPHOUS_SUBTYPES: set[str] = {
187+
"opalescent",
188+
"glassy",
189+
"waxy",
190+
"resinous",
191+
"cryptocrystalline",
192+
}
193+
194+
AMORPHOUS_SHAPES: set[str] = {
195+
"massive",
196+
"botryoidal",
197+
"reniform",
198+
"stalactitic",
199+
"mammillary",
200+
"nodular",
201+
"conchoidal",
202+
}
203+
204+
# =============================================================================
205+
# Aggregate Constants (CDL v2.0)
206+
# =============================================================================
207+
208+
AGGREGATE_ARRANGEMENTS: set[str] = {
209+
"parallel",
210+
"random",
211+
"radial",
212+
"epitaxial",
213+
"druse",
214+
"cluster",
215+
}
216+
217+
AGGREGATE_ORIENTATIONS: set[str] = {
218+
"aligned",
219+
"random",
220+
"planar",
221+
"spherical",
222+
}

0 commit comments

Comments
 (0)