asciichem 0.31.5 → 0.31.6
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/.github/workflows/ci.yml +8 -2
- data/CHANGELOG.md +21 -0
- data/lib/asciichem/linter/balance_check.rb +12 -1
- data/lib/asciichem/linter/base.rb +6 -6
- data/lib/asciichem/linter/diagnostic.rb +6 -1
- data/lib/asciichem/rinchi/binary_engine.rb +27 -12
- data/lib/asciichem/rinchi.rb +5 -3
- data/lib/asciichem/version.rb +1 -1
- data/lib/asciichem/wire_adapter.rb +64 -31
- data/scripts/update-model-schemas.sh +3 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac2c54a74f212244c74efeb88156c34398cdfe87751de14a597833533f4ccb92
|
|
4
|
+
data.tar.gz: 8f5614c2703ae6ee1440be65fa2c7654bc3cc12d2c8b1207f43a974ab804e3f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 487d422ca183453e7e9a82de50aaa1c6e671383047838ca0acab124678520bf08edbe20a115f3c416407683e45b07cc5654b46bdcb71559004ef22271989d90b
|
|
7
|
+
data.tar.gz: ae81c712c9d31ffd440b8ad49200aea40c52251f8a6017344d0ce6bbd70b3b236ac89c850e2aac57c6866f87cec986a0c26694989b5f4b40eed7ffc147c1bbdb
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -5,6 +5,12 @@ on:
|
|
|
5
5
|
push:
|
|
6
6
|
branches: [main]
|
|
7
7
|
|
|
8
|
+
env:
|
|
9
|
+
# Hermetic CI: the corpus ref is pinned, never floating. Bump this
|
|
10
|
+
# one line deliberately with each corpus release (the tag carries
|
|
11
|
+
# what changed).
|
|
12
|
+
CORPUS_REF: v0.8.0
|
|
13
|
+
|
|
8
14
|
jobs:
|
|
9
15
|
test:
|
|
10
16
|
runs-on: ubuntu-latest
|
|
@@ -18,12 +24,12 @@ jobs:
|
|
|
18
24
|
uses: actions/checkout@v7
|
|
19
25
|
with:
|
|
20
26
|
repository: asciichem/asciichem-tests
|
|
27
|
+
ref: ${{ env.CORPUS_REF }}
|
|
21
28
|
path: .asciichem-tests
|
|
22
29
|
- name: Point the suite at the corpus and record its version
|
|
23
30
|
run: |
|
|
24
31
|
echo "ASCIICHEM_CORPUS=$(pwd)/.asciichem-tests/corpus/fixtures" >> "$GITHUB_ENV"
|
|
25
|
-
|
|
26
|
-
echo "ASCIICHEM_CORPUS_VERSION=$(git -C .asciichem-tests describe --tags --abbrev=0 2>/dev/null || echo main)" >> "$GITHUB_ENV"
|
|
32
|
+
echo "ASCIICHEM_CORPUS_VERSION=${{ env.CORPUS_REF }}" >> "$GITHUB_ENV"
|
|
27
33
|
- uses: ruby/setup-ruby@v1
|
|
28
34
|
with:
|
|
29
35
|
ruby-version: ${{ matrix.ruby }}
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to AsciiChem are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.31.6] - 2026-10-21
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **Mechanism wire ingestion** (TODO.v2/13 item 4): `from_model_json`
|
|
10
|
+
restores mechanisms — steps return as canonical reaction text, flows
|
|
11
|
+
and spectators intact; cascade steps have no wire form and raise
|
|
12
|
+
instead of silently flattening. Mechanism round-trip is asserted
|
|
13
|
+
corpus-wide (the `modelJsonRoundTrip` flag is gone).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **BalanceCheck diagnostics carry structured details**: each
|
|
17
|
+
imbalance reports `element => [reactants, products]` deltas for
|
|
18
|
+
every element whose counts differ, alongside the message.
|
|
19
|
+
- **RInChI engine matches the real `rinchi_cmdline` contract**
|
|
20
|
+
(validated end-to-end against the IUPAC binary built from source):
|
|
21
|
+
the RXN file is the positional argument (no flags), direction-distinct
|
|
22
|
+
identities for a reaction and its reverse.
|
|
23
|
+
- Hermetic CI: the conformance corpus is pinned (`CORPUS_REF: v0.8.0`)
|
|
24
|
+
and the schema re-vendor script defaults to the released
|
|
25
|
+
asciichem-model tag (v0.6.0) — CI inputs no longer float at `main`.
|
|
26
|
+
|
|
6
27
|
## [0.31.5] - 2026-10-20
|
|
7
28
|
|
|
8
29
|
### Changed
|
|
@@ -33,7 +33,8 @@ module AsciiChem
|
|
|
33
33
|
diffs = compute_diffs(reactants, products)
|
|
34
34
|
diagnostics << error(
|
|
35
35
|
"Reaction is not balanced: #{diffs}",
|
|
36
|
-
node: reaction
|
|
36
|
+
node: reaction,
|
|
37
|
+
details: element_deltas(reactants, products)
|
|
37
38
|
)
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -87,6 +88,16 @@ module AsciiChem
|
|
|
87
88
|
result
|
|
88
89
|
end
|
|
89
90
|
|
|
91
|
+
# Element => [reactant_count, product_count] for every element
|
|
92
|
+
# whose counts differ — the numbers behind the message.
|
|
93
|
+
def element_deltas(reactants, products)
|
|
94
|
+
(reactants.keys | products.keys).sort.each_with_object({}) do |element, out|
|
|
95
|
+
r = reactants.fetch(element, 0)
|
|
96
|
+
p = products.fetch(element, 0)
|
|
97
|
+
out[element] = [r, p] if r != p
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
90
101
|
def compute_diffs(reactants, products)
|
|
91
102
|
all_elements = (reactants.keys + products.keys).uniq.sort
|
|
92
103
|
all_elements.map do |element|
|
|
@@ -22,16 +22,16 @@ module AsciiChem
|
|
|
22
22
|
|
|
23
23
|
protected
|
|
24
24
|
|
|
25
|
-
def error(message, node: nil)
|
|
26
|
-
Diagnostic.new(severity: :error, message: message, node: node)
|
|
25
|
+
def error(message, node: nil, details: nil)
|
|
26
|
+
Diagnostic.new(severity: :error, message: message, node: node, details: details)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def warning(message, node: nil)
|
|
30
|
-
Diagnostic.new(severity: :warning, message: message, node: node)
|
|
29
|
+
def warning(message, node: nil, details: nil)
|
|
30
|
+
Diagnostic.new(severity: :warning, message: message, node: node, details: details)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def info(message, node: nil)
|
|
34
|
-
Diagnostic.new(severity: :info, message: message, node: node)
|
|
33
|
+
def info(message, node: nil, details: nil)
|
|
34
|
+
Diagnostic.new(severity: :info, message: message, node: node, details: details)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Depth-first walk over every node in the formula. Yields each
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module AsciiChem
|
|
4
4
|
module Linter
|
|
5
|
-
|
|
5
|
+
# `details` carries optional structured data behind the message
|
|
6
|
+
# (e.g. BalanceCheck's per-element [reactants, products] counts)
|
|
7
|
+
# for consumers that want the numbers, not the prose. Human-facing
|
|
8
|
+
# output stays the message.
|
|
9
|
+
Diagnostic = Struct.new(:severity, :message, :node, :details,
|
|
10
|
+
keyword_init: true) do
|
|
6
11
|
def to_s
|
|
7
12
|
context = node&.diagnostic_label
|
|
8
13
|
if context
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
|
+
require 'tempfile'
|
|
4
5
|
|
|
5
6
|
module AsciiChem
|
|
6
7
|
module Rinchi
|
|
@@ -10,24 +11,28 @@ module AsciiChem
|
|
|
10
11
|
# TODO.v2/10, a maintainer decision).
|
|
11
12
|
#
|
|
12
13
|
# AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new
|
|
13
|
-
# AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new(bin: "/usr/local/bin/
|
|
14
|
+
# AsciiChem::Rinchi.engine = AsciiChem::Rinchi::BinaryEngine.new(bin: "/usr/local/bin/rinchi_cmdline")
|
|
15
|
+
# The reference CLI (IUPAC-InChI/RInChI's `rinchi_cmdline`) takes a
|
|
16
|
+
# FILE argument, not stdin, needs no flags, and prints RInChI=,
|
|
17
|
+
# RAuxInfo=, Long-RInChIKey=, Short-RInChIKey= and Web-RInChIKey=
|
|
18
|
+
# lines. The engine writes the RXN to a temp file, runs the binary
|
|
19
|
+
# on it, and reads the identity lines back.
|
|
14
20
|
class BinaryEngine
|
|
15
|
-
DEFAULT_FLAGS =
|
|
21
|
+
DEFAULT_FLAGS = [].freeze
|
|
16
22
|
|
|
17
23
|
attr_reader :bin, :flags
|
|
18
24
|
|
|
19
|
-
def initialize(bin: '
|
|
25
|
+
def initialize(bin: 'rinchi_cmdline', flags: DEFAULT_FLAGS)
|
|
20
26
|
super()
|
|
21
27
|
@bin = bin
|
|
22
28
|
@flags = flags.freeze
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
# reaction → RXN → rinchi
|
|
26
|
-
# pipe: reactants and products cross into the engine as V2000
|
|
31
|
+
# reaction → RXN → rinchi binary → Identity. The RXN emission is
|
|
32
|
+
# the pipe: reactants and products cross into the engine as V2000
|
|
27
33
|
# mol blocks inside one $RXN container.
|
|
28
34
|
def identity(reaction)
|
|
29
|
-
|
|
30
|
-
out = execute(rxn)
|
|
35
|
+
out = execute(AsciiChem::Rxn.write(reaction))
|
|
31
36
|
rinchi = out[/^RInChI=\S+/]
|
|
32
37
|
raise AsciiChem::Error, "#{bin} produced no RInChI:\n#{out}" unless rinchi
|
|
33
38
|
|
|
@@ -40,16 +45,26 @@ module AsciiChem
|
|
|
40
45
|
|
|
41
46
|
private
|
|
42
47
|
|
|
43
|
-
def execute(
|
|
44
|
-
out
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
def execute(rxn)
|
|
49
|
+
out = err = nil
|
|
50
|
+
status = nil
|
|
51
|
+
Tempfile.create(['asciichem', '.rxn']) do |file|
|
|
52
|
+
file.write(rxn)
|
|
53
|
+
file.flush
|
|
54
|
+
out, err, status = Open3.capture3(bin, file.path, *flags)
|
|
47
55
|
end
|
|
48
|
-
|
|
56
|
+
report_failure(status, err, out)
|
|
49
57
|
out
|
|
50
58
|
rescue Errno::ENOENT
|
|
51
59
|
raise EngineMissingError, "RInChI engine binary #{bin.inspect} not found — #{AsciiChem::Rinchi::INSTALL_GUIDE}"
|
|
52
60
|
end
|
|
61
|
+
|
|
62
|
+
def report_failure(status, err, out)
|
|
63
|
+
return if status&.success?
|
|
64
|
+
|
|
65
|
+
raise AsciiChem::Error,
|
|
66
|
+
"#{bin} failed (exit #{status&.exitstatus}): #{((err || '') + (out || '')).strip[0, 500]}"
|
|
67
|
+
end
|
|
53
68
|
end
|
|
54
69
|
end
|
|
55
70
|
end
|
data/lib/asciichem/rinchi.rb
CHANGED
|
@@ -15,9 +15,11 @@ module AsciiChem
|
|
|
15
15
|
autoload :Identity, 'asciichem/rinchi/identity'
|
|
16
16
|
|
|
17
17
|
INSTALL_GUIDE = 'install the IUPAC RInChI software ' \
|
|
18
|
-
'(https://github.com/IUPAC-InChI/RInChI
|
|
19
|
-
'
|
|
20
|
-
'
|
|
18
|
+
'(https://github.com/IUPAC-InChI/RInChI — build ' \
|
|
19
|
+
'src/example_apps/rinchi_cmdline against InChI v1.07.5) ' \
|
|
20
|
+
'and set AsciiChem::Rinchi.engine = ' \
|
|
21
|
+
'AsciiChem::Rinchi::BinaryEngine.new ' \
|
|
22
|
+
'(or pass bin: "path/to/rinchi_cmdline")'
|
|
21
23
|
|
|
22
24
|
class << self
|
|
23
25
|
attr_accessor :engine
|
data/lib/asciichem/version.rb
CHANGED
|
@@ -9,17 +9,19 @@ module AsciiChem
|
|
|
9
9
|
# fields.
|
|
10
10
|
#
|
|
11
11
|
# Emission (to_wire/to_model_json) covers every wire type.
|
|
12
|
-
# Ingestion (from_wire/from_model_json)
|
|
13
|
-
#
|
|
12
|
+
# Ingestion (from_wire/from_model_json) covers the lossless core
|
|
13
|
+
# set plus mechanism: formula, atom, molecule, group, bond,
|
|
14
14
|
# identifier, name, reaction (+conditions), reaction-cascade,
|
|
15
|
-
# electron-configuration, embedded-math, text
|
|
16
|
-
#
|
|
15
|
+
# electron-configuration, embedded-math, text, mechanism (steps as
|
|
16
|
+
# canonical reaction text, flows, spectators; cascade steps have no
|
|
17
|
+
# wire form). Spectrum, crystal, zmatrix and calculation remain
|
|
17
18
|
# emission-only until their round-trip acceptance lands in the
|
|
18
19
|
# corpus.
|
|
19
20
|
module WireAdapter
|
|
20
21
|
CORE_INGEST_TYPES = %w[
|
|
21
22
|
formula atom molecule group bond identifier name reaction
|
|
22
23
|
reaction-cascade electron-configuration embedded-math text
|
|
24
|
+
mechanism
|
|
23
25
|
].freeze
|
|
24
26
|
|
|
25
27
|
class << self
|
|
@@ -72,20 +74,27 @@ module AsciiChem
|
|
|
72
74
|
# only routes `type` and re-injects routed children into
|
|
73
75
|
# polymorphic collections (which lutaml deserialises flat).
|
|
74
76
|
def wire_tree_from_hash(hash)
|
|
75
|
-
type = hash.fetch(
|
|
77
|
+
type = hash.fetch('type') { raise KeyError, 'wire node missing type discriminator' }
|
|
76
78
|
klass = AsciiChem::Wire::REGISTRY.fetch(type) do
|
|
77
79
|
raise KeyError, "unknown wire node type #{type.inspect}"
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
wire = klass.from_json(JSON.generate(hash))
|
|
81
83
|
case type
|
|
82
|
-
when
|
|
83
|
-
wire.nodes = hash.fetch(
|
|
84
|
-
when
|
|
85
|
-
wire.reactants = hash.fetch(
|
|
86
|
-
wire.products = hash.fetch(
|
|
87
|
-
when
|
|
88
|
-
wire.steps = hash.fetch(
|
|
84
|
+
when 'formula', 'molecule', 'group'
|
|
85
|
+
wire.nodes = hash.fetch('nodes', []).map { |child| wire_tree_from_hash(child) }
|
|
86
|
+
when 'reaction'
|
|
87
|
+
wire.reactants = hash.fetch('reactants', []).map { |child| wire_tree_from_hash(child) }
|
|
88
|
+
wire.products = hash.fetch('products', []).map { |child| wire_tree_from_hash(child) }
|
|
89
|
+
when 'reaction-cascade'
|
|
90
|
+
wire.steps = hash.fetch('steps', []).map { |child| wire_tree_from_hash(child) }
|
|
91
|
+
when 'mechanism'
|
|
92
|
+
wire.steps = hash.fetch('steps', []).map do |step_hash|
|
|
93
|
+
step = AsciiChem::Wire::MechanismStep.from_json(JSON.generate(step_hash))
|
|
94
|
+
step.reaction = wire_tree_from_hash(step_hash.fetch('reaction'))
|
|
95
|
+
step
|
|
96
|
+
end
|
|
97
|
+
wire.spectators = hash.fetch('spectators', []).map { |child| wire_tree_from_hash(child) }
|
|
89
98
|
end
|
|
90
99
|
wire
|
|
91
100
|
end
|
|
@@ -93,12 +102,12 @@ module AsciiChem
|
|
|
93
102
|
private
|
|
94
103
|
|
|
95
104
|
def formula_to_wire(node)
|
|
96
|
-
AsciiChem::Wire::Formula.new(type:
|
|
105
|
+
AsciiChem::Wire::Formula.new(type: 'formula', nodes: node.nodes.map { |n| to_wire(n) })
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
def atom_to_wire(atom)
|
|
100
109
|
AsciiChem::Wire::Atom.new(
|
|
101
|
-
type:
|
|
110
|
+
type: 'atom',
|
|
102
111
|
element: atom.element,
|
|
103
112
|
isotope: digits_or_nil(atom.isotope), charge: str_or_nil(atom.charge),
|
|
104
113
|
subscript: digits_or_nil(atom.subscript),
|
|
@@ -134,7 +143,7 @@ module AsciiChem
|
|
|
134
143
|
|
|
135
144
|
def molecule_to_wire(node)
|
|
136
145
|
AsciiChem::Wire::Molecule.new(
|
|
137
|
-
type:
|
|
146
|
+
type: 'molecule',
|
|
138
147
|
nodes: node.nodes.map { |n| to_wire(n) },
|
|
139
148
|
coefficient: node.coefficient,
|
|
140
149
|
stereo: stereo_to_wire(node.stereo),
|
|
@@ -158,7 +167,7 @@ module AsciiChem
|
|
|
158
167
|
|
|
159
168
|
def group_to_wire(node)
|
|
160
169
|
AsciiChem::Wire::Group.new(
|
|
161
|
-
type:
|
|
170
|
+
type: 'group',
|
|
162
171
|
nodes: node.nodes.map { |n| to_wire(n) },
|
|
163
172
|
multiplicity: node.multiplicity,
|
|
164
173
|
bracket: node.bracket.to_s
|
|
@@ -166,27 +175,27 @@ module AsciiChem
|
|
|
166
175
|
end
|
|
167
176
|
|
|
168
177
|
def bond_to_wire(node)
|
|
169
|
-
AsciiChem::Wire::Bond.new(type:
|
|
178
|
+
AsciiChem::Wire::Bond.new(type: 'bond', kind: node.kind.to_s,
|
|
170
179
|
direction: str_or_nil(node.direction&.to_s))
|
|
171
180
|
end
|
|
172
181
|
|
|
173
182
|
def identifier_to_wire(node)
|
|
174
183
|
AsciiChem::Wire::Identifier.new(
|
|
175
|
-
type:
|
|
184
|
+
type: 'identifier',
|
|
176
185
|
value: node.value, convention: node.convention.to_s, dict_ref: node.dict_ref
|
|
177
186
|
)
|
|
178
187
|
end
|
|
179
188
|
|
|
180
189
|
def name_to_wire(node)
|
|
181
190
|
AsciiChem::Wire::Name.new(
|
|
182
|
-
type:
|
|
191
|
+
type: 'name',
|
|
183
192
|
content: node.content, convention: node.convention, dict_ref: node.dict_ref
|
|
184
193
|
)
|
|
185
194
|
end
|
|
186
195
|
|
|
187
196
|
def reaction_to_wire(node)
|
|
188
197
|
AsciiChem::Wire::Reaction.new(
|
|
189
|
-
type:
|
|
198
|
+
type: 'reaction',
|
|
190
199
|
reactants: node.reactants.map { |m| molecule_to_wire(m) },
|
|
191
200
|
products: node.products.map { |m| molecule_to_wire(m) },
|
|
192
201
|
arrow: node.arrow.to_s,
|
|
@@ -202,14 +211,14 @@ module AsciiChem
|
|
|
202
211
|
|
|
203
212
|
def cascade_to_wire(node)
|
|
204
213
|
AsciiChem::Wire::ReactionCascade.new(
|
|
205
|
-
type:
|
|
214
|
+
type: 'reaction-cascade',
|
|
206
215
|
steps: node.steps.map { |r| reaction_to_wire(r) }
|
|
207
216
|
)
|
|
208
217
|
end
|
|
209
218
|
|
|
210
219
|
def econfig_to_wire(node)
|
|
211
220
|
AsciiChem::Wire::ElectronConfiguration.new(
|
|
212
|
-
type:
|
|
221
|
+
type: 'electron-configuration',
|
|
213
222
|
orbitals: node.orbitals.map do |(label, count)|
|
|
214
223
|
AsciiChem::Wire::OrbitalOccupancy.new(orbital: label.to_s, occupancy: count.to_s)
|
|
215
224
|
end,
|
|
@@ -226,16 +235,16 @@ module AsciiChem
|
|
|
226
235
|
end
|
|
227
236
|
|
|
228
237
|
def math_to_wire(node)
|
|
229
|
-
AsciiChem::Wire::EmbeddedMath.new(type:
|
|
238
|
+
AsciiChem::Wire::EmbeddedMath.new(type: 'embedded-math', source: node.source)
|
|
230
239
|
end
|
|
231
240
|
|
|
232
241
|
def text_to_wire(node)
|
|
233
|
-
AsciiChem::Wire::Text.new(type:
|
|
242
|
+
AsciiChem::Wire::Text.new(type: 'text', content: node.content)
|
|
234
243
|
end
|
|
235
244
|
|
|
236
245
|
def mechanism_to_wire(node)
|
|
237
246
|
AsciiChem::Wire::Mechanism.new(
|
|
238
|
-
type:
|
|
247
|
+
type: 'mechanism',
|
|
239
248
|
steps: node.steps.map { |step| mechanism_step_to_wire(step) },
|
|
240
249
|
spectators: node.spectators.map { |sp| mechanism_spectator_to_wire(sp) }
|
|
241
250
|
)
|
|
@@ -264,12 +273,16 @@ module AsciiChem
|
|
|
264
273
|
|
|
265
274
|
# The model keeps a step's reaction as TEXT (the mechanism body
|
|
266
275
|
# is verbatim); the wire carries the typed node, so the same
|
|
267
|
-
# grammar parses it here.
|
|
276
|
+
# grammar parses it here. A cascade step has no wire form —
|
|
277
|
+
# MechanismStep.reaction is typed Reaction — and says so instead
|
|
278
|
+
# of silently flattening the chain.
|
|
268
279
|
def mechanism_reaction_to_wire(text)
|
|
269
280
|
node = AsciiChem.parse(text).nodes.first
|
|
270
281
|
case node
|
|
271
282
|
when AsciiChem::Model::Reaction then reaction_to_wire(node)
|
|
272
|
-
when AsciiChem::Model::ReactionCascade
|
|
283
|
+
when AsciiChem::Model::ReactionCascade
|
|
284
|
+
raise AsciiChem::ParseError,
|
|
285
|
+
"mechanism step cascades have no wire form: #{text.inspect}"
|
|
273
286
|
else
|
|
274
287
|
raise AsciiChem::ParseError,
|
|
275
288
|
"mechanism step is not a reaction: #{text.inspect}"
|
|
@@ -278,7 +291,7 @@ module AsciiChem
|
|
|
278
291
|
|
|
279
292
|
def spectrum_to_wire(node)
|
|
280
293
|
AsciiChem::Wire::Spectrum.new(
|
|
281
|
-
type:
|
|
294
|
+
type: 'spectrum',
|
|
282
295
|
technique: node.type,
|
|
283
296
|
params: node.params,
|
|
284
297
|
peaks: node.peaks.map do |peak|
|
|
@@ -292,7 +305,7 @@ module AsciiChem
|
|
|
292
305
|
|
|
293
306
|
def crystal_to_wire(node)
|
|
294
307
|
AsciiChem::Wire::Crystal.new(
|
|
295
|
-
type:
|
|
308
|
+
type: 'crystal',
|
|
296
309
|
name: node.name, a: node.a, b: node.b, c: node.c,
|
|
297
310
|
alpha: node.alpha, beta: node.beta, gamma: node.gamma,
|
|
298
311
|
spacegroup: node.spacegroup,
|
|
@@ -302,7 +315,7 @@ module AsciiChem
|
|
|
302
315
|
|
|
303
316
|
def zmatrix_to_wire(node)
|
|
304
317
|
AsciiChem::Wire::ZMatrix.new(
|
|
305
|
-
type:
|
|
318
|
+
type: 'zmatrix',
|
|
306
319
|
rows: node.rows.map do |row|
|
|
307
320
|
AsciiChem::Wire::ZRow.new(
|
|
308
321
|
atom: row.atom, ref1: row.ref1, distance: row.distance,
|
|
@@ -314,7 +327,7 @@ module AsciiChem
|
|
|
314
327
|
|
|
315
328
|
def calculation_to_wire(node)
|
|
316
329
|
AsciiChem::Wire::Calculation.new(
|
|
317
|
-
type:
|
|
330
|
+
type: 'calculation',
|
|
318
331
|
method: node.method, basis: node.basis,
|
|
319
332
|
properties: node.properties.map do |prop|
|
|
320
333
|
AsciiChem::Wire::CalculatedProperty.new(
|
|
@@ -394,6 +407,26 @@ module AsciiChem
|
|
|
394
407
|
)
|
|
395
408
|
end
|
|
396
409
|
|
|
410
|
+
def mechanism_from_wire(wire)
|
|
411
|
+
AsciiChem::Model::Mechanism.new(
|
|
412
|
+
steps: Array(wire.steps).map { |s| mechanism_step_from_wire(s) },
|
|
413
|
+
spectators: Array(wire.spectators).map { |sp| molecule_from_wire(sp).to_text }
|
|
414
|
+
)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# The model keeps a step's reaction as TEXT; the wire carries the
|
|
418
|
+
# typed node, so ingestion renders it back through the canonical
|
|
419
|
+
# text form.
|
|
420
|
+
def mechanism_step_from_wire(step)
|
|
421
|
+
AsciiChem::Model::Mechanism::Step.new(
|
|
422
|
+
label: step.label,
|
|
423
|
+
reaction: from_wire(step.reaction).to_text,
|
|
424
|
+
flows: Array(step.flows).map do |flow|
|
|
425
|
+
AsciiChem::Model::Mechanism::Flow.new(source: flow.source, target: flow.target)
|
|
426
|
+
end
|
|
427
|
+
)
|
|
428
|
+
end
|
|
429
|
+
|
|
397
430
|
def electron_configuration_from_wire(wire)
|
|
398
431
|
AsciiChem::Model::ElectronConfiguration.new(
|
|
399
432
|
orbitals: Array(wire.orbitals).map { |o| [o.orbital, o.occupancy] },
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Re-vendor the asciichem-model v1 JSON Schemas (YAML) and examples.
|
|
3
|
-
# Usage: scripts/update-model-schemas.sh [model-tag]
|
|
3
|
+
# Usage: scripts/update-model-schemas.sh [model-tag]. Default is the
|
|
4
|
+
# released tag — vendored schemas stay hermetic, never floating.
|
|
4
5
|
set -euo pipefail
|
|
5
6
|
|
|
6
|
-
tag="${1:-
|
|
7
|
+
tag="${1:-v0.6.0}"
|
|
7
8
|
work="$(mktemp -d)"
|
|
8
9
|
trap 'rm -rf "$work"' EXIT
|
|
9
10
|
|