asciichem 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1272affa5aa88924aaa06424dbc59bb115255963b675f596b58fd9588cd94fdc
4
- data.tar.gz: bc6e6da1096c7a1c4e404d23036ec9513eae0f2866feeff9bd54a31c04540473
3
+ metadata.gz: 9d87f933cf6baef0a8a1f5c8e189948ba3a2f49ea9adea77b5af22854e2ccb1c
4
+ data.tar.gz: afb6ab79bf85415caa65fe9315a7cbc76913562cb969aa5e5947a6c7eaede38b
5
5
  SHA512:
6
- metadata.gz: f1fb3e8ae50d397af66fa526f4351b6876f2c28c76c13604d8bd4c488abbff0b10eace074df3d48b1ff6b12b86a81f6f88ddabf07a6bf1588c63540ef0ad0113
7
- data.tar.gz: c0c694ec1edf82c9d7dfc57adaa8853cf37c1d94dca7a1288e0587f669776e5d68275345ebcc89717628b12711acd88feb3adcc885aeb945b53bf4d7f8f08d42
6
+ metadata.gz: 912c55ba11516ed11b613ac538ee6c90d792f95b86b3337d1348e04bf50531c5f08bead8e1781155dea56b8a8cfdd66506e885ccbac713b5384acb9e982f52c2
7
+ data.tar.gz: c044eecacf93ae9c80a8f6ddebc66f8be38820b8157a6410d20f9bfc60428ea7377e39e8271343b4645bffa371dd67b179d3f74183c754778f0fd333984afc0a
@@ -158,7 +158,47 @@ module AsciiChem
158
158
  parts.join
159
159
  end
160
160
 
161
- private
161
+ def visit_calculation(calc)
162
+ parts = ["calc"]
163
+ params = []
164
+ params << calc.method if calc.method
165
+ params << calc.basis if calc.basis
166
+ parts << "(#{params.join('/')})" unless params.empty?
167
+ unless calc.properties.empty?
168
+ lines = calc.properties.map do |p|
169
+ line = "#{p[:title]}: #{p[:value]}"
170
+ line += " #{p[:units]}" if p[:units]
171
+ line
172
+ end
173
+ parts << "{\n #{lines.join("\n ")}\n}"
174
+ end
175
+ parts.join
176
+ end
177
+
178
+ def visit_z_matrix(zm)
179
+ parts = ["zmatrix"]
180
+ unless zm.rows.empty?
181
+ lines = zm.rows.map do |row|
182
+ tokens = [row.atom]
183
+ tokens << row.ref1 << row.distance if row.ref1
184
+ tokens << row.ref2 << row.angle if row.ref2
185
+ tokens << row.ref3 << row.dihedral if row.ref3
186
+ tokens.compact.join(" ")
187
+ end
188
+ parts << "{\n #{lines.join("\n ")}\n}"
189
+ end
190
+ parts.join
191
+ end
192
+
193
+ def visit_mechanism(mech)
194
+ parts = ["mechanism"]
195
+ unless mech.steps.empty? && mech.spectators.empty?
196
+ lines = mech.steps.map { |s| "#{s[:label]}: #{s[:reaction]}" }
197
+ mech.spectators.each { |sp| lines << "spectator: #{sp}" }
198
+ parts << "{\n #{lines.join("\n ")}\n}"
199
+ end
200
+ parts.join
201
+ end
162
202
 
163
203
  def render_node(node)
164
204
  node.accept(self)
@@ -27,7 +27,7 @@ module AsciiChem
27
27
 
28
28
  rule(:nodes) { node >> (spaces? >> node).repeat }
29
29
 
30
- rule(:node) { reaction_cascade | reaction | electron_config | crystal | spectrum | annotated_molecule | molecule | embedded_math | text_run.as(:text_run) }
30
+ rule(:node) { reaction_cascade | reaction | electron_config | crystal | spectrum | calculation | zmatrix | mechanism | annotated_molecule | molecule | embedded_math | text_run.as(:text_run) }
31
31
 
32
32
  # -- crystallography -------------------------------------------------
33
33
 
@@ -73,6 +73,45 @@ module AsciiChem
73
73
  str('{') >> (str('}').absent? >> any).repeat.as(:spectrum_body) >> str('}')
74
74
  end
75
75
 
76
+ # -- computational chemistry ----------------------------------------
77
+
78
+ # calc(method/basis){key: value units}
79
+ rule(:calculation) do
80
+ (str('calc') >>
81
+ calc_params.maybe >>
82
+ calc_body.maybe).as(:calc_node)
83
+ end
84
+
85
+ rule(:calc_params) do
86
+ str('(') >> (str(')').absent? >> any).repeat.as(:calc_params) >> str(')')
87
+ end
88
+
89
+ rule(:calc_body) do
90
+ str('{') >> (str('}').absent? >> any).repeat.as(:calc_body) >> str('}')
91
+ end
92
+
93
+ # -- Z-Matrix -------------------------------------------------------
94
+
95
+ rule(:zmatrix) do
96
+ (str('zmatrix') >>
97
+ zmatrix_body.maybe).as(:zmatrix_node)
98
+ end
99
+
100
+ rule(:zmatrix_body) do
101
+ str('{') >> (str('}').absent? >> any).repeat.as(:zmatrix_body) >> str('}')
102
+ end
103
+
104
+ # -- reaction mechanisms --------------------------------------------
105
+
106
+ rule(:mechanism) do
107
+ (str('mechanism') >>
108
+ mechanism_body.maybe).as(:mechanism_node)
109
+ end
110
+
111
+ rule(:mechanism_body) do
112
+ str('{') >> (str('}').absent? >> any).repeat.as(:mechanism_body) >> str('}')
113
+ end
114
+
76
115
  # Annotated molecule: a molecule followed by one or more
77
116
  # `@key("value")` annotations for CML metadata (names,
78
117
  # identifiers, title, formula, labels).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Model
5
+ # A computational chemistry calculation result.
6
+ #
7
+ # Syntax:
8
+ # calc(b3lyp/6-31G*){
9
+ # energy: -234.5 Hartree
10
+ # dipole: [0.1, 0.2, 0.3] Debye
11
+ # }
12
+ class Calculation < Node
13
+ attr_accessor :method, :basis, :properties
14
+
15
+ def initialize(method: nil, basis: nil, properties: [])
16
+ @method = method
17
+ @basis = basis
18
+ @properties = properties
19
+ end
20
+
21
+ def value_attributes
22
+ { method: method, basis: basis, properties: properties }
23
+ end
24
+
25
+ def children
26
+ []
27
+ end
28
+
29
+ def diagnostic_label
30
+ "Calculation(#{method}/#{basis})"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Model
5
+ # A reaction mechanism: multi-step pathway with spectators.
6
+ #
7
+ # Syntax:
8
+ # mechanism{
9
+ # step1: A + B -> C
10
+ # step2: C -> D + E
11
+ # spectator: Na+
12
+ # }
13
+ class Mechanism < Node
14
+ attr_accessor :steps, :spectators
15
+
16
+ def initialize(steps: [], spectators: [])
17
+ @steps = steps
18
+ @spectators = spectators
19
+ end
20
+
21
+ def value_attributes
22
+ { steps: steps, spectators: spectators }
23
+ end
24
+
25
+ def children
26
+ []
27
+ end
28
+
29
+ def diagnostic_label
30
+ "Mechanism(#{steps.length} steps)"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Model
5
+ # A Z-Matrix: internal coordinates (bond lengths, angles, torsions).
6
+ #
7
+ # Syntax:
8
+ # zmatrix{
9
+ # C1
10
+ # H2 C1 1.09
11
+ # H3 C1 1.09 H2 109.5
12
+ # H4 C1 1.09 H2 109.5 H3 120.0
13
+ # }
14
+ class ZMatrix < Node
15
+ ZRow = Struct.new(:atom, :ref1, :distance, :ref2, :angle, :ref3, :dihedral, keyword_init: true)
16
+
17
+ attr_accessor :rows
18
+
19
+ def initialize(rows: [])
20
+ @rows = rows
21
+ end
22
+
23
+ def value_attributes
24
+ { rows: rows }
25
+ end
26
+
27
+ def children
28
+ []
29
+ end
30
+
31
+ def diagnostic_label
32
+ "ZMatrix(#{rows.length} rows)"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -8,12 +8,14 @@ module AsciiChem
8
8
  module Model
9
9
  autoload :Atom, "asciichem/model/atom"
10
10
  autoload :Bond, "asciichem/model/bond"
11
+ autoload :Calculation, "asciichem/model/calculation"
11
12
  autoload :Crystal, "asciichem/model/crystal"
12
13
  autoload :ElectronConfiguration, "asciichem/model/electron_configuration"
13
14
  autoload :EmbeddedMath, "asciichem/model/embedded_math"
14
15
  autoload :Formula, "asciichem/model/formula"
15
16
  autoload :Group, "asciichem/model/group"
16
17
  autoload :Identifier, "asciichem/model/identifier"
18
+ autoload :Mechanism, "asciichem/model/mechanism"
17
19
  autoload :Molecule, "asciichem/model/molecule"
18
20
  autoload :Name, "asciichem/model/name"
19
21
  autoload :Node, "asciichem/model/node"
@@ -21,5 +23,6 @@ module AsciiChem
21
23
  autoload :ReactionCascade, "asciichem/model/reaction_cascade"
22
24
  autoload :Spectrum, "asciichem/model/spectrum"
23
25
  autoload :Text, "asciichem/model/text"
26
+ autoload :ZMatrix, "asciichem/model/zmatrix"
24
27
  end
25
28
  end
@@ -171,6 +171,27 @@ module AsciiChem
171
171
  ).build
172
172
  end
173
173
 
174
+ # -- computational chemistry ----------------------------------------
175
+
176
+ rule(calc_node: subtree(:data)) do
177
+ hash = data.is_a?(Hash) ? data : {}
178
+ CalculationBuilder.new(hash[:calc_params], hash[:calc_body]).build
179
+ end
180
+
181
+ # -- Z-Matrix -------------------------------------------------------
182
+
183
+ rule(zmatrix_node: subtree(:data)) do
184
+ hash = data.is_a?(Hash) ? data : {}
185
+ ZMatrixBuilder.new(hash[:zmatrix_body]).build
186
+ end
187
+
188
+ # -- reaction mechanisms --------------------------------------------
189
+
190
+ rule(mechanism_node: subtree(:data)) do
191
+ hash = data.is_a?(Hash) ? data : {}
192
+ MechanismBuilder.new(hash[:mechanism_body]).build
193
+ end
194
+
174
195
  # -- internal helpers ------------------------------------------------
175
196
 
176
197
  # Builds a Crystal from parsed grammar captures. The grammar
@@ -292,6 +313,141 @@ module AsciiChem
292
313
  end
293
314
  end
294
315
 
316
+ # Builds a Calculation from grammar captures.
317
+ # Params: "method/basis" string. Body: key-value lines.
318
+ class CalculationBuilder
319
+ def initialize(params_str, body_str)
320
+ @params_str = strip_value(params_str)
321
+ @body_str = strip_value(body_str)
322
+ end
323
+
324
+ def build
325
+ method, basis = parse_method_basis(@params_str)
326
+ Model::Calculation.new(
327
+ method: method,
328
+ basis: basis,
329
+ properties: parse_properties(@body_str)
330
+ )
331
+ end
332
+
333
+ private
334
+
335
+ def strip_value(value)
336
+ return nil if value.nil?
337
+
338
+ s = value.to_s.strip
339
+ s.empty? ? nil : s
340
+ end
341
+
342
+ def parse_method_basis(str)
343
+ return [nil, nil] unless str
344
+
345
+ parts = str.split('/', 2)
346
+ [parts[0]&.strip, parts[1]&.strip]
347
+ end
348
+
349
+ def parse_properties(str)
350
+ return [] unless str
351
+
352
+ str.split("\n").filter_map do |line|
353
+ line = line.strip
354
+ next nil if line.empty?
355
+
356
+ key, rest = line.split(':', 2)
357
+ next nil unless key
358
+
359
+ tokens = rest&.strip&.split(/\s+/) || []
360
+ { title: key.strip, value: tokens[0], units: tokens[1] }
361
+ end
362
+ end
363
+ end
364
+
365
+ # Builds a ZMatrix from grammar captures.
366
+ # Each body line: atom [ref1 distance] [ref2 angle] [ref3 dihedral]
367
+ class ZMatrixBuilder
368
+ def initialize(body_str)
369
+ @body_str = strip_value(body_str)
370
+ end
371
+
372
+ def build
373
+ Model::ZMatrix.new(rows: parse_rows(@body_str))
374
+ end
375
+
376
+ private
377
+
378
+ def strip_value(value)
379
+ return nil if value.nil?
380
+
381
+ s = value.to_s.strip
382
+ s.empty? ? nil : s
383
+ end
384
+
385
+ def parse_rows(str)
386
+ return [] unless str
387
+
388
+ str.split("\n").filter_map { |line| parse_row(line.strip) }
389
+ end
390
+
391
+ def parse_row(line)
392
+ return nil if line.empty?
393
+
394
+ tokens = line.split(/\s+/)
395
+ Model::ZMatrix::ZRow.new(
396
+ atom: tokens[0],
397
+ ref1: tokens[1],
398
+ distance: tokens[2],
399
+ ref2: tokens[3],
400
+ angle: tokens[4],
401
+ ref3: tokens[5],
402
+ dihedral: tokens[6]
403
+ )
404
+ end
405
+ end
406
+
407
+ # Builds a Mechanism from grammar captures.
408
+ # Each body line: key: value (step1: reaction, spectator: ion)
409
+ class MechanismBuilder
410
+ def initialize(body_str)
411
+ @body_str = strip_value(body_str)
412
+ end
413
+
414
+ def build
415
+ steps = []
416
+ spectators = []
417
+ parse_entries(@body_str).each do |key, value|
418
+ if key == 'spectator'
419
+ spectators.concat(value.split(/\s+/).map(&:strip))
420
+ else
421
+ steps << { label: key, reaction: value }
422
+ end
423
+ end
424
+ Model::Mechanism.new(steps: steps, spectators: spectators)
425
+ end
426
+
427
+ private
428
+
429
+ def strip_value(value)
430
+ return nil if value.nil?
431
+
432
+ s = value.to_s.strip
433
+ s.empty? ? nil : s
434
+ end
435
+
436
+ def parse_entries(str)
437
+ return [] unless str
438
+
439
+ str.split("\n").filter_map do |line|
440
+ line = line.strip
441
+ next nil if line.empty?
442
+
443
+ key, val = line.split(':', 2)
444
+ next nil unless key && val
445
+
446
+ [key.strip, val.strip]
447
+ end
448
+ end
449
+ end
450
+
295
451
  # Strips the surrounding `"..."` quotes from a quoted text match.
296
452
  # Used by both `text_run` and `group_text_run` rules so the
297
453
  # model never carries the delimiters — the formatter re-adds them
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
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.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -155,12 +155,14 @@ files:
155
155
  - lib/asciichem/model.rb
156
156
  - lib/asciichem/model/atom.rb
157
157
  - lib/asciichem/model/bond.rb
158
+ - lib/asciichem/model/calculation.rb
158
159
  - lib/asciichem/model/crystal.rb
159
160
  - lib/asciichem/model/electron_configuration.rb
160
161
  - lib/asciichem/model/embedded_math.rb
161
162
  - lib/asciichem/model/formula.rb
162
163
  - lib/asciichem/model/group.rb
163
164
  - lib/asciichem/model/identifier.rb
165
+ - lib/asciichem/model/mechanism.rb
164
166
  - lib/asciichem/model/molecule.rb
165
167
  - lib/asciichem/model/name.rb
166
168
  - lib/asciichem/model/node.rb
@@ -168,6 +170,7 @@ files:
168
170
  - lib/asciichem/model/reaction_cascade.rb
169
171
  - lib/asciichem/model/spectrum.rb
170
172
  - lib/asciichem/model/text.rb
173
+ - lib/asciichem/model/zmatrix.rb
171
174
  - lib/asciichem/model_adapter.rb
172
175
  - lib/asciichem/model_adapter/from_canonical.rb
173
176
  - lib/asciichem/model_adapter/to_canonical.rb