asciichem 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/TODO.beyond-formulas/01-crystallography.md +69 -0
- data/TODO.beyond-formulas/02-spectroscopy.md +39 -0
- data/TODO.beyond-formulas/03-compchem.md +31 -0
- data/TODO.beyond-formulas/04-structural.md +48 -0
- data/TODO.beyond-formulas/05-mechanisms.md +40 -0
- data/TODO.beyond-formulas/README.md +69 -0
- data/lib/asciichem/formatter/text.rb +17 -0
- data/lib/asciichem/grammar.rb +23 -1
- data/lib/asciichem/model/crystal.rb +58 -0
- data/lib/asciichem/model.rb +1 -0
- data/lib/asciichem/transform.rb +67 -0
- data/lib/asciichem/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d401ee7215f54961b4126b1da5451a2e71ac316c05e3864d199c5482e28d366
|
|
4
|
+
data.tar.gz: c132a8c376dd7485bf0319f8d971d89cb952c27d9f2466c0f3af4af945b0ca4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab9ee8f2410f46c659cc97426a7272c511e983b601cbf043809c5b657621f0d7a48e79f17c2c4f79707cfebaf265e963077a5f636d8e03c87be2942e4fddc27e
|
|
7
|
+
data.tar.gz: 1cf4d97ef6459a29b9d7f05ddbd8b8b1db0ca512f547cb48badd7ebb5abc42f9c70c98cdade086af9dedf48f2e16c96ddd199097ee90c3e2ba56984e66a91aa7
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 01 — Crystallography: unit cell and space group
|
|
2
|
+
|
|
3
|
+
- **Priority:** P1
|
|
4
|
+
- **Status:** in progress
|
|
5
|
+
|
|
6
|
+
## Motivation
|
|
7
|
+
|
|
8
|
+
Crystallography is the foundation of materials science. CML's
|
|
9
|
+
`<crystal>`, `<scalar>` (for a/b/c/α/β/γ), and `<symmetry>` elements
|
|
10
|
+
capture crystal structures. An ASCII encoding is dramatically more
|
|
11
|
+
readable than XML:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
crystal[NaCl](a=5.64,b=5.64,c=5.64,alpha=90,beta=90,gamma=90,sg=Fm-3m){
|
|
15
|
+
Na@f(0,0,0)
|
|
16
|
+
Cl@f(0.5,0.5,0.5)
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
vs CML's verbose `<crystal><scalar title="a">5.64</scalar>...`.
|
|
21
|
+
|
|
22
|
+
## Syntax design
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
crystal[<name>](
|
|
26
|
+
a=<float>, b=<float>, c=<float>,
|
|
27
|
+
alpha=<float>, beta=<float>, gamma=<float>,
|
|
28
|
+
sg=<spacegroup>
|
|
29
|
+
) {
|
|
30
|
+
<atoms with @f(x,y,z) fractional coords>
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `crystal` is a keyword (not an element symbol — parsed before
|
|
35
|
+
molecule).
|
|
36
|
+
- Square brackets carry the optional crystal name/title.
|
|
37
|
+
- Parentheses carry unit cell parameters as `key=value` pairs.
|
|
38
|
+
- Curly braces carry the asymmetric unit atoms with fractional coords.
|
|
39
|
+
- `sg` is the Hermann-Mauguin space group symbol.
|
|
40
|
+
|
|
41
|
+
## Model
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class Crystal < Node
|
|
45
|
+
attr_accessor :name, :a, :b, :c, :alpha, :beta, :gamma,
|
|
46
|
+
:spacegroup, :atoms
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Atoms inside a crystal use `@f(x,y,z)` fractional coordinates
|
|
51
|
+
(already implemented in v0.3.4).
|
|
52
|
+
|
|
53
|
+
## CML mapping
|
|
54
|
+
|
|
55
|
+
- `Crystal` → `<crystal>` with child `<scalar>` elements for cell
|
|
56
|
+
parameters and `<symmetry>` for space group.
|
|
57
|
+
- Atoms with fractional coords → `<atom xFract="..." yFract="..."`
|
|
58
|
+
zFract="..."/> inside `<atomArray>`.
|
|
59
|
+
- The crystal is a child of `<molecule>` (CML convention: crystal
|
|
60
|
+
structures live inside molecules).
|
|
61
|
+
|
|
62
|
+
## Acceptance criteria
|
|
63
|
+
|
|
64
|
+
- [ ] `crystal[NaCl](...)` parses to a Crystal model node
|
|
65
|
+
- [ ] Text round-trip: `parse(s).to_text == s`
|
|
66
|
+
- [ ] CML round-trip: `parse(s).to_cml → parse → .to_text == s`
|
|
67
|
+
- [ ] Cell parameters carry through as CML `<scalar>` elements
|
|
68
|
+
- [ ] Space group carries as CML `<symmetry>` element
|
|
69
|
+
- [ ] Fractional coordinates on atoms (already supported)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 02 — Spectroscopy: NMR / IR / MS spectra
|
|
2
|
+
|
|
3
|
+
- **Priority:** P2
|
|
4
|
+
- **Status:** pending
|
|
5
|
+
|
|
6
|
+
## Syntax
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
spectrum[nmr](type=1H,solvent=CDCl3,freq=400){
|
|
10
|
+
1.2: 3H s "CH3"
|
|
11
|
+
3.5: 2H q J=7 "CH2"
|
|
12
|
+
7.2: 5H m "C6H5"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
spectrum[ir]{
|
|
16
|
+
3300: broad "O-H stretch"
|
|
17
|
+
1700: strong "C=O stretch"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
spectrum[ms]{
|
|
21
|
+
18: 100% "M+"
|
|
22
|
+
17: 23% "M-1"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Model
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class Spectrum < Node
|
|
30
|
+
attr_accessor :type, :params, :peaks
|
|
31
|
+
# peaks: [{position:, intensity:, multiplicity:, assignment:}, ...]
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## CML mapping
|
|
36
|
+
|
|
37
|
+
Maps to `<spectrum>` with `<peakList>` containing `<peak>` children.
|
|
38
|
+
CML peak attributes: `xValue`, `xUnits`, `yValue`, `yUnits`,
|
|
39
|
+
`atomRefs`, `title`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 03 — Computational chemistry: QC calculation results
|
|
2
|
+
|
|
3
|
+
- **Priority:** P2
|
|
4
|
+
- **Status:** pending
|
|
5
|
+
|
|
6
|
+
## Syntax
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
calc(dft, b3lyp/6-31G*){
|
|
10
|
+
energy: -234.5 Hartree
|
|
11
|
+
dipole: [0.1, 0.2, 0.3] Debye
|
|
12
|
+
homo: -0.32 Hartree
|
|
13
|
+
lumo: 0.15 Hartree
|
|
14
|
+
gap: 0.47 Hartree
|
|
15
|
+
zpe: 0.05 Hartree
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Model
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class Calculation < Node
|
|
23
|
+
attr_accessor :method, :basis, :properties
|
|
24
|
+
# properties: [{title:, value:, units:}, ...]
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## CML mapping
|
|
29
|
+
|
|
30
|
+
Maps to `<module convention="convention:compchem">` containing
|
|
31
|
+
`<parameterList>` (method/basis) and `<propertyList>` (results).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 04 — Structural extensions: Z-Matrix and fragments
|
|
2
|
+
|
|
3
|
+
- **Priority:** P3
|
|
4
|
+
- **Status:** pending
|
|
5
|
+
|
|
6
|
+
## Syntax
|
|
7
|
+
|
|
8
|
+
### Z-Matrix
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
zmatrix{
|
|
12
|
+
C1
|
|
13
|
+
H2 C1 1.09
|
|
14
|
+
H3 C1 1.09 H2 109.5
|
|
15
|
+
H4 C1 1.09 H2 109.5 H3 120.0
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each line: atom, reference atom, bond length, [reference atom, angle,
|
|
20
|
+
[reference atom, dihedral]].
|
|
21
|
+
|
|
22
|
+
### Fragments
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
fragment(phenyl){
|
|
26
|
+
C1-C2=C3-C4=C5-C6=1
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Named structural fragments that can be referenced by other molecules.
|
|
31
|
+
|
|
32
|
+
## Model
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
class ZMatrix < Node
|
|
36
|
+
attr_accessor :rows
|
|
37
|
+
# rows: [{atom:, ref1:, r12:, ref2:, angle:, ref3:, dihedral:}, ...]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Fragment < Node
|
|
41
|
+
attr_accessor :name, :molecule
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CML mapping
|
|
46
|
+
|
|
47
|
+
ZMatrix → `<zMatrix>` with rows as `<atom>` references.
|
|
48
|
+
Fragment → `<fragment>` containing a `<molecule>`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 05 — Reaction mechanisms and spectators
|
|
2
|
+
|
|
3
|
+
- **Priority:** P3
|
|
4
|
+
- **Status:** pending
|
|
5
|
+
|
|
6
|
+
## Syntax
|
|
7
|
+
|
|
8
|
+
### Reaction mechanism
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
mechanism{
|
|
12
|
+
step1: Cl- + CH3Br -> [TS: Cl...C...Br] -> ClCH3 + Br-
|
|
13
|
+
step2: ClCH3 + Na+ -> CH3Cl + Na+
|
|
14
|
+
spectator: Na+
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Spectator ions
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Ag+(aq) + Cl-(aq) -> AgCl(s) | spectator: Na+(aq) NO3-(aq)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Inline notation: `| spectator: <atoms>` after the reaction.
|
|
25
|
+
|
|
26
|
+
## Model
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class Mechanism < Node
|
|
30
|
+
attr_accessor :steps, :spectators, :reactive_centre
|
|
31
|
+
# steps: [{label:, transition_state:, reaction:}, ...]
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## CML mapping
|
|
36
|
+
|
|
37
|
+
Mechanism → `<reactionScheme>` with `<reactionStepList>`
|
|
38
|
+
containing `<reactionStep>` children.
|
|
39
|
+
Spectators → `<spectatorList>` with `<spectator>` children.
|
|
40
|
+
ReactiveCentre → `<reactiveCentre>` with atom references.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# TODO.beyond-formulas index
|
|
2
|
+
|
|
3
|
+
Expanding AsciiChem beyond molecular formulas into the full CML
|
|
4
|
+
domain. Each workstream adds a new construct type with grammar,
|
|
5
|
+
model, formatter, and CML round-trip support.
|
|
6
|
+
|
|
7
|
+
## Phase 1: Crystallography
|
|
8
|
+
|
|
9
|
+
| # | Title | Status |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 01 | [Crystallography — unit cell and space group](01-crystallography.md) | **in progress** |
|
|
12
|
+
|
|
13
|
+
Syntax: `crystal[NaCl](a=5.64,b=5.64,c=5.64,alpha=90,beta=90,gamma=90,sg=Fm-3m){Na@f(0,0,0) Cl@f(0.5,0.5,0.5)}`
|
|
14
|
+
|
|
15
|
+
## Phase 2: Spectroscopy
|
|
16
|
+
|
|
17
|
+
| # | Title | Status |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| 02 | [NMR / IR / MS spectra](02-spectroscopy.md) | pending |
|
|
20
|
+
|
|
21
|
+
Syntax:
|
|
22
|
+
```
|
|
23
|
+
spectrum[nmr](type=1H,solvent=CDCl3){
|
|
24
|
+
1.2: 3H s "CH3"
|
|
25
|
+
7.2: 5H m "ArH"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Phase 3: Computational Chemistry
|
|
30
|
+
|
|
31
|
+
| # | Title | Status |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 03 | [QC calculation results](03-compchem.md) | pending |
|
|
34
|
+
|
|
35
|
+
Syntax: `calc(b3lyp/6-31G*){energy:-234.5 dipole:[0.1,0.2,0.3]}`
|
|
36
|
+
|
|
37
|
+
## Phase 4: Structural Extensions
|
|
38
|
+
|
|
39
|
+
| # | Title | Status |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 04 | [Z-Matrix and fragments](04-structural.md) | pending |
|
|
42
|
+
|
|
43
|
+
Syntax:
|
|
44
|
+
```
|
|
45
|
+
zmatrix{C1; H2 C1 1.09; H3 C1 1.09 H2 109.5}
|
|
46
|
+
fragment(phenyl){C1-C2=C3-C4=C5-C6=1}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Phase 5: Reaction Mechanisms
|
|
50
|
+
|
|
51
|
+
| # | Title | Status |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| 05 | [Mechanisms and spectators](05-mechanisms.md) | pending |
|
|
54
|
+
|
|
55
|
+
Syntax: `mechanism{step: A->[TS:B*]->C; spectator:Na+}`
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
Each domain adds:
|
|
60
|
+
1. A new `Model::*` class (autoload from `lib/asciichem/model/`)
|
|
61
|
+
2. A grammar rule in `grammar.rb`
|
|
62
|
+
3. A transform rule in `transform.rb`
|
|
63
|
+
4. `visit_*` methods on formatters (Text, CML, others as needed)
|
|
64
|
+
5. ModelAdapter mapping to `Chemicalml::Cml::*` wire classes
|
|
65
|
+
6. Specs
|
|
66
|
+
|
|
67
|
+
The OCP principle: each domain is a self-contained construct that
|
|
68
|
+
plugs into the existing `Formula` → `nodes` array. No changes to
|
|
69
|
+
existing model classes.
|
|
@@ -123,6 +123,23 @@ module AsciiChem
|
|
|
123
123
|
%("#{text.content}")
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
def visit_crystal(crystal)
|
|
127
|
+
parts = ["crystal"]
|
|
128
|
+
parts << "[#{crystal.name}]" if crystal.name
|
|
129
|
+
params = []
|
|
130
|
+
params << "a=#{crystal.a}" if crystal.a
|
|
131
|
+
params << "b=#{crystal.b}" if crystal.b
|
|
132
|
+
params << "c=#{crystal.c}" if crystal.c
|
|
133
|
+
params << "alpha=#{crystal.alpha}" if crystal.alpha
|
|
134
|
+
params << "beta=#{crystal.beta}" if crystal.beta
|
|
135
|
+
params << "gamma=#{crystal.gamma}" if crystal.gamma
|
|
136
|
+
params << "sg=#{crystal.spacegroup}" if crystal.spacegroup
|
|
137
|
+
parts << "(#{params.join(',')})" unless params.empty?
|
|
138
|
+
atom_strs = crystal.atoms.map { |a| render_node(a) }
|
|
139
|
+
parts << "{#{atom_strs.join(' ')}}" unless atom_strs.empty?
|
|
140
|
+
parts.join
|
|
141
|
+
end
|
|
142
|
+
|
|
126
143
|
private
|
|
127
144
|
|
|
128
145
|
def render_node(node)
|
data/lib/asciichem/grammar.rb
CHANGED
|
@@ -27,7 +27,29 @@ module AsciiChem
|
|
|
27
27
|
|
|
28
28
|
rule(:nodes) { node >> (spaces? >> node).repeat }
|
|
29
29
|
|
|
30
|
-
rule(:node) { reaction_cascade | reaction | electron_config | annotated_molecule | molecule | embedded_math | text_run.as(:text_run) }
|
|
30
|
+
rule(:node) { reaction_cascade | reaction | electron_config | crystal | annotated_molecule | molecule | embedded_math | text_run.as(:text_run) }
|
|
31
|
+
|
|
32
|
+
# -- crystallography -------------------------------------------------
|
|
33
|
+
|
|
34
|
+
# crystal[Name](a=X,b=Y,...,sg=SG){atoms with @f(x,y,z)}
|
|
35
|
+
rule(:crystal) do
|
|
36
|
+
str('crystal') >>
|
|
37
|
+
crystal_name.maybe >>
|
|
38
|
+
crystal_params.maybe >>
|
|
39
|
+
crystal_body.maybe
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
rule(:crystal_name) do
|
|
43
|
+
str('[') >> (str(']').absent? >> any).repeat.as(:crystal_name) >> str(']')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
rule(:crystal_params) do
|
|
47
|
+
str('(') >> (str(')').absent? >> any).repeat.as(:crystal_params) >> str(')')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
rule(:crystal_body) do
|
|
51
|
+
str('{') >> (str('}').absent? >> any).repeat.as(:crystal_body) >> str('}')
|
|
52
|
+
end
|
|
31
53
|
|
|
32
54
|
# Annotated molecule: a molecule followed by one or more
|
|
33
55
|
# `@key("value")` annotations for CML metadata (names,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Model
|
|
5
|
+
# A crystal structure: unit cell parameters + space group +
|
|
6
|
+
# asymmetric-unit atoms with fractional coordinates.
|
|
7
|
+
#
|
|
8
|
+
# Syntax:
|
|
9
|
+
# crystal[NaCl](a=5.64,b=5.64,c=5.64,alpha=90,beta=90,gamma=90,sg=Fm-3m){
|
|
10
|
+
# Na@f(0,0,0)
|
|
11
|
+
# Cl@f(0.5,0.5,0.5)
|
|
12
|
+
# }
|
|
13
|
+
class Crystal < Node
|
|
14
|
+
attr_accessor :name, :a, :b, :c, :alpha, :beta, :gamma,
|
|
15
|
+
:spacegroup, :atoms
|
|
16
|
+
|
|
17
|
+
def initialize(name: nil, a: nil, b: nil, c: nil,
|
|
18
|
+
alpha: nil, beta: nil, gamma: nil,
|
|
19
|
+
spacegroup: nil, atoms: [])
|
|
20
|
+
@name = name
|
|
21
|
+
@a = a
|
|
22
|
+
@b = b
|
|
23
|
+
@c = c
|
|
24
|
+
@alpha = alpha
|
|
25
|
+
@beta = beta
|
|
26
|
+
@gamma = gamma
|
|
27
|
+
@spacegroup = spacegroup
|
|
28
|
+
@atoms = atoms
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def value_attributes
|
|
32
|
+
{ name: name, a: a, b: b, c: c, alpha: alpha,
|
|
33
|
+
beta: beta, gamma: gamma, spacegroup: spacegroup,
|
|
34
|
+
atoms: atoms }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def children
|
|
38
|
+
atoms
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def diagnostic_label
|
|
42
|
+
"Crystal(#{name || 'unnamed'})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s
|
|
46
|
+
params = []
|
|
47
|
+
params << "a=#{a}" if a
|
|
48
|
+
params << "b=#{b}" if b
|
|
49
|
+
params << "c=#{c}" if c
|
|
50
|
+
params << "alpha=#{alpha}" if alpha
|
|
51
|
+
params << "beta=#{beta}" if beta
|
|
52
|
+
params << "gamma=#{gamma}" if gamma
|
|
53
|
+
params << "sg=#{spacegroup}" if spacegroup
|
|
54
|
+
"crystal[#{name}](#{params.join(',')}){#{atoms.map(&:to_s).join(' ')}}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/asciichem/model.rb
CHANGED
|
@@ -8,6 +8,7 @@ module AsciiChem
|
|
|
8
8
|
module Model
|
|
9
9
|
autoload :Atom, "asciichem/model/atom"
|
|
10
10
|
autoload :Bond, "asciichem/model/bond"
|
|
11
|
+
autoload :Crystal, "asciichem/model/crystal"
|
|
11
12
|
autoload :ElectronConfiguration, "asciichem/model/electron_configuration"
|
|
12
13
|
autoload :EmbeddedMath, "asciichem/model/embedded_math"
|
|
13
14
|
autoload :Formula, "asciichem/model/formula"
|
data/lib/asciichem/transform.rb
CHANGED
|
@@ -146,8 +146,75 @@ module AsciiChem
|
|
|
146
146
|
Model::ElectronConfiguration.new(orbitals: pairs)
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
+
# -- crystals -------------------------------------------------------
|
|
150
|
+
#
|
|
151
|
+
# Grammar captures crystal_name, crystal_params, and crystal_body
|
|
152
|
+
# as optional strings. CrystalBuilder parses them into the model.
|
|
153
|
+
|
|
154
|
+
rule(crystal_name: subtree(:name),
|
|
155
|
+
crystal_params: subtree(:params),
|
|
156
|
+
crystal_body: subtree(:body)) do
|
|
157
|
+
CrystalBuilder.new(name, params, body).build
|
|
158
|
+
end
|
|
159
|
+
|
|
149
160
|
# -- internal helpers ------------------------------------------------
|
|
150
161
|
|
|
162
|
+
# Builds a Crystal from parsed grammar captures. The grammar
|
|
163
|
+
# captures the name, params, and body as raw strings; this class
|
|
164
|
+
# parses them into the model fields.
|
|
165
|
+
class CrystalBuilder
|
|
166
|
+
def initialize(name, params_str, body_str)
|
|
167
|
+
@name = strip_parslet(name)
|
|
168
|
+
@params_str = strip_parslet(params_str)
|
|
169
|
+
@body_str = strip_parslet(body_str)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build
|
|
173
|
+
params = parse_params(@params_str)
|
|
174
|
+
atoms = parse_atoms(@body_str)
|
|
175
|
+
Model::Crystal.new(
|
|
176
|
+
name: @name,
|
|
177
|
+
a: params['a'],
|
|
178
|
+
b: params['b'],
|
|
179
|
+
c: params['c'],
|
|
180
|
+
alpha: params['alpha'],
|
|
181
|
+
beta: params['beta'],
|
|
182
|
+
gamma: params['gamma'],
|
|
183
|
+
spacegroup: params['sg'],
|
|
184
|
+
atoms: atoms
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def strip_parslet(value)
|
|
191
|
+
return nil if value.nil?
|
|
192
|
+
|
|
193
|
+
s = value.to_s.strip
|
|
194
|
+
s.empty? ? nil : s
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def parse_params(str)
|
|
198
|
+
return {} unless str
|
|
199
|
+
|
|
200
|
+
str.split(',').each_with_object({}) do |pair, memo|
|
|
201
|
+
key, val = pair.strip.split('=', 2)
|
|
202
|
+
memo[key] = val&.strip if key
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def parse_atoms(str)
|
|
207
|
+
return [] unless str
|
|
208
|
+
|
|
209
|
+
formula = AsciiChem.parse(str)
|
|
210
|
+
formula.nodes.flat_map do |node|
|
|
211
|
+
next [] unless node.is_a?(Model::Molecule)
|
|
212
|
+
|
|
213
|
+
node.nodes.select { |n| n.is_a?(Model::Atom) }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
151
218
|
# Strips the surrounding `"..."` quotes from a quoted text match.
|
|
152
219
|
# Used by both `text_run` and `group_text_run` rules so the
|
|
153
220
|
# model never carries the delimiters — the formatter re-adds them
|
data/lib/asciichem/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asciichem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -114,6 +114,12 @@ files:
|
|
|
114
114
|
- README.adoc
|
|
115
115
|
- RELEASING.md
|
|
116
116
|
- Rakefile
|
|
117
|
+
- TODO.beyond-formulas/01-crystallography.md
|
|
118
|
+
- TODO.beyond-formulas/02-spectroscopy.md
|
|
119
|
+
- TODO.beyond-formulas/03-compchem.md
|
|
120
|
+
- TODO.beyond-formulas/04-structural.md
|
|
121
|
+
- TODO.beyond-formulas/05-mechanisms.md
|
|
122
|
+
- TODO.beyond-formulas/README.md
|
|
117
123
|
- asciichem.gemspec
|
|
118
124
|
- benchmarks/RESULTS.md
|
|
119
125
|
- benchmarks/benchmark.rb
|
|
@@ -149,6 +155,7 @@ files:
|
|
|
149
155
|
- lib/asciichem/model.rb
|
|
150
156
|
- lib/asciichem/model/atom.rb
|
|
151
157
|
- lib/asciichem/model/bond.rb
|
|
158
|
+
- lib/asciichem/model/crystal.rb
|
|
152
159
|
- lib/asciichem/model/electron_configuration.rb
|
|
153
160
|
- lib/asciichem/model/embedded_math.rb
|
|
154
161
|
- lib/asciichem/model/formula.rb
|