asciichem 0.3.3 → 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 +28 -7
- data/lib/asciichem/grammar.rb +54 -8
- data/lib/asciichem/model/atom.rb +14 -3
- data/lib/asciichem/model/crystal.rb +58 -0
- data/lib/asciichem/model.rb +1 -0
- data/lib/asciichem/model_adapter/from_canonical.rb +12 -1
- data/lib/asciichem/model_adapter/to_canonical.rb +12 -1
- data/lib/asciichem/transform.rb +87 -3
- 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.
|
|
@@ -57,15 +57,19 @@ module AsciiChem
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def atom_annotation(atom)
|
|
60
|
-
|
|
60
|
+
parts = []
|
|
61
61
|
if atom.x2 && atom.y2
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
coord = "@(#{format_coord(atom.x2)},#{format_coord(atom.y2)}"
|
|
63
|
+
coord << ",#{format_coord(atom.z2)}" if atom.z2
|
|
64
|
+
parts << "#{coord})"
|
|
65
|
+
end
|
|
66
|
+
parts << "@#{atom.atom_parity}" if atom.atom_parity
|
|
67
|
+
parts << "@m(#{atom.spin_multiplicity})" if atom.spin_multiplicity
|
|
68
|
+
parts << %(@t("#{atom.atom_title}")) if atom.atom_title
|
|
69
|
+
if atom.x_fract && atom.y_fract && atom.z_fract
|
|
70
|
+
parts << "@f(#{format_coord(atom.x_fract)},#{format_coord(atom.y_fract)},#{format_coord(atom.z_fract)})"
|
|
67
71
|
end
|
|
68
|
-
|
|
72
|
+
parts.join
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
def format_coord(value)
|
|
@@ -119,6 +123,23 @@ module AsciiChem
|
|
|
119
123
|
%("#{text.content}")
|
|
120
124
|
end
|
|
121
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
|
+
|
|
122
143
|
private
|
|
123
144
|
|
|
124
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,
|
|
@@ -182,7 +204,7 @@ module AsciiChem
|
|
|
182
204
|
atom_suffix >>
|
|
183
205
|
lewis_radicals.maybe >>
|
|
184
206
|
ring_closures.maybe.as(:ring_closures) >>
|
|
185
|
-
atom_annotation
|
|
207
|
+
atom_annotation).as(:atom)
|
|
186
208
|
end
|
|
187
209
|
|
|
188
210
|
rule(:plain_atom) do
|
|
@@ -191,15 +213,18 @@ module AsciiChem
|
|
|
191
213
|
atom_suffix >>
|
|
192
214
|
lewis_radicals.maybe >>
|
|
193
215
|
ring_closures.maybe.as(:ring_closures) >>
|
|
194
|
-
atom_annotation
|
|
216
|
+
atom_annotation).as(:atom)
|
|
195
217
|
end
|
|
196
218
|
|
|
197
|
-
# Atom annotations:
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
# would require compound syntax (deferred).
|
|
219
|
+
# Atom annotations: each is independently optional via .maybe.
|
|
220
|
+
# Order matters: @(x,y) → @R/@S → @m(N) → @t("...") → @f(x,y,z).
|
|
221
|
+
# Example: C@(10,20)@R@m(2)@t("C1")@f(0.5,0.5,0.5)
|
|
201
222
|
rule(:atom_annotation) do
|
|
202
|
-
coordinate_annotation
|
|
223
|
+
coordinate_annotation.maybe >>
|
|
224
|
+
parity_annotation.maybe >>
|
|
225
|
+
multiplicity_annotation.maybe >>
|
|
226
|
+
atom_title_annotation.maybe >>
|
|
227
|
+
fractional_annotation.maybe
|
|
203
228
|
end
|
|
204
229
|
|
|
205
230
|
rule(:parity_annotation) do
|
|
@@ -214,6 +239,27 @@ module AsciiChem
|
|
|
214
239
|
str(')')
|
|
215
240
|
end
|
|
216
241
|
|
|
242
|
+
# Spin multiplicity: @m(2) for doublet, @m(1) for singlet
|
|
243
|
+
rule(:multiplicity_annotation) do
|
|
244
|
+
str('@m(') >> match('[0-9]').repeat(1).as(:spin_multiplicity) >> str(')')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Atom title/label: @t("C1")
|
|
248
|
+
rule(:atom_title_annotation) do
|
|
249
|
+
str('@t(') >> str('"') >>
|
|
250
|
+
(str('"').absent? >> any).repeat.as(:atom_title) >>
|
|
251
|
+
str('"') >> str(')')
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Fractional coordinates (crystallographic): @f(0.5,0.25,0.75)
|
|
255
|
+
rule(:fractional_annotation) do
|
|
256
|
+
str('@f(') >>
|
|
257
|
+
float_number.as(:x_fract) >> str(',') >>
|
|
258
|
+
float_number.as(:y_fract) >> str(',') >>
|
|
259
|
+
float_number.as(:z_fract) >>
|
|
260
|
+
str(')')
|
|
261
|
+
end
|
|
262
|
+
|
|
217
263
|
rule(:float_number) do
|
|
218
264
|
str('-').maybe >> match('[0-9]').repeat(1) >> (str('.') >> match('[0-9]').repeat(0)).maybe
|
|
219
265
|
end
|
data/lib/asciichem/model/atom.rb
CHANGED
|
@@ -20,13 +20,17 @@ module AsciiChem
|
|
|
20
20
|
:charge, :oxidation_state,
|
|
21
21
|
:lone_pairs, :radical_electrons,
|
|
22
22
|
:ring_closures,
|
|
23
|
-
:x2, :y2, :z2, :atom_parity
|
|
23
|
+
:x2, :y2, :z2, :atom_parity,
|
|
24
|
+
:spin_multiplicity, :atom_title,
|
|
25
|
+
:x_fract, :y_fract, :z_fract
|
|
24
26
|
|
|
25
27
|
def initialize(element:, isotope: nil, subscript: nil,
|
|
26
28
|
superscript: nil, charge: nil, oxidation_state: nil,
|
|
27
29
|
lone_pairs: nil, radical_electrons: nil,
|
|
28
30
|
ring_closures: nil,
|
|
29
|
-
x2: nil, y2: nil, z2: nil, atom_parity: nil
|
|
31
|
+
x2: nil, y2: nil, z2: nil, atom_parity: nil,
|
|
32
|
+
spin_multiplicity: nil, atom_title: nil,
|
|
33
|
+
x_fract: nil, y_fract: nil, z_fract: nil)
|
|
30
34
|
@element = element
|
|
31
35
|
@isotope = isotope
|
|
32
36
|
@subscript = subscript
|
|
@@ -40,6 +44,11 @@ module AsciiChem
|
|
|
40
44
|
@y2 = y2
|
|
41
45
|
@z2 = z2
|
|
42
46
|
@atom_parity = atom_parity
|
|
47
|
+
@spin_multiplicity = spin_multiplicity
|
|
48
|
+
@atom_title = atom_title
|
|
49
|
+
@x_fract = x_fract
|
|
50
|
+
@y_fract = y_fract
|
|
51
|
+
@z_fract = z_fract
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
def value_attributes
|
|
@@ -48,7 +57,9 @@ module AsciiChem
|
|
|
48
57
|
oxidation_state: oxidation_state,
|
|
49
58
|
lone_pairs: lone_pairs, radical_electrons: radical_electrons,
|
|
50
59
|
ring_closures: ring_closures,
|
|
51
|
-
x2: x2, y2: y2, z2: z2, atom_parity: atom_parity
|
|
60
|
+
x2: x2, y2: y2, z2: z2, atom_parity: atom_parity,
|
|
61
|
+
spin_multiplicity: spin_multiplicity, atom_title: atom_title,
|
|
62
|
+
x_fract: x_fract, y_fract: y_fract, z_fract: z_fract }
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
def diagnostic_label
|
|
@@ -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"
|
|
@@ -215,7 +215,10 @@ module AsciiChem
|
|
|
215
215
|
isotope: atom.isotope,
|
|
216
216
|
subscript: subscript_from_count(atom.count),
|
|
217
217
|
charge: atom.formal_charge,
|
|
218
|
-
|
|
218
|
+
spin_multiplicity: atom.spin_multiplicity,
|
|
219
|
+
atom_title: atom.title,
|
|
220
|
+
**extract_coordinates(atom),
|
|
221
|
+
**extract_fractional(atom)
|
|
219
222
|
)
|
|
220
223
|
end
|
|
221
224
|
|
|
@@ -232,6 +235,14 @@ module AsciiChem
|
|
|
232
235
|
end
|
|
233
236
|
end
|
|
234
237
|
|
|
238
|
+
def extract_fractional(atom)
|
|
239
|
+
return {} unless atom.xFract && atom.yFract && atom.zFract
|
|
240
|
+
|
|
241
|
+
{ x_fract: atom.xFract.to_f,
|
|
242
|
+
y_fract: atom.yFract.to_f,
|
|
243
|
+
z_fract: atom.zFract.to_f }
|
|
244
|
+
end
|
|
245
|
+
|
|
235
246
|
def subscript_from_count(count)
|
|
236
247
|
return nil if count.nil? || count.to_s == "1"
|
|
237
248
|
|
|
@@ -330,9 +330,12 @@ module AsciiChem
|
|
|
330
330
|
formal_charge: atom.charge,
|
|
331
331
|
count: effective_count(atom, multiplier),
|
|
332
332
|
lone_pairs: atom.lone_pairs,
|
|
333
|
-
radical_electrons: atom.radical_electrons
|
|
333
|
+
radical_electrons: atom.radical_electrons,
|
|
334
|
+
spin_multiplicity: atom.spin_multiplicity,
|
|
335
|
+
title: atom.atom_title
|
|
334
336
|
}
|
|
335
337
|
merge_coordinates(attrs, atom)
|
|
338
|
+
merge_fractional_coords(attrs, atom)
|
|
336
339
|
@atoms << Chemicalml::Cml::Atom.new(**attrs)
|
|
337
340
|
emit_pending_bond(id) if @pending_bond_kind && @last_atom_id
|
|
338
341
|
@last_atom_id = id
|
|
@@ -386,6 +389,14 @@ module AsciiChem
|
|
|
386
389
|
end
|
|
387
390
|
end
|
|
388
391
|
|
|
392
|
+
def merge_fractional_coords(attrs, atom)
|
|
393
|
+
return unless atom.x_fract && atom.y_fract && atom.z_fract
|
|
394
|
+
|
|
395
|
+
attrs[:xFract] = atom.x_fract
|
|
396
|
+
attrs[:yFract] = atom.y_fract
|
|
397
|
+
attrs[:zFract] = atom.z_fract
|
|
398
|
+
end
|
|
399
|
+
|
|
389
400
|
def combine(left, right)
|
|
390
401
|
lv = integer_or_nil(left)
|
|
391
402
|
rv = integer_or_nil(right)
|
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
|
|
@@ -272,7 +339,12 @@ module AsciiChem
|
|
|
272
339
|
x2: float_or_nil(hash[:x2]),
|
|
273
340
|
y2: float_or_nil(hash[:y2]),
|
|
274
341
|
z2: float_or_nil(hash[:z2]),
|
|
275
|
-
atom_parity: hash[:atom_parity]&.to_s
|
|
342
|
+
atom_parity: hash[:atom_parity]&.to_s,
|
|
343
|
+
spin_multiplicity: hash[:spin_multiplicity]&.to_s,
|
|
344
|
+
atom_title: hash[:atom_title]&.to_s,
|
|
345
|
+
x_fract: float_or_nil(hash[:x_fract]),
|
|
346
|
+
y_fract: float_or_nil(hash[:y_fract]),
|
|
347
|
+
z_fract: float_or_nil(hash[:z_fract])
|
|
276
348
|
)
|
|
277
349
|
end
|
|
278
350
|
|
|
@@ -305,7 +377,9 @@ module AsciiChem
|
|
|
305
377
|
def initialize(element, isotope: nil, subscript: nil, superscript: nil,
|
|
306
378
|
lone_pairs: nil, radical_electrons: nil,
|
|
307
379
|
ring_closures: nil,
|
|
308
|
-
x2: nil, y2: nil, z2: nil, atom_parity: nil
|
|
380
|
+
x2: nil, y2: nil, z2: nil, atom_parity: nil,
|
|
381
|
+
spin_multiplicity: nil, atom_title: nil,
|
|
382
|
+
x_fract: nil, y_fract: nil, z_fract: nil)
|
|
309
383
|
@element = element
|
|
310
384
|
@isotope = isotope
|
|
311
385
|
@subscript = subscript
|
|
@@ -317,6 +391,11 @@ module AsciiChem
|
|
|
317
391
|
@y2 = y2
|
|
318
392
|
@z2 = z2
|
|
319
393
|
@atom_parity = atom_parity
|
|
394
|
+
@spin_multiplicity = spin_multiplicity
|
|
395
|
+
@atom_title = atom_title
|
|
396
|
+
@x_fract = x_fract
|
|
397
|
+
@y_fract = y_fract
|
|
398
|
+
@z_fract = z_fract
|
|
320
399
|
end
|
|
321
400
|
|
|
322
401
|
def build
|
|
@@ -335,7 +414,12 @@ module AsciiChem
|
|
|
335
414
|
x2: @x2,
|
|
336
415
|
y2: @y2,
|
|
337
416
|
z2: @z2,
|
|
338
|
-
atom_parity: @atom_parity
|
|
417
|
+
atom_parity: @atom_parity,
|
|
418
|
+
spin_multiplicity: @spin_multiplicity,
|
|
419
|
+
atom_title: @atom_title,
|
|
420
|
+
x_fract: @x_fract,
|
|
421
|
+
y_fract: @y_fract,
|
|
422
|
+
z_fract: @z_fract
|
|
339
423
|
)
|
|
340
424
|
end
|
|
341
425
|
|
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
|