asciichem 0.4.0 → 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: 7d401ee7215f54961b4126b1da5451a2e71ac316c05e3864d199c5482e28d366
4
- data.tar.gz: c132a8c376dd7485bf0319f8d971d89cb952c27d9f2466c0f3af4af945b0ca4b
3
+ metadata.gz: 9d87f933cf6baef0a8a1f5c8e189948ba3a2f49ea9adea77b5af22854e2ccb1c
4
+ data.tar.gz: afb6ab79bf85415caa65fe9315a7cbc76913562cb969aa5e5947a6c7eaede38b
5
5
  SHA512:
6
- metadata.gz: ab9ee8f2410f46c659cc97426a7272c511e983b601cbf043809c5b657621f0d7a48e79f17c2c4f79707cfebaf265e963077a5f636d8e03c87be2942e4fddc27e
7
- data.tar.gz: 1cf4d97ef6459a29b9d7f05ddbd8b8b1db0ca512f547cb48badd7ebb5abc42f9c70c98cdade086af9dedf48f2e16c96ddd199097ee90c3e2ba56984e66a91aa7
6
+ metadata.gz: 912c55ba11516ed11b613ac538ee6c90d792f95b86b3337d1348e04bf50531c5f08bead8e1781155dea56b8a8cfdd66506e885ccbac713b5384acb9e982f52c2
7
+ data.tar.gz: c044eecacf93ae9c80a8f6ddebc66f8be38820b8157a6410d20f9bfc60428ea7377e39e8271343b4645bffa371dd67b179d3f74183c754778f0fd333984afc0a
@@ -140,7 +140,65 @@ module AsciiChem
140
140
  parts.join
141
141
  end
142
142
 
143
- private
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
+
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
144
202
 
145
203
  def render_node(node)
146
204
  node.accept(self)
@@ -27,16 +27,16 @@ module AsciiChem
27
27
 
28
28
  rule(:nodes) { node >> (spaces? >> node).repeat }
29
29
 
30
- rule(:node) { reaction_cascade | reaction | electron_config | crystal | 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
 
34
34
  # crystal[Name](a=X,b=Y,...,sg=SG){atoms with @f(x,y,z)}
35
35
  rule(:crystal) do
36
- str('crystal') >>
36
+ (str('crystal') >>
37
37
  crystal_name.maybe >>
38
38
  crystal_params.maybe >>
39
- crystal_body.maybe
39
+ crystal_body.maybe).as(:crystal_node)
40
40
  end
41
41
 
42
42
  rule(:crystal_name) do
@@ -51,6 +51,67 @@ module AsciiChem
51
51
  str('{') >> (str('}').absent? >> any).repeat.as(:crystal_body) >> str('}')
52
52
  end
53
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
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
+
54
115
  # Annotated molecule: a molecule followed by one or more
55
116
  # `@key("value")` annotations for CML metadata (names,
56
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,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
@@ -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,17 +8,21 @@ 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"
20
22
  autoload :Reaction, "asciichem/model/reaction"
21
23
  autoload :ReactionCascade, "asciichem/model/reaction_cascade"
24
+ autoload :Spectrum, "asciichem/model/spectrum"
22
25
  autoload :Text, "asciichem/model/text"
26
+ autoload :ZMatrix, "asciichem/model/zmatrix"
23
27
  end
24
28
  end
@@ -151,10 +151,45 @@ module AsciiChem
151
151
  # Grammar captures crystal_name, crystal_params, and crystal_body
152
152
  # as optional strings. CrystalBuilder parses them into the model.
153
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
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
+
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
158
193
  end
159
194
 
160
195
  # -- internal helpers ------------------------------------------------
@@ -215,6 +250,204 @@ module AsciiChem
215
250
  end
216
251
  end
217
252
 
253
+ # Builds a Spectrum from parsed grammar captures. Parses peak
254
+ # lines from the body string.
255
+ class SpectrumBuilder
256
+ def initialize(type_str, params_str, body_str)
257
+ @type = strip_value(type_str)
258
+ @params_str = strip_value(params_str)
259
+ @body_str = strip_value(body_str)
260
+ end
261
+
262
+ def build
263
+ Model::Spectrum.new(
264
+ type: @type,
265
+ params: parse_params(@params_str),
266
+ peaks: parse_peaks(@body_str)
267
+ )
268
+ end
269
+
270
+ private
271
+
272
+ def strip_value(value)
273
+ return nil if value.nil?
274
+
275
+ s = value.to_s.strip
276
+ s.empty? ? nil : s
277
+ end
278
+
279
+ def parse_params(str)
280
+ return {} unless str
281
+
282
+ str.split(',').each_with_object({}) do |pair, memo|
283
+ key, val = pair.strip.split('=', 2)
284
+ memo[key] = val&.strip if key
285
+ end
286
+ end
287
+
288
+ def parse_peaks(str)
289
+ return [] unless str
290
+
291
+ str.split("\n").filter_map { |line| parse_peak(line.strip) }
292
+ end
293
+
294
+ def parse_peak(line)
295
+ return nil if line.empty?
296
+
297
+ assignment = nil
298
+ match = line.match(/"([^"]*)"/)
299
+ if match
300
+ assignment = match[1]
301
+ line = line.sub(/"[^"]*"/, '').strip
302
+ end
303
+
304
+ pos, rest = line.split(':', 2)
305
+ tokens = rest&.strip&.split(/\s+/) || []
306
+
307
+ {
308
+ position: pos&.strip,
309
+ intensity: tokens[0],
310
+ multiplicity: tokens[1],
311
+ assignment: assignment
312
+ }
313
+ end
314
+ end
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
+
218
451
  # Strips the surrounding `"..."` quotes from a quoted text match.
219
452
  # Used by both `text_run` and `group_text_run` rules so the
220
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.0"
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -155,18 +155,22 @@ 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
167
169
  - lib/asciichem/model/reaction.rb
168
170
  - lib/asciichem/model/reaction_cascade.rb
171
+ - lib/asciichem/model/spectrum.rb
169
172
  - lib/asciichem/model/text.rb
173
+ - lib/asciichem/model/zmatrix.rb
170
174
  - lib/asciichem/model_adapter.rb
171
175
  - lib/asciichem/model_adapter/from_canonical.rb
172
176
  - lib/asciichem/model_adapter/to_canonical.rb