asciichem 0.26.0 → 0.28.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 +4 -4
- data/CHANGELOG.md +45 -0
- data/asciichem.gemspec +1 -0
- data/lib/asciichem/cli.rb +38 -0
- data/lib/asciichem/errors.rb +5 -0
- data/lib/asciichem/formatter/mathml.rb +38 -23
- data/lib/asciichem/inchi/binary_engine.rb +54 -0
- data/lib/asciichem/inchi/engine.rb +30 -0
- data/lib/asciichem/inchi/identity.rb +9 -0
- data/lib/asciichem/inchi.rb +37 -0
- data/lib/asciichem/linter/identity_cross_check.rb +79 -0
- data/lib/asciichem/linter.rb +1 -0
- data/lib/asciichem/version.rb +1 -1
- data/lib/asciichem/wire/core.rb +2 -0
- data/lib/asciichem/wire_adapter.rb +16 -0
- data/lib/asciichem.rb +2 -0
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fb92ff1cd35eeb546372b2ac9c3cf97b908971409e9a1d473b575479464e356
|
|
4
|
+
data.tar.gz: 2cc3be9f85b7ca2d7e3fe773e981073b6a755344cc6b593b1940666653fb52e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d2f065122017d83a4e0fdb5a9da9db50a9dbd321e335a9f49bce5c3d253e78ae11f7ae33899ad4ce60fa28b9053a085f4cac7acefbd22999c88287fc0d38d2d
|
|
7
|
+
data.tar.gz: 369b73e26be9c6234fd36c6a2e5d14b53a8746a5fa44c7bc5949297cc17978e6a135e4f003a90641ad7e63a2ce4dd400e05107bb79157628028f14bc68104ac2
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
All notable changes to AsciiChem are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.28.0] - 2026-09-14
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- Embedded-math MathML now crosses through the mml gem
|
|
10
|
+
(plurimath/mml) in both directions: Plurimath output is parsed
|
|
11
|
+
into a typed Mml::V3 graph and re-serialized by the framework
|
|
12
|
+
before grafting - replacing xpath surgery on the raw string.
|
|
13
|
+
mml (~> 2.3) is now a direct dependency (previously transitive
|
|
14
|
+
via plurimath).
|
|
15
|
+
- New spec contract: every emitted MathML (and every shared-corpus
|
|
16
|
+
golden) parses back through Mml.parse - output is valid per the
|
|
17
|
+
typed MathML contract model, not just well-formed XML.
|
|
18
|
+
|
|
19
|
+
## [0.27.0] - 2026-09-14
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Local InChI identity engine (TODO.impl 48 / TODO.v2 10): opt-in
|
|
23
|
+
`AsciiChem::Inchi` abstraction over the IUPAC `inchi-1` binary
|
|
24
|
+
(never reimplemented). `EngineMissingError` carries install
|
|
25
|
+
guidance. `IdentityCrossCheck` linter compares annotated
|
|
26
|
+
`@inchi`/`@inchikey` against the drawn structure (error with
|
|
27
|
+
engine, one guidance warning without). CLI: `asciichem identity`.
|
|
28
|
+
- L2 MathML golden conformance: corpus suite pinned against the
|
|
29
|
+
reference goldens; report claims the level.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Electron-configuration MathML separator is a real U+00A0 (was a
|
|
33
|
+
double-escaped entity artifact).
|
|
34
|
+
- Reaction conditions render in-place through the same MathML
|
|
35
|
+
formatter (was serialize → reparse → graft, which leaked xmlns and
|
|
36
|
+
reset indentation).
|
|
37
|
+
- Molecule stereo markers (`(R)-`, `(alpha)-`, …) survive the v1
|
|
38
|
+
wire form (additive optional `stereo` field; paired with
|
|
39
|
+
asciichem-model#13).
|
|
40
|
+
|
|
6
41
|
## [0.26.0] - 2026-09-14
|
|
7
42
|
|
|
8
43
|
### Added
|
|
@@ -483,3 +518,13 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
483
518
|
[0.3.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.2.0...v0.3.0
|
|
484
519
|
[0.2.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.1.0...v0.2.0
|
|
485
520
|
[0.1.0]: https://github.com/asciichem/asciichem-ruby/releases/tag/v0.1.0
|
|
521
|
+
### Changed
|
|
522
|
+
- Embedded-math MathML now crosses through the mml gem (plurimath/mml)
|
|
523
|
+
in both directions: Plurimath output is parsed into a typed
|
|
524
|
+
Mml::V3 graph and re-serialized by the framework before grafting —
|
|
525
|
+
replacing xpath surgery on the raw string. mml (~> 2.3) is now a
|
|
526
|
+
direct dependency (previously transitive via plurimath).
|
|
527
|
+
- New spec contract: every emitted MathML (and every shared-corpus
|
|
528
|
+
golden) parses back through Mml.parse — output is valid per the
|
|
529
|
+
typed MathML contract model, not just well-formed XML.
|
|
530
|
+
|
data/asciichem.gemspec
CHANGED
|
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
|
|
|
36
36
|
spec.add_dependency "chemicalml", "~> 0.3.0"
|
|
37
37
|
spec.add_dependency "elkrb", "~> 1.0"
|
|
38
38
|
spec.add_dependency "lutaml-model", ">= 0.8", "< 2"
|
|
39
|
+
spec.add_dependency "mml", "~> 2.3"
|
|
39
40
|
spec.add_dependency "nokogiri", "~> 1.16"
|
|
40
41
|
spec.add_dependency "parslet", "~> 2.0"
|
|
41
42
|
spec.add_dependency "relaton-bib", ">= 0.1", "< 2"
|
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)
|
data/lib/asciichem/errors.rb
CHANGED
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "nokogiri"
|
|
4
|
+
require "mml"
|
|
4
5
|
|
|
5
6
|
module AsciiChem
|
|
6
7
|
module Formatter
|
|
@@ -186,7 +187,7 @@ module AsciiChem
|
|
|
186
187
|
def visit_electron_configuration(ec)
|
|
187
188
|
mrow = el("mrow")
|
|
188
189
|
ec.orbitals.each_with_index do |(orbital, occupancy), index|
|
|
189
|
-
mrow.add_child(mo("
|
|
190
|
+
mrow.add_child(mo(" ")) if index.positive?
|
|
190
191
|
msup = el("msup")
|
|
191
192
|
msup.add_child(mi(orbital))
|
|
192
193
|
msup.add_child(mn(occupancy))
|
|
@@ -196,17 +197,27 @@ module AsciiChem
|
|
|
196
197
|
end
|
|
197
198
|
|
|
198
199
|
def visit_embedded_math(em)
|
|
199
|
-
#
|
|
200
|
-
# into
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# Detach children into a fresh <mrow>.
|
|
200
|
+
# Embedded math crosses through the mml gem in both directions:
|
|
201
|
+
# Plurimath's MathML string is parsed into a typed Mml::V3
|
|
202
|
+
# graph (the MathML contract model), and each top-level child
|
|
203
|
+
# is re-serialized by the framework before grafting — never
|
|
204
|
+
# ad-hoc xpath surgery on the string.
|
|
205
|
+
math = Mml.parse(em.formula.to_mathml, version: 3)
|
|
207
206
|
mrow = el("mrow")
|
|
208
|
-
math.
|
|
207
|
+
math.element_order.each do |entry|
|
|
208
|
+
next unless entry.respond_to?(:name)
|
|
209
|
+
|
|
210
|
+
attr_name = xml_element_attribute(math.class, entry.name)
|
|
211
|
+
next unless attr_name
|
|
212
|
+
|
|
213
|
+
Array(math.public_send(attr_name)).each do |child|
|
|
214
|
+
fragment = Nokogiri::XML::DocumentFragment.parse(child.to_xml)
|
|
215
|
+
fragment.children.each { |node| mrow.add_child(node) }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
209
218
|
mrow
|
|
219
|
+
rescue Mml::Error, Lutaml::Model::Error, Nokogiri::XML::SyntaxError
|
|
220
|
+
el("mrow")
|
|
210
221
|
end
|
|
211
222
|
|
|
212
223
|
def visit_text(text)
|
|
@@ -309,6 +320,14 @@ module AsciiChem
|
|
|
309
320
|
|
|
310
321
|
private
|
|
311
322
|
|
|
323
|
+
# Symbol of the model attribute an XML child element name maps
|
|
324
|
+
# to on a Mml class (e.g. "mstyle" => :mstyle_value), via the
|
|
325
|
+
# lutaml-model XML mapping — no name-munging.
|
|
326
|
+
def xml_element_attribute(klass, element_name)
|
|
327
|
+
rule = klass.mappings_for(:xml).elements.find { |r| r.name == element_name }
|
|
328
|
+
rule&.to
|
|
329
|
+
end
|
|
330
|
+
|
|
312
331
|
def render_node(node)
|
|
313
332
|
node.accept(self)
|
|
314
333
|
end
|
|
@@ -338,25 +357,21 @@ module AsciiChem
|
|
|
338
357
|
# Render a reaction-condition string. The condition is captured
|
|
339
358
|
# as raw text by the grammar, but chemists expect `_N` and `^N`
|
|
340
359
|
# patterns to render as proper sub/superscripts. We parse the
|
|
341
|
-
# condition as AsciiChem and
|
|
342
|
-
#
|
|
343
|
-
#
|
|
360
|
+
# condition as AsciiChem and render it in-place with this same
|
|
361
|
+
# formatter (no serialize/reparse round-trip, so no stray xmlns
|
|
362
|
+
# or reset indentation). If the parse fails (e.g. the condition
|
|
363
|
+
# is free-form prose), fall back to plain <mtext>.
|
|
344
364
|
def render_condition(text)
|
|
345
365
|
return mtext("") if text.nil? || text.empty?
|
|
346
366
|
|
|
347
367
|
begin
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
mrow = el("mrow")
|
|
353
|
-
math.children.each { |c| mrow.add_child(c.dup) }
|
|
354
|
-
return mrow
|
|
355
|
-
end
|
|
368
|
+
formula = AsciiChem.parse(text)
|
|
369
|
+
mrow = el("mrow")
|
|
370
|
+
formula.nodes.each { |n| mrow.add_child(render_node(n)) }
|
|
371
|
+
mrow
|
|
356
372
|
rescue AsciiChem::ParseError, AsciiChem::Error
|
|
357
|
-
|
|
373
|
+
mtext(text)
|
|
358
374
|
end
|
|
359
|
-
mtext(text)
|
|
360
375
|
end
|
|
361
376
|
|
|
362
377
|
def wrap_in_sub(base, sub)
|
|
@@ -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,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
|
data/lib/asciichem/linter.rb
CHANGED
|
@@ -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"
|
data/lib/asciichem/version.rb
CHANGED
data/lib/asciichem/wire/core.rb
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -57,6 +57,20 @@ dependencies:
|
|
|
57
57
|
- - "<"
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '2'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: mml
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '2.3'
|
|
67
|
+
type: :runtime
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '2.3'
|
|
60
74
|
- !ruby/object:Gem::Dependency
|
|
61
75
|
name: nokogiri
|
|
62
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -206,6 +220,10 @@ files:
|
|
|
206
220
|
- lib/asciichem/identifiers/inchi.rb
|
|
207
221
|
- lib/asciichem/identifiers/inchikey.rb
|
|
208
222
|
- lib/asciichem/identifiers/smiles.rb
|
|
223
|
+
- lib/asciichem/inchi.rb
|
|
224
|
+
- lib/asciichem/inchi/binary_engine.rb
|
|
225
|
+
- lib/asciichem/inchi/engine.rb
|
|
226
|
+
- lib/asciichem/inchi/identity.rb
|
|
209
227
|
- lib/asciichem/layout.rb
|
|
210
228
|
- lib/asciichem/linter.rb
|
|
211
229
|
- lib/asciichem/linter/balance_check.rb
|
|
@@ -217,6 +235,7 @@ files:
|
|
|
217
235
|
- lib/asciichem/linter/element_validation_check.rb
|
|
218
236
|
- lib/asciichem/linter/identifier_consistency_check.rb
|
|
219
237
|
- lib/asciichem/linter/identifier_format_check.rb
|
|
238
|
+
- lib/asciichem/linter/identity_cross_check.rb
|
|
220
239
|
- lib/asciichem/linter/isotope_sanity_check.rb
|
|
221
240
|
- lib/asciichem/linter/registry.rb
|
|
222
241
|
- lib/asciichem/linter/spectrum_peak_check.rb
|