asciichem 0.25.0 → 0.27.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: 7748aa7fb152b3dad47ff42c2debe7335af35a2a36c21d2248ff4f5a5231c575
4
- data.tar.gz: 4352b4fe40fc6998f9860d155e791d7cf2c4f3cd48f0184696346ba934adaa57
3
+ metadata.gz: 4602bf349585d9d81483e3561d970efd6bb06f3196d30a0a3017df316699664f
4
+ data.tar.gz: ea4c38d2af66b75ce2024c5cb1cc866af19584af2edba92b09cc264a7e48037e
5
5
  SHA512:
6
- metadata.gz: 6a22a8e911ec6a04498285f949f79c1ea5e7e6870d464e3229c3e783e744d151f0506b5200dba58e4e06f23177983dd80b24d172c6d5528e960907da821db64a
7
- data.tar.gz: 9d57bab57705518c6f2b40d28f40b0b62c20237d26b9daf20ae4d65bd9b60ad8497ea304c60c163878c412cc6cf61a3c99a6a9179b027bef527e524758b87085
6
+ metadata.gz: 4dad38021607a88a0f37b90a2320b228d47b6e6197b7102eb48616ea94486ccfc16f953c2c233446ca216eefa1d76aebd0c88b1d40edcb2f582abb56c062e79f
7
+ data.tar.gz: 4fc1f074a3b182e2d22c263f09044d873a9d96ba1e627e89780e42977a00aedd0e7142590df6aa645e405e7dcbca89076e505abccc3e1e37be99dddfca527372
data/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.27.0] - 2026-09-14
7
+
8
+ ### Added
9
+ - Local InChI identity engine (TODO.impl 48 / TODO.v2 10): opt-in
10
+ `AsciiChem::Inchi` abstraction over the IUPAC `inchi-1` binary
11
+ (never reimplemented). `EngineMissingError` carries install
12
+ guidance. `IdentityCrossCheck` linter compares annotated
13
+ `@inchi`/`@inchikey` against the drawn structure (error with
14
+ engine, one guidance warning without). CLI: `asciichem identity`.
15
+ - L2 MathML golden conformance: corpus suite pinned against the
16
+ reference goldens; report claims the level.
17
+
18
+ ### Fixed
19
+ - Electron-configuration MathML separator is a real U+00A0 (was a
20
+ double-escaped entity artifact).
21
+ - Reaction conditions render in-place through the same MathML
22
+ formatter (was serialize → reparse → graft, which leaked xmlns and
23
+ reset indentation).
24
+ - Molecule stereo markers (`(R)-`, `(alpha)-`, …) survive the v1
25
+ wire form (additive optional `stereo` field; paired with
26
+ asciichem-model#13).
27
+
28
+ ## [0.26.0] - 2026-09-14
29
+
30
+ ### Added
31
+ - The cite syntax (TODO.impl 45): `@cite("pubchem")` on a molecule
32
+ declares which source to cite it from; `Citation.for_molecule`
33
+ resolves the molecule's identifiers (registry keys preferred over
34
+ names) and emits one bibitem per cited source. Zero grammar
35
+ changes — `@cite` rides the property-annotation form by design.
36
+
37
+ ### Fixed
38
+ - Molecule annotations now canonicalise with spaces between them
39
+ (`@name("Water") @cas("...")`), matching the TypeScript and Python
40
+ canonicalisers; Ruby was the outlier joining them without a
41
+ separator.
42
+
6
43
  ## [0.25.0] - 2026-09-14
7
44
 
8
45
  ### Added
@@ -435,7 +472,8 @@ This project follows [Semantic Versioning](https://semver.org/).
435
472
  `version`.
436
473
  - Comprehensive RSpec suite with round-trip conformance.
437
474
 
438
- [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.25.0...HEAD
475
+ [Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.26.0...HEAD
476
+ [0.26.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.25.0...v0.26.0
439
477
  [0.25.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.24.0...v0.25.0
440
478
  [0.24.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.23.0...v0.24.0
441
479
  [0.23.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.22.0...v0.23.0
data/benchmarks/README.md CHANGED
@@ -41,6 +41,16 @@ as a drop-in speedup for the reference grammar:
41
41
  revalidation against the whole corpus with no measured win to
42
42
  justify it yet.
43
43
 
44
- **Verdict: not adopted.** Revisit when Parsanol's native backend can
45
- be engaged for full grammars and shows measured wins on this
46
- workload; the shim path is a documented no.
44
+ **Verdict: not adopted yet.** Two corrections to the spike (tracked
45
+ upstream in parsanol-ruby#25):
46
+
47
+ 1. The native-DSL micro-benchmark ran in Parsanol's default `:ruby`
48
+ mode — the Rust core (`:native`) was never engaged, so the native
49
+ path is unmeasured, not disproven.
50
+ 2. The `SO_4^2-` failure is a candidate upstream bug (`repeat` of a
51
+ `maybe`-prefixed sequence fails at end-of-input; minimal repro in
52
+ the issue).
53
+
54
+ Revisit trigger unchanged: engage the native backend for full
55
+ grammars, fix the repetition-termination bug, and beat parslet on
56
+ this workload — then re-run the corpus against the port.
@@ -81,6 +81,57 @@ module AsciiChem
81
81
  bibitem(substance).to_xml
82
82
  end
83
83
 
84
+ # The cite syntax (TODO.impl 45): a molecule annotated
85
+ # `@cite("pubchem")` (a property annotation — the grammar needs
86
+ # no extension) declares *which source to cite it from*. This
87
+ # resolves the molecule's identifiers and emits one bibitem per
88
+ # cited source. Returns [[source, bibitem]] pairs; empty when the
89
+ # molecule has no @cite annotations.
90
+ #
91
+ # AsciiChem.parse('H_2O @name("water") @cite("pubchem")')
92
+ # AsciiChem::Citation.for_molecule(formula.nodes.first).map(&:last)
93
+ def for_molecule(molecule, cache: nil, fetch: nil)
94
+ sources = citation_sources(molecule)
95
+ return [] if sources.empty?
96
+
97
+ convention, value = lookup_key(molecule)
98
+ unless value
99
+ raise Error,
100
+ "molecule carries no resolvable identifier for citation " \
101
+ "(annotate @cas/@inchikey/@smiles or @name)"
102
+ end
103
+
104
+ sources.filter_map do |source|
105
+ substance = AsciiChem::Resolver[source].new.resolve(
106
+ value: value, convention: convention, cache: cache, fetch: fetch)
107
+ next unless substance
108
+
109
+ [source, bibitem(substance)]
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # The property annotation whose title is "cite": values are the
116
+ # source names to cite from.
117
+ def citation_sources(molecule)
118
+ molecule.properties
119
+ .select { |p| p.title == "cite" && p.value }
120
+ .map(&:value)
121
+ end
122
+
123
+ # First identifier the resolver can look up by, in preference
124
+ # order: unambiguous registry keys before names.
125
+ def lookup_key(molecule)
126
+ identifier = molecule.identifiers.find { |i| %w[cas inchikey pubchem-cid].include?(i.convention) }
127
+ return [identifier.convention, identifier.value] if identifier
128
+
129
+ name = molecule.names.first
130
+ return ["name", name.content] if name
131
+
132
+ nil
133
+ end
134
+
84
135
  private
85
136
 
86
137
  def title_base(substance)
data/lib/asciichem/cli.rb CHANGED
@@ -168,6 +168,31 @@ module AsciiChem
168
168
  exit 1
169
169
  end
170
170
 
171
+ desc "identity -i INPUT", "Derive InChI/InChIKey locally from the structure (offline; requires an InChI engine)"
172
+ method_option :input, aliases: "-i", type: :string,
173
+ desc: "Source text (or '-' for stdin)"
174
+ method_option :file, aliases: "-f", type: :string,
175
+ desc: "Read source from a file"
176
+ method_option :from, type: :string, default: "asciichem",
177
+ desc: "Input grammar: asciichem|smiles|molfile"
178
+ method_option :engine_bin, type: :string,
179
+ desc: "Path to the inchi-1 binary (overrides the configured engine)"
180
+ def identity
181
+ formula = ingest(read_source, options[:from])
182
+ molecules = molecules_in(formula)
183
+ unless molecules.length == 1
184
+ raise AsciiChem::Error, "identity derivation needs exactly one molecule, got #{molecules.length}"
185
+ end
186
+
187
+ engine = options[:engine_bin] ? AsciiChem::Inchi::BinaryEngine.new(bin: options[:engine_bin]) : nil
188
+ derived = AsciiChem::Inchi.identity_for(molecules.first, engine: engine)
189
+ puts derived.inchi
190
+ puts derived.inchikey if derived.inchikey
191
+ rescue AsciiChem::EngineMissingError, AsciiChem::Error => e
192
+ warn "Identity error: #{e.message}"
193
+ exit 5
194
+ end
195
+
171
196
  private
172
197
 
173
198
  def read_source
@@ -206,6 +231,19 @@ module AsciiChem
206
231
  AsciiChem::Formatter.render(format.to_sym, formula)
207
232
  end
208
233
 
234
+ # Molecules anywhere at the top level: direct formula nodes plus
235
+ # reaction terms. Identity derivation needs exactly one molecule.
236
+ def molecules_in(formula)
237
+ formula.nodes.flat_map do |node|
238
+ case node
239
+ when AsciiChem::Model::Reaction then node.reactants + node.products
240
+ when AsciiChem::Model::ReactionCascade
241
+ node.steps.flat_map { |step| step.reactants + step.products }
242
+ else [node]
243
+ end
244
+ end.grep(AsciiChem::Model::Molecule)
245
+ end
246
+
209
247
  def output_lint(diagnostics, format)
210
248
  case format.to_s
211
249
  when "json" then output_lint_json(diagnostics)
@@ -6,4 +6,9 @@ module AsciiChem
6
6
  class ParseError < Error; end
7
7
 
8
8
  class FormatError < Error; end
9
+
10
+ # No InChI engine is configured (or the engine binary is absent).
11
+ # Identity derivation is opt-in; the message always carries install
12
+ # guidance (TODO.v2 10: never a silent fallback or homemade InChI).
13
+ class EngineMissingError < Error; end
9
14
  end
@@ -186,7 +186,7 @@ module AsciiChem
186
186
  def visit_electron_configuration(ec)
187
187
  mrow = el("mrow")
188
188
  ec.orbitals.each_with_index do |(orbital, occupancy), index|
189
- mrow.add_child(mo("&#xA0;")) if index.positive?
189
+ mrow.add_child(mo(" ")) if index.positive?
190
190
  msup = el("msup")
191
191
  msup.add_child(mi(orbital))
192
192
  msup.add_child(mn(occupancy))
@@ -338,25 +338,21 @@ module AsciiChem
338
338
  # Render a reaction-condition string. The condition is captured
339
339
  # as raw text by the grammar, but chemists expect `_N` and `^N`
340
340
  # patterns to render as proper sub/superscripts. We parse the
341
- # condition as AsciiChem and use its MathML output. If the parse
342
- # fails (e.g. the condition is free-form prose), fall back to
343
- # plain <mtext>.
341
+ # condition as AsciiChem and render it in-place with this same
342
+ # formatter (no serialize/reparse round-trip, so no stray xmlns
343
+ # or reset indentation). If the parse fails (e.g. the condition
344
+ # is free-form prose), fall back to plain <mtext>.
344
345
  def render_condition(text)
345
346
  return mtext("") if text.nil? || text.empty?
346
347
 
347
348
  begin
348
- inner = AsciiChem.parse(text).to_mathml
349
- parsed = Nokogiri::XML(inner)
350
- math = parsed.at_xpath("//m:math", m: MATHML_NS)
351
- if math
352
- mrow = el("mrow")
353
- math.children.each { |c| mrow.add_child(c.dup) }
354
- return mrow
355
- end
349
+ formula = AsciiChem.parse(text)
350
+ mrow = el("mrow")
351
+ formula.nodes.each { |n| mrow.add_child(render_node(n)) }
352
+ mrow
356
353
  rescue AsciiChem::ParseError, AsciiChem::Error
357
- # fall through to plain text
354
+ mtext(text)
358
355
  end
359
- mtext(text)
360
356
  end
361
357
 
362
358
  def wrap_in_sub(base, sub)
@@ -38,7 +38,7 @@ module AsciiChem
38
38
  molecule.labels.each { |l| parts << %(@label("#{l.value}")) if l.value }
39
39
  molecule.properties.each { |p| parts << %(@#{p.title}("#{p.value}")) if p.title && p.value }
40
40
  molecule.metadata.each { |m| parts << %(@meta("#{m.name}","#{m.content}")) }
41
- parts.empty? ? "" : " #{parts.join}"
41
+ parts.empty? ? "" : " #{parts.join(" ")}"
42
42
  end
43
43
 
44
44
  def visit_atom(atom)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module AsciiChem
6
+ module Inchi
7
+ # Engine that shells out to the standalone IUPAC `inchi-1`
8
+ # binary. The binary is a documented external dependency — nothing
9
+ # is vendored (vendoring the InChI library requires the licence
10
+ # review recorded in TODO.v2 10, a maintainer decision).
11
+ #
12
+ # AsciiChem::Inchi.engine = AsciiChem::Inchi::BinaryEngine.new
13
+ # AsciiChem::Inchi.engine = AsciiChem::Inchi::BinaryEngine.new(bin: "/usr/local/bin/inchi-1")
14
+ class BinaryEngine < Engine
15
+ DEFAULT_FLAGS = %w[-STDIO -AuxNone -NoLabels -Key].freeze
16
+
17
+ attr_reader :bin, :flags
18
+
19
+ def initialize(bin: 'inchi-1', flags: DEFAULT_FLAGS)
20
+ super()
21
+ @bin = bin
22
+ @flags = flags.freeze
23
+ end
24
+
25
+ # model → molfile → inchi-1 → Identity. The molfile emission is
26
+ # the pipe: atom coordinates, charges and isotopes cross into
27
+ # the engine through the V2000 block.
28
+ def identity(molecule)
29
+ molfile = AsciiChem::Molfile.write(molecule)
30
+ out = execute(molfile)
31
+ inchi = out[/^InChI=\S+/]
32
+ raise AsciiChem::Error, "#{bin} produced no InChI:\n#{out}" unless inchi
33
+
34
+ inchikey = out[/^InChIKey=(\S+)/, 1]
35
+ raise AsciiChem::Error, "#{bin} produced no InChIKey (pass -Key):\n#{out}" unless inchikey
36
+
37
+ Identity.new(inchi: inchi, inchikey: inchikey)
38
+ end
39
+
40
+ private
41
+
42
+ def execute(input)
43
+ out, err, status = Open3.capture3(bin, *flags, stdin_data: input)
44
+ unless status.success?
45
+ raise AsciiChem::Error, "#{bin} failed (exit #{status.exitstatus}): #{(err + out).strip[0, 500]}"
46
+ end
47
+
48
+ out
49
+ rescue Errno::ENOENT
50
+ raise EngineMissingError, "InChI engine binary #{bin.inspect} not found — #{AsciiChem::Inchi::INSTALL_GUIDE}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Inchi
5
+ # Abstract identity engine. Subclasses wrap an InChI
6
+ # implementation (IUPAC software, RDKit, WASM build) behind one
7
+ # contract. Registering an engine is how a user opts in to local
8
+ # identity derivation — the core gem stays dependency-free.
9
+ class Engine
10
+ # The single engine obligation: return the Identity of a
11
+ # Model::Molecule.
12
+ def identity(_molecule)
13
+ raise NotImplementedError, "#{self.class} must implement #identity"
14
+ end
15
+
16
+ # Standard InChI string of the molecule (TODO.v2 10 interface).
17
+ def to_inchi(molecule, standard: true)
18
+ identity = identity(molecule)
19
+ return identity.inchi if standard
20
+
21
+ raise ArgumentError, "#{self.class} derives standard InChI only"
22
+ end
23
+
24
+ # InChIKey of the molecule.
25
+ def to_inchikey(molecule)
26
+ identity(molecule).inchikey
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Inchi
5
+ # Derived identity of a structure: the standard InChI string and
6
+ # its InChIKey. Value object returned by engines.
7
+ Identity = Struct.new(:inchi, :inchikey, keyword_init: true)
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ # Local identity derivation (TODO.v2 10; TODO.impl 48).
5
+ #
6
+ # The InChI algorithm is never reimplemented: engines wrap the IUPAC
7
+ # reference software (the standalone `inchi-1` binary here). The pipe
8
+ # is Model::Molecule → molfile → engine → InChI/InChIKey.
9
+ #
10
+ # Engines are opt-in: set `AsciiChem::Inchi.engine` (or pass one per
11
+ # call). Without an engine every derivation raises EngineMissingError
12
+ # with install guidance — no silent fallback.
13
+ module Inchi
14
+ autoload :BinaryEngine, 'asciichem/inchi/binary_engine'
15
+ autoload :Engine, 'asciichem/inchi/engine'
16
+ autoload :Identity, 'asciichem/inchi/identity'
17
+
18
+ INSTALL_GUIDE = 'install the IUPAC InChI software ' \
19
+ '(https://www.inchi-trust.org/downloads/) and set ' \
20
+ 'AsciiChem::Inchi.engine = AsciiChem::Inchi::BinaryEngine.new ' \
21
+ '(or pass bin: "path/to/inchi-1")'
22
+
23
+ class << self
24
+ attr_accessor :engine
25
+
26
+ # Derives the identity of one molecule. Falls back to the
27
+ # configured engine when none is passed; raises
28
+ # EngineMissingError when neither exists.
29
+ def identity_for(molecule, engine: nil)
30
+ selected = engine || self.engine
31
+ raise EngineMissingError, "no InChI engine configured — #{INSTALL_GUIDE}" if selected.nil?
32
+
33
+ selected.identity(molecule)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiChem
4
+ module Linter
5
+ # Structural cross-check of identity annotations (TODO.v2 10).
6
+ # Where IdentifierConsistencyCheck compares offline-analysable
7
+ # layers (formula composition), this check derives the InChI of
8
+ # the drawn structure via the configured engine and compares it
9
+ # to the annotated `@inchi`/`@inchikey` — catching transcription
10
+ # errors that composition checks cannot (wrong connectivity,
11
+ # stereo, isotopes).
12
+ #
13
+ # Behaviour is engine-scaled, mirroring the resolver's opt-in
14
+ # sources: with no engine configured the check emits one warning
15
+ # explaining how to enable it (protection off is worth saying);
16
+ # with an engine, mismatches are errors. Molecules without
17
+ # identity annotations are never checked.
18
+ class IdentityCrossCheck < Base
19
+ register :identity_cross_check
20
+
21
+ def run(formula)
22
+ annotated = []
23
+ walk(formula) do |node|
24
+ next unless node.is_a?(AsciiChem::Model::Molecule)
25
+
26
+ annotated << node if identity_annotations?(node)
27
+ end
28
+ return [] if annotated.empty?
29
+
30
+ engine = AsciiChem::Inchi.engine
31
+ return [warning(skip_message(annotated.length))] if engine.nil?
32
+
33
+ annotated.flat_map { |molecule| cross_check(molecule, engine) }
34
+ rescue AsciiChem::EngineMissingError => e
35
+ [warning(e.message)]
36
+ end
37
+
38
+ private
39
+
40
+ def identity_annotations?(molecule)
41
+ molecule.identifiers.any? { |i| %w[inchi inchikey].include?(i.convention.to_s) }
42
+ end
43
+
44
+ def cross_check(molecule, engine)
45
+ identity = engine.identity(molecule)
46
+ molecule.identifiers.filter_map do |i|
47
+ diagnostic_for(i, identity, molecule)
48
+ end
49
+ rescue AsciiChem::Error => e
50
+ # Not derivable (e.g. a formula is not a structure: no bonds)
51
+ # or the engine rejected it — report, never raise: the linter
52
+ # stays total.
53
+ [warning("cannot derive InChI for cross-check: #{e.message}", node: molecule)]
54
+ end
55
+
56
+ def diagnostic_for(identifier, identity, molecule)
57
+ case identifier.convention.to_s
58
+ when 'inchi'
59
+ return nil if identifier.value == identity.inchi
60
+
61
+ error('InChI annotation does not match the drawn structure: ' \
62
+ "annotated #{identifier.value}, computed #{identity.inchi}",
63
+ node: molecule)
64
+ when 'inchikey'
65
+ return nil if identifier.value == identity.inchikey
66
+
67
+ error('InChIKey annotation does not match the drawn structure: ' \
68
+ "annotated #{identifier.value}, computed #{identity.inchikey}",
69
+ node: molecule)
70
+ end
71
+ end
72
+
73
+ def skip_message(count)
74
+ "structural identity cross-check skipped for #{count} annotated molecule(s): " \
75
+ "no InChI engine configured — #{AsciiChem::Inchi::INSTALL_GUIDE}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -18,6 +18,7 @@ module AsciiChem
18
18
  autoload :ElementValidationCheck, "asciichem/linter/element_validation_check"
19
19
  autoload :IdentifierConsistencyCheck, "asciichem/linter/identifier_consistency_check"
20
20
  autoload :IdentifierFormatCheck, "asciichem/linter/identifier_format_check"
21
+ autoload :IdentityCrossCheck, "asciichem/linter/identity_cross_check"
21
22
  autoload :Diagnostic, "asciichem/linter/diagnostic"
22
23
  autoload :IsotopeSanityCheck, "asciichem/linter/isotope_sanity_check"
23
24
  autoload :Registry, "asciichem/linter/registry"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.25.0"
4
+ VERSION = "0.27.0"
5
5
  end
@@ -63,10 +63,12 @@ module AsciiChem
63
63
 
64
64
  attribute :nodes, Base, collection: true
65
65
  attribute :coefficient, :string
66
+ attribute :stereo, :string
66
67
  attribute :identifiers, Identifier, collection: true
67
68
  json do
68
69
  map "nodes", to: :nodes
69
70
  map "coefficient", to: :coefficient
71
+ map "stereo", to: :stereo
70
72
  map "identifiers", to: :identifiers
71
73
  end
72
74
  end
@@ -136,10 +136,25 @@ module AsciiChem
136
136
  type: "molecule",
137
137
  nodes: node.nodes.map { |n| to_wire(n) },
138
138
  coefficient: node.coefficient,
139
+ stereo: stereo_to_wire(node.stereo),
139
140
  identifiers: node.identifiers.map { |i| identifier_to_wire(i) }
140
141
  )
141
142
  end
142
143
 
144
+ # Stereo markers cross the wire as the v1 enum strings
145
+ # ("R", "alpha", ...); the model carries symbols.
146
+ def stereo_to_wire(stereo)
147
+ return nil unless stereo
148
+
149
+ AsciiChem::Model::Molecule::STEREO_TO_LETTER.fetch(stereo)
150
+ end
151
+
152
+ def stereo_from_wire(stereo)
153
+ return nil unless stereo
154
+
155
+ AsciiChem::Model::Molecule::STEREO_LETTERS.fetch(stereo.to_s)
156
+ end
157
+
143
158
  def group_to_wire(node)
144
159
  AsciiChem::Wire::Group.new(
145
160
  type: "group",
@@ -295,6 +310,7 @@ module AsciiChem
295
310
  AsciiChem::Model::Molecule.new(
296
311
  nodes: Array(wire.nodes).map { |n| from_wire(n) },
297
312
  coefficient: wire.coefficient,
313
+ stereo: stereo_from_wire(wire.stereo),
298
314
  identifiers: Array(wire.identifiers).map { |i| identifier_from_wire(i) }
299
315
  )
300
316
  end
data/lib/asciichem.rb CHANGED
@@ -15,10 +15,12 @@ module AsciiChem
15
15
  autoload :Error, "asciichem/errors"
16
16
  autoload :ParseError, "asciichem/errors"
17
17
  autoload :FormatError, "asciichem/errors"
18
+ autoload :EngineMissingError, "asciichem/errors"
18
19
  autoload :Formatter, "asciichem/formatter"
19
20
  autoload :Grammar, "asciichem/grammar"
20
21
  autoload :Greek, "asciichem/greek"
21
22
  autoload :Identifiers, "asciichem/identifiers"
23
+ autoload :Inchi, "asciichem/inchi"
22
24
  autoload :Layout, "asciichem/layout"
23
25
  autoload :Linter, "asciichem/linter"
24
26
  autoload :Model, "asciichem/model"
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.25.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -206,6 +206,10 @@ files:
206
206
  - lib/asciichem/identifiers/inchi.rb
207
207
  - lib/asciichem/identifiers/inchikey.rb
208
208
  - lib/asciichem/identifiers/smiles.rb
209
+ - lib/asciichem/inchi.rb
210
+ - lib/asciichem/inchi/binary_engine.rb
211
+ - lib/asciichem/inchi/engine.rb
212
+ - lib/asciichem/inchi/identity.rb
209
213
  - lib/asciichem/layout.rb
210
214
  - lib/asciichem/linter.rb
211
215
  - lib/asciichem/linter/balance_check.rb
@@ -217,6 +221,7 @@ files:
217
221
  - lib/asciichem/linter/element_validation_check.rb
218
222
  - lib/asciichem/linter/identifier_consistency_check.rb
219
223
  - lib/asciichem/linter/identifier_format_check.rb
224
+ - lib/asciichem/linter/identity_cross_check.rb
220
225
  - lib/asciichem/linter/isotope_sanity_check.rb
221
226
  - lib/asciichem/linter/registry.rb
222
227
  - lib/asciichem/linter/spectrum_peak_check.rb