asciichem 0.3.2 → 0.3.4

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: dc89810e56d2b42835eddf9bdc0609e4520a4deccbc9451d6fab66ffc2f441ae
4
- data.tar.gz: 39e94b4f58d2e61c44cbc377bea8fb8eb60bd8f5f0123053126fc634b94a8b5f
3
+ metadata.gz: 82e379bdd585e67154ee2ac11685d34d343fc7603dad41c8e629d27610f66a0e
4
+ data.tar.gz: 443e0c865b127e706b61ec9d8781b3516aecb8dfb86a83630e1363af5ae52779
5
5
  SHA512:
6
- metadata.gz: 7f765d66cad35e53f0e069bd1244cbc4bf1f4ba9ca029318a88c6276b28c91ca06f68e50d721a4f61fbbf494c496fbccf36f52f41d13b814c50501b86b8d23cb
7
- data.tar.gz: 7762771fb9322427440533bbac79991ffa203f220456c30c8d9cbe5be5b9129928ce7905684090a07172d0d9273127c30d23183d20d5e0e31a376d04e36fbbac
6
+ metadata.gz: 1dca3abf1149dbbeb1166053cdc6afd58e90c4ad1635c5d13a8884aa11e00b1f43dbdadda6a860d5110dc3924f1ce8f01cba41992faaabbe5bd8a5b05382cdf6
7
+ data.tar.gz: 2fb960db615687093e7cb30f70752303d1b143fc9c3af8cd582d0fef7afcff00e45feabe88769bbb8f6af6431171b3b431d2689339a1d52903f21f397acc23a3
@@ -24,6 +24,7 @@ module AsciiChem
24
24
  xml = translation.document.to_xml
25
25
  xml = inject_atom_extensions(xml, translation.atom_mapping)
26
26
  xml = inject_reaction_conditions(xml, formula)
27
+ xml = inject_metadata(xml, formula)
27
28
  inject_molecule_extensions(xml, formula, translation)
28
29
  end
29
30
 
@@ -32,12 +33,14 @@ module AsciiChem
32
33
  top_level = Extensions.extract_top_level(xml)
33
34
  atom_extensions = Extensions.extract(xml)
34
35
  group_extensions = GroupExtensions.extract(xml)
36
+ metadata_map = extract_metadata(xml)
35
37
  reaction_conditions = extract_reaction_conditions(xml)
36
38
  wire_doc = Chemicalml::Cml::Document.from_xml(xml)
37
39
  formula = AsciiChem::ModelAdapter.from_canonical(wire_doc)
38
40
  Extensions.restore(formula, wire_doc, atom_extensions)
39
41
  GroupExtensions.restore(formula, wire_doc, group_extensions)
40
42
  restore_reaction_conditions(formula, reaction_conditions)
43
+ restore_metadata(formula, metadata_map)
41
44
  Extensions.restore_top_level(formula, top_level)
42
45
  formula
43
46
  end
@@ -115,6 +118,65 @@ module AsciiChem
115
118
  formula
116
119
  end
117
120
 
121
+ # Inject molecule metadata via aci: attributes on <molecule>.
122
+ # Each {name: "k", content: "v"} produces aci:meta-k="v".
123
+ def inject_metadata(xml, formula)
124
+ require 'nokogiri'
125
+ doc = Nokogiri::XML(xml)
126
+ root = doc.root
127
+ molecules = formula.nodes.select { |n| n.is_a?(AsciiChem::Model::Molecule) }
128
+ has_meta = molecules.any? { |m| !m.metadata.empty? }
129
+ return xml unless has_meta
130
+
131
+ root.add_namespace(Extensions::PREFIX, Extensions::NAMESPACE) unless root.namespaces.value?(Extensions::NAMESPACE)
132
+ molecules.each_with_index do |mol, idx|
133
+ next if mol.metadata.empty?
134
+
135
+ mol_el = root.at_xpath("//cml:molecule[@id='m#{idx + 1}']", cml: Extensions::CML_NS)
136
+ next unless mol_el
137
+
138
+ mol.metadata.each do |m|
139
+ mol_el["#{Extensions::PREFIX}:meta-#{m[:name]}"] = m[:content]
140
+ end
141
+ end
142
+ doc.to_xml
143
+ end
144
+
145
+ def extract_metadata(xml)
146
+ require 'nokogiri'
147
+ doc = Nokogiri::XML(xml)
148
+ result = {}
149
+ doc.xpath("//cml:molecule", cml: Extensions::CML_NS).each do |el|
150
+ id = el['id']
151
+ next unless id
152
+
153
+ meta = {}
154
+ el.attributes.each do |name, attr|
155
+ next unless name.start_with?('meta-')
156
+ next unless attr.namespace && attr.namespace.prefix == Extensions::PREFIX
157
+
158
+ key = name.sub('meta-', '')
159
+ meta[key] = attr.value
160
+ end
161
+ result[id] = meta unless meta.empty?
162
+ end
163
+ result
164
+ end
165
+
166
+ def restore_metadata(formula, metadata_map)
167
+ return formula if metadata_map.empty?
168
+
169
+ formula.nodes.each_with_index do |node, idx|
170
+ next unless node.is_a?(AsciiChem::Model::Molecule)
171
+
172
+ meta = metadata_map["m#{idx + 1}"]
173
+ next unless meta
174
+
175
+ meta.each { |name, content| node.metadata << { name: name, content: content } }
176
+ end
177
+ formula
178
+ end
179
+
118
180
  def inject_molecule_extensions(xml, formula, translation)
119
181
  xml = inject_groups(xml, formula, translation)
120
182
  inject_top_level(xml, formula)
@@ -57,15 +57,19 @@ module AsciiChem
57
57
  end
58
58
 
59
59
  def atom_annotation(atom)
60
- annotation = +""
60
+ parts = []
61
61
  if atom.x2 && atom.y2
62
- annotation << "@(#{format_coord(atom.x2)},#{format_coord(atom.y2)}"
63
- annotation << ",#{format_coord(atom.z2)}" if atom.z2
64
- annotation << ")"
65
- elsif atom.atom_parity
66
- annotation << "@#{atom.atom_parity}"
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
- annotation
72
+ parts.join
69
73
  end
70
74
 
71
75
  def format_coord(value)
@@ -182,7 +182,7 @@ module AsciiChem
182
182
  atom_suffix >>
183
183
  lewis_radicals.maybe >>
184
184
  ring_closures.maybe.as(:ring_closures) >>
185
- atom_annotation.maybe).as(:atom)
185
+ atom_annotation).as(:atom)
186
186
  end
187
187
 
188
188
  rule(:plain_atom) do
@@ -191,15 +191,18 @@ module AsciiChem
191
191
  atom_suffix >>
192
192
  lewis_radicals.maybe >>
193
193
  ring_closures.maybe.as(:ring_closures) >>
194
- atom_annotation.maybe).as(:atom)
194
+ atom_annotation).as(:atom)
195
195
  end
196
196
 
197
- # Atom annotations: stereo parity (@R / @S) or 2D/3D coordinates
198
- # (@(x,y) / @(x,y,z)). Both use the `@` prefix. An atom can carry
199
- # at most one annotation in the grammar; multiple annotations
200
- # would require compound syntax (deferred).
197
+ # Atom annotations: each is independently optional via .maybe.
198
+ # Order matters: @(x,y) @R/@S → @m(N) → @t("...") → @f(x,y,z).
199
+ # Example: C@(10,20)@R@m(2)@t("C1")@f(0.5,0.5,0.5)
201
200
  rule(:atom_annotation) do
202
- coordinate_annotation | parity_annotation
201
+ coordinate_annotation.maybe >>
202
+ parity_annotation.maybe >>
203
+ multiplicity_annotation.maybe >>
204
+ atom_title_annotation.maybe >>
205
+ fractional_annotation.maybe
203
206
  end
204
207
 
205
208
  rule(:parity_annotation) do
@@ -214,6 +217,27 @@ module AsciiChem
214
217
  str(')')
215
218
  end
216
219
 
220
+ # Spin multiplicity: @m(2) for doublet, @m(1) for singlet
221
+ rule(:multiplicity_annotation) do
222
+ str('@m(') >> match('[0-9]').repeat(1).as(:spin_multiplicity) >> str(')')
223
+ end
224
+
225
+ # Atom title/label: @t("C1")
226
+ rule(:atom_title_annotation) do
227
+ str('@t(') >> str('"') >>
228
+ (str('"').absent? >> any).repeat.as(:atom_title) >>
229
+ str('"') >> str(')')
230
+ end
231
+
232
+ # Fractional coordinates (crystallographic): @f(0.5,0.25,0.75)
233
+ rule(:fractional_annotation) do
234
+ str('@f(') >>
235
+ float_number.as(:x_fract) >> str(',') >>
236
+ float_number.as(:y_fract) >> str(',') >>
237
+ float_number.as(:z_fract) >>
238
+ str(')')
239
+ end
240
+
217
241
  rule(:float_number) do
218
242
  str('-').maybe >> match('[0-9]').repeat(1) >> (str('.') >> match('[0-9]').repeat(0)).maybe
219
243
  end
@@ -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
@@ -104,7 +104,7 @@ module AsciiChem
104
104
  canonical_properties.map do |p|
105
105
  {
106
106
  title: p.title,
107
- value: extract_scalar_value(p.value),
107
+ value: extract_scalar_value(p.scalar),
108
108
  dict_ref: p.dict_ref,
109
109
  convention: p.convention
110
110
  }
@@ -113,7 +113,7 @@ module AsciiChem
113
113
 
114
114
  def extract_scalar_value(value)
115
115
  return nil if value.nil?
116
- return value.value if value.is_a?(Chemicalml::Cml::Scalar)
116
+ return value.content if value.is_a?(Chemicalml::Cml::Scalar)
117
117
 
118
118
  value.to_s
119
119
  end
@@ -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
- **extract_coordinates(atom)
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
 
@@ -130,7 +130,7 @@ module AsciiChem
130
130
  properties.map do |p|
131
131
  Chemicalml::Cml::Property.new(
132
132
  title: p[:title],
133
- value: build_scalar(p[:value]),
133
+ scalar: build_scalar(p[:value]),
134
134
  dict_ref: p[:dict_ref],
135
135
  convention: p[:convention]
136
136
  )
@@ -141,7 +141,7 @@ module AsciiChem
141
141
  return nil if value.nil?
142
142
 
143
143
  Chemicalml::Cml::Scalar.new(
144
- value: value.to_s,
144
+ content: value.to_s,
145
145
  dict_ref: nil
146
146
  )
147
147
  end
@@ -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)
@@ -272,7 +272,12 @@ module AsciiChem
272
272
  x2: float_or_nil(hash[:x2]),
273
273
  y2: float_or_nil(hash[:y2]),
274
274
  z2: float_or_nil(hash[:z2]),
275
- atom_parity: hash[:atom_parity]&.to_s
275
+ atom_parity: hash[:atom_parity]&.to_s,
276
+ spin_multiplicity: hash[:spin_multiplicity]&.to_s,
277
+ atom_title: hash[:atom_title]&.to_s,
278
+ x_fract: float_or_nil(hash[:x_fract]),
279
+ y_fract: float_or_nil(hash[:y_fract]),
280
+ z_fract: float_or_nil(hash[:z_fract])
276
281
  )
277
282
  end
278
283
 
@@ -305,7 +310,9 @@ module AsciiChem
305
310
  def initialize(element, isotope: nil, subscript: nil, superscript: nil,
306
311
  lone_pairs: nil, radical_electrons: nil,
307
312
  ring_closures: nil,
308
- x2: nil, y2: nil, z2: nil, atom_parity: nil)
313
+ x2: nil, y2: nil, z2: nil, atom_parity: nil,
314
+ spin_multiplicity: nil, atom_title: nil,
315
+ x_fract: nil, y_fract: nil, z_fract: nil)
309
316
  @element = element
310
317
  @isotope = isotope
311
318
  @subscript = subscript
@@ -317,6 +324,11 @@ module AsciiChem
317
324
  @y2 = y2
318
325
  @z2 = z2
319
326
  @atom_parity = atom_parity
327
+ @spin_multiplicity = spin_multiplicity
328
+ @atom_title = atom_title
329
+ @x_fract = x_fract
330
+ @y_fract = y_fract
331
+ @z_fract = z_fract
320
332
  end
321
333
 
322
334
  def build
@@ -335,7 +347,12 @@ module AsciiChem
335
347
  x2: @x2,
336
348
  y2: @y2,
337
349
  z2: @z2,
338
- atom_parity: @atom_parity
350
+ atom_parity: @atom_parity,
351
+ spin_multiplicity: @spin_multiplicity,
352
+ atom_title: @atom_title,
353
+ x_fract: @x_fract,
354
+ y_fract: @y_fract,
355
+ z_fract: @z_fract
339
356
  )
340
357
  end
341
358
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.4"
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.3.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.