asciichem 0.3.4 → 0.4.1
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 +35 -0
- data/lib/asciichem/grammar.rb +45 -1
- data/lib/asciichem/model/crystal.rb +58 -0
- data/lib/asciichem/model/spectrum.rb +46 -0
- data/lib/asciichem/model.rb +2 -0
- data/lib/asciichem/transform.rb +144 -0
- data/lib/asciichem/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1272affa5aa88924aaa06424dbc59bb115255963b675f596b58fd9588cd94fdc
|
|
4
|
+
data.tar.gz: bc6e6da1096c7a1c4e404d23036ec9513eae0f2866feeff9bd54a31c04540473
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1fb3e8ae50d397af66fa526f4351b6876f2c28c76c13604d8bd4c488abbff0b10eace074df3d48b1ff6b12b86a81f6f88ddabf07a6bf1588c63540ef0ad0113
|
|
7
|
+
data.tar.gz: c0c694ec1edf82c9d7dfc57adaa8853cf37c1d94dca7a1288e0587f669776e5d68275345ebcc89717628b12711acd88feb3adcc885aeb945b53bf4d7f8f08d42
|
|
@@ -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,41 @@ 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
|
+
|
|
143
|
+
def visit_spectrum(spectrum)
|
|
144
|
+
parts = ["spectrum"]
|
|
145
|
+
parts << "[#{spectrum.type}]" if spectrum.type
|
|
146
|
+
params = spectrum.params.map { |k, v| "#{k}=#{v}" }.join(',')
|
|
147
|
+
parts << "(#{params})" unless params.empty?
|
|
148
|
+
peak_lines = spectrum.peaks.map do |peak|
|
|
149
|
+
line = "#{peak[:position]}: #{peak[:intensity]}"
|
|
150
|
+
line += " #{peak[:multiplicity]}" if peak[:multiplicity]
|
|
151
|
+
line += %( "#{peak[:assignment]}") if peak[:assignment]
|
|
152
|
+
line
|
|
153
|
+
end
|
|
154
|
+
unless peak_lines.empty?
|
|
155
|
+
body = peak_lines.join("\n ")
|
|
156
|
+
parts << "{\n #{body}\n}"
|
|
157
|
+
end
|
|
158
|
+
parts.join
|
|
159
|
+
end
|
|
160
|
+
|
|
126
161
|
private
|
|
127
162
|
|
|
128
163
|
def render_node(node)
|
data/lib/asciichem/grammar.rb
CHANGED
|
@@ -27,7 +27,51 @@ 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 | spectrum | 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).as(:crystal_node)
|
|
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
|
|
53
|
+
|
|
54
|
+
# -- spectroscopy ---------------------------------------------------
|
|
55
|
+
|
|
56
|
+
# spectrum[type](params){peak data}
|
|
57
|
+
rule(:spectrum) do
|
|
58
|
+
(str('spectrum') >>
|
|
59
|
+
spectrum_type.maybe >>
|
|
60
|
+
spectrum_params.maybe >>
|
|
61
|
+
spectrum_body.maybe).as(:spectrum_node)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
rule(:spectrum_type) do
|
|
65
|
+
str('[') >> (str(']').absent? >> any).repeat.as(:spectrum_type) >> str(']')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
rule(:spectrum_params) do
|
|
69
|
+
str('(') >> (str(')').absent? >> any).repeat.as(:spectrum_params) >> str(')')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
rule(:spectrum_body) do
|
|
73
|
+
str('{') >> (str('}').absent? >> any).repeat.as(:spectrum_body) >> str('}')
|
|
74
|
+
end
|
|
31
75
|
|
|
32
76
|
# Annotated molecule: a molecule followed by one or more
|
|
33
77
|
# `@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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Model
|
|
5
|
+
# A spectroscopy result: NMR, IR, MS, UV-Vis peaks.
|
|
6
|
+
#
|
|
7
|
+
# Syntax:
|
|
8
|
+
# spectrum[nmr](type=1H,solvent=CDCl3){
|
|
9
|
+
# 1.2: 3H s "CH3"
|
|
10
|
+
# 7.2: 5H m "C6H5"
|
|
11
|
+
# }
|
|
12
|
+
#
|
|
13
|
+
# spectrum[ir]{
|
|
14
|
+
# 3300: broad "O-H stretch"
|
|
15
|
+
# }
|
|
16
|
+
#
|
|
17
|
+
# spectrum[ms]{
|
|
18
|
+
# 18: 100% "M+"
|
|
19
|
+
# }
|
|
20
|
+
class Spectrum < Node
|
|
21
|
+
attr_accessor :type, :params, :peaks
|
|
22
|
+
|
|
23
|
+
def initialize(type: nil, params: {}, peaks: [])
|
|
24
|
+
@type = type
|
|
25
|
+
@params = params
|
|
26
|
+
@peaks = peaks
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def value_attributes
|
|
30
|
+
{ type: type, params: params, peaks: peaks }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def children
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def diagnostic_label
|
|
38
|
+
"Spectrum(#{type || 'unknown'})"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
"spectrum[#{type}](#{params.map { |k, v| "#{k}=#{v}" }.join(',')})"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
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"
|
|
@@ -18,6 +19,7 @@ module AsciiChem
|
|
|
18
19
|
autoload :Node, "asciichem/model/node"
|
|
19
20
|
autoload :Reaction, "asciichem/model/reaction"
|
|
20
21
|
autoload :ReactionCascade, "asciichem/model/reaction_cascade"
|
|
22
|
+
autoload :Spectrum, "asciichem/model/spectrum"
|
|
21
23
|
autoload :Text, "asciichem/model/text"
|
|
22
24
|
end
|
|
23
25
|
end
|
data/lib/asciichem/transform.rb
CHANGED
|
@@ -146,8 +146,152 @@ 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_node: subtree(:data)) do
|
|
155
|
+
hash = data.is_a?(Hash) ? data : {}
|
|
156
|
+
CrystalBuilder.new(
|
|
157
|
+
hash[:crystal_name],
|
|
158
|
+
hash[:crystal_params],
|
|
159
|
+
hash[:crystal_body]
|
|
160
|
+
).build
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# -- spectra --------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
rule(spectrum_node: subtree(:data)) do
|
|
166
|
+
hash = data.is_a?(Hash) ? data : {}
|
|
167
|
+
SpectrumBuilder.new(
|
|
168
|
+
hash[:spectrum_type],
|
|
169
|
+
hash[:spectrum_params],
|
|
170
|
+
hash[:spectrum_body]
|
|
171
|
+
).build
|
|
172
|
+
end
|
|
173
|
+
|
|
149
174
|
# -- internal helpers ------------------------------------------------
|
|
150
175
|
|
|
176
|
+
# Builds a Crystal from parsed grammar captures. The grammar
|
|
177
|
+
# captures the name, params, and body as raw strings; this class
|
|
178
|
+
# parses them into the model fields.
|
|
179
|
+
class CrystalBuilder
|
|
180
|
+
def initialize(name, params_str, body_str)
|
|
181
|
+
@name = strip_parslet(name)
|
|
182
|
+
@params_str = strip_parslet(params_str)
|
|
183
|
+
@body_str = strip_parslet(body_str)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def build
|
|
187
|
+
params = parse_params(@params_str)
|
|
188
|
+
atoms = parse_atoms(@body_str)
|
|
189
|
+
Model::Crystal.new(
|
|
190
|
+
name: @name,
|
|
191
|
+
a: params['a'],
|
|
192
|
+
b: params['b'],
|
|
193
|
+
c: params['c'],
|
|
194
|
+
alpha: params['alpha'],
|
|
195
|
+
beta: params['beta'],
|
|
196
|
+
gamma: params['gamma'],
|
|
197
|
+
spacegroup: params['sg'],
|
|
198
|
+
atoms: atoms
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def strip_parslet(value)
|
|
205
|
+
return nil if value.nil?
|
|
206
|
+
|
|
207
|
+
s = value.to_s.strip
|
|
208
|
+
s.empty? ? nil : s
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def parse_params(str)
|
|
212
|
+
return {} unless str
|
|
213
|
+
|
|
214
|
+
str.split(',').each_with_object({}) do |pair, memo|
|
|
215
|
+
key, val = pair.strip.split('=', 2)
|
|
216
|
+
memo[key] = val&.strip if key
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def parse_atoms(str)
|
|
221
|
+
return [] unless str
|
|
222
|
+
|
|
223
|
+
formula = AsciiChem.parse(str)
|
|
224
|
+
formula.nodes.flat_map do |node|
|
|
225
|
+
next [] unless node.is_a?(Model::Molecule)
|
|
226
|
+
|
|
227
|
+
node.nodes.select { |n| n.is_a?(Model::Atom) }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Builds a Spectrum from parsed grammar captures. Parses peak
|
|
233
|
+
# lines from the body string.
|
|
234
|
+
class SpectrumBuilder
|
|
235
|
+
def initialize(type_str, params_str, body_str)
|
|
236
|
+
@type = strip_value(type_str)
|
|
237
|
+
@params_str = strip_value(params_str)
|
|
238
|
+
@body_str = strip_value(body_str)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build
|
|
242
|
+
Model::Spectrum.new(
|
|
243
|
+
type: @type,
|
|
244
|
+
params: parse_params(@params_str),
|
|
245
|
+
peaks: parse_peaks(@body_str)
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def strip_value(value)
|
|
252
|
+
return nil if value.nil?
|
|
253
|
+
|
|
254
|
+
s = value.to_s.strip
|
|
255
|
+
s.empty? ? nil : s
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def parse_params(str)
|
|
259
|
+
return {} unless str
|
|
260
|
+
|
|
261
|
+
str.split(',').each_with_object({}) do |pair, memo|
|
|
262
|
+
key, val = pair.strip.split('=', 2)
|
|
263
|
+
memo[key] = val&.strip if key
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def parse_peaks(str)
|
|
268
|
+
return [] unless str
|
|
269
|
+
|
|
270
|
+
str.split("\n").filter_map { |line| parse_peak(line.strip) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def parse_peak(line)
|
|
274
|
+
return nil if line.empty?
|
|
275
|
+
|
|
276
|
+
assignment = nil
|
|
277
|
+
match = line.match(/"([^"]*)"/)
|
|
278
|
+
if match
|
|
279
|
+
assignment = match[1]
|
|
280
|
+
line = line.sub(/"[^"]*"/, '').strip
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
pos, rest = line.split(':', 2)
|
|
284
|
+
tokens = rest&.strip&.split(/\s+/) || []
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
position: pos&.strip,
|
|
288
|
+
intensity: tokens[0],
|
|
289
|
+
multiplicity: tokens[1],
|
|
290
|
+
assignment: assignment
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
151
295
|
# Strips the surrounding `"..."` quotes from a quoted text match.
|
|
152
296
|
# Used by both `text_run` and `group_text_run` rules so the
|
|
153
297
|
# 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.1
|
|
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
|
|
@@ -159,6 +166,7 @@ files:
|
|
|
159
166
|
- lib/asciichem/model/node.rb
|
|
160
167
|
- lib/asciichem/model/reaction.rb
|
|
161
168
|
- lib/asciichem/model/reaction_cascade.rb
|
|
169
|
+
- lib/asciichem/model/spectrum.rb
|
|
162
170
|
- lib/asciichem/model/text.rb
|
|
163
171
|
- lib/asciichem/model_adapter.rb
|
|
164
172
|
- lib/asciichem/model_adapter/from_canonical.rb
|