asciichem 0.19.0 → 0.20.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/.github/workflows/ci.yml +9 -0
- data/.github/workflows/release.yml +7 -17
- data/.gitignore +1 -0
- data/CHANGELOG.md +18 -1
- data/RELEASING.md +9 -4
- data/asciichem.gemspec +3 -0
- data/lib/asciichem/model/node.rb +8 -0
- data/lib/asciichem/version.rb +1 -1
- data/lib/asciichem/wire/base.rb +25 -0
- data/lib/asciichem/wire/chemistry.rb +98 -0
- data/lib/asciichem/wire/core.rb +92 -0
- data/lib/asciichem/wire/extended.rb +136 -0
- data/lib/asciichem/wire/identity.rb +63 -0
- data/lib/asciichem/wire.rb +52 -0
- data/lib/asciichem/wire_adapter.rb +361 -0
- data/lib/asciichem.rb +9 -0
- metadata +43 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a77da515ac52e848a4ff0f5855a595cb81d71a36c18b4991d875d682d2c3598
|
|
4
|
+
data.tar.gz: 70c14be9ddec1e767ddddef09a1ad91a02f996f1fdd22645d0269c4de7e96020
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b71e29edd758d5d4393d750a95676dc8007c5850f830b414ca83a2d21e99c1ff39fa88f5f66b54198e442e7d29b9010639c4a55138567cd9dee8160d5b2be3dc
|
|
7
|
+
data.tar.gz: f08f835457ce6d58b70ff0b3bc502f84f4404684a13ae60ad52bbae44a790dfd66621c15bba73ed2843e0d76ba4ef47545a37e838867b54d12eb9246f11277b6
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -14,8 +14,17 @@ jobs:
|
|
|
14
14
|
ruby: ["3.3", "3.4"]
|
|
15
15
|
steps:
|
|
16
16
|
- uses: actions/checkout@v4
|
|
17
|
+
- name: Clone conformance corpus (asciichem-tests)
|
|
18
|
+
run: git clone --depth 1 https://github.com/asciichem/asciichem-tests.git ../asciichem-tests
|
|
17
19
|
- uses: ruby/setup-ruby@v1
|
|
18
20
|
with:
|
|
19
21
|
ruby-version: ${{ matrix.ruby }}
|
|
20
22
|
bundler-cache: true
|
|
21
23
|
- run: bundle exec rspec
|
|
24
|
+
- name: Publish conformance report
|
|
25
|
+
if: matrix.ruby == '3.4'
|
|
26
|
+
uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: conformance
|
|
29
|
+
path: conformance.json
|
|
30
|
+
if-no-files-found: warn
|
|
@@ -11,14 +11,17 @@ on:
|
|
|
11
11
|
jobs:
|
|
12
12
|
release:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
|
-
|
|
14
|
+
# Trusted publishing (OIDC): the publisher registered on RubyGems.org
|
|
15
|
+
# is repository asciichem/asciichem-ruby + workflow release.yml, with
|
|
16
|
+
# no environment — so this job must not claim one.
|
|
15
17
|
permissions:
|
|
16
18
|
contents: read
|
|
17
|
-
|
|
19
|
+
id-token: write
|
|
18
20
|
steps:
|
|
19
21
|
- uses: actions/checkout@v4
|
|
20
22
|
with:
|
|
21
23
|
ref: main
|
|
24
|
+
persist-credentials: false
|
|
22
25
|
- name: Verify version matches input
|
|
23
26
|
run: |
|
|
24
27
|
actual=$(ruby -e 'require "./lib/asciichem/version"; print AsciiChem::VERSION')
|
|
@@ -31,21 +34,8 @@ jobs:
|
|
|
31
34
|
with:
|
|
32
35
|
ruby-version: "3.4"
|
|
33
36
|
bundler-cache: true
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- name: Set up RubyGems credentials
|
|
37
|
-
env:
|
|
38
|
-
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
39
|
-
run: |
|
|
40
|
-
mkdir -p $HOME/.gem
|
|
41
|
-
cat > $HOME/.gem/credentials <<EOF
|
|
42
|
-
---
|
|
43
|
-
:rubygems_api_key: ${RUBYGEMS_API_KEY}
|
|
44
|
-
EOF
|
|
45
|
-
chmod 0600 $HOME/.gem/credentials
|
|
46
|
-
- name: Push to RubyGems
|
|
47
|
-
run: |
|
|
48
|
-
gem push pkg/asciichem-${{ inputs.version }}.gem
|
|
37
|
+
# Builds and pushes using the GitHub OIDC identity — no API keys.
|
|
38
|
+
- uses: rubygems/release-gem@v1
|
|
49
39
|
- name: Summary
|
|
50
40
|
run: |
|
|
51
41
|
echo "Released asciichem ${{ inputs.version }} to RubyGems"
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to AsciiChem are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.20.0] - 2026-09-12
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Canonical JSON wire form (`to_model_json` / `AsciiChem.from_model_json`)
|
|
10
|
+
per asciichem-model v1: `AsciiChem::Wire` (lutaml-model Serializable
|
|
11
|
+
classes, json mappings only - no hand-rolled serialization) bridged by
|
|
12
|
+
`AsciiChem::WireAdapter` (model-to-model conversion, same pattern as
|
|
13
|
+
the CML ModelAdapter). Emission covers every node type; ingestion
|
|
14
|
+
covers the lossless core set (beyond-formulas nodes are emission-only
|
|
15
|
+
until their corpus round-trip acceptance lands).
|
|
16
|
+
- Conformance runner over the shared corpus (asciichem-tests): L0
|
|
17
|
+
emission + schema validation, L1 Text round-trip, L3 CML round-trip,
|
|
18
|
+
L4 linter diagnostics, plus the ParseError contract for rejects.
|
|
19
|
+
Emits conformance.json; CI clones the corpus and publishes the report
|
|
20
|
+
as an artifact. Current claim: L0 149/149, L1 16/16, L3 23/23, L4 6/6.
|
|
21
|
+
|
|
6
22
|
## [0.19.0] - 2026-09-09
|
|
7
23
|
|
|
8
24
|
### Added
|
|
@@ -347,7 +363,8 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
347
363
|
`version`.
|
|
348
364
|
- Comprehensive RSpec suite with round-trip conformance.
|
|
349
365
|
|
|
350
|
-
[Unreleased]: https://github.com/asciichem/asciichem-ruby/
|
|
366
|
+
[Unreleased]: https://github.com/asciichem/asciichem-ruby/compare/v0.20.0...HEAD
|
|
367
|
+
[0.20.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.19.0...v0.20.0
|
|
351
368
|
[0.18.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.18.0...v0.18.1
|
|
352
369
|
[0.18.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.17.0...v0.18.0
|
|
353
370
|
[0.17.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.16.0...v0.17.0
|
data/RELEASING.md
CHANGED
|
@@ -83,10 +83,15 @@ tag.
|
|
|
83
83
|
|
|
84
84
|
== Publish to RubyGems
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
The Release workflow (`.github/workflows/release.yml`) publishes via
|
|
87
|
+
https://guides.rubygems.org/trusted-publishing/[RubyGems trusted publishing]
|
|
88
|
+
(OIDC) — there are no API keys. Its trusted publisher is registered on
|
|
89
|
+
https://rubygems.org (repository `asciichem/asciichem-ruby`, workflow
|
|
90
|
+
`release.yml`, no environment).
|
|
91
|
+
|
|
92
|
+
On the repository's Actions tab, run the "Release" workflow with the
|
|
93
|
+
version number. It verifies the version matches `version.rb`, builds
|
|
94
|
+
the gem, and pushes it using the workflow's GitHub OIDC identity.
|
|
90
95
|
|
|
91
96
|
Verify at https://rubygems.org/gems/asciichem.
|
|
92
97
|
|
data/asciichem.gemspec
CHANGED
|
@@ -35,8 +35,11 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
|
|
36
36
|
spec.add_dependency "chemicalml", "~> 0.3.0"
|
|
37
37
|
spec.add_dependency "elkrb", "~> 1.0"
|
|
38
|
+
spec.add_dependency "lutaml-model", ">= 0.8", "< 2"
|
|
38
39
|
spec.add_dependency "nokogiri", "~> 1.16"
|
|
39
40
|
spec.add_dependency "parslet", "~> 2.0"
|
|
40
41
|
spec.add_dependency "plurimath", "~> 0.8"
|
|
41
42
|
spec.add_dependency "thor", "~> 1.3"
|
|
43
|
+
|
|
44
|
+
spec.add_development_dependency "asciichem-model", "~> 0.3.4"
|
|
42
45
|
end
|
data/lib/asciichem/model/node.rb
CHANGED
|
@@ -62,6 +62,14 @@ module AsciiChem
|
|
|
62
62
|
AsciiChem::Cml.from_asciichem(self)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Canonical JSON wire form (asciichem-model v1). The Text
|
|
66
|
+
# formatter canonicalises AsciiChem text; this canonicalises
|
|
67
|
+
# the semantic model itself — the interchange format every
|
|
68
|
+
# implementation must parse and emit.
|
|
69
|
+
def to_model_json
|
|
70
|
+
AsciiChem::WireAdapter.to_model_json(self)
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
# Subclasses override to expose the attributes that participate in
|
|
66
74
|
# equality. Default: empty (so two bare Nodes are equal).
|
|
67
75
|
def value_attributes
|
data/lib/asciichem/version.rb
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Wire
|
|
5
|
+
# Common base for wire nodes: declares the `type` discriminator
|
|
6
|
+
# attribute and registers the class under its wire type string.
|
|
7
|
+
# The converter sets `type` explicitly on construction because
|
|
8
|
+
# lutaml-model omits default values on serialisation.
|
|
9
|
+
class Base < Lutaml::Model::Serializable
|
|
10
|
+
class << self
|
|
11
|
+
def wire_type(name = nil)
|
|
12
|
+
return @wire_type if name.nil?
|
|
13
|
+
|
|
14
|
+
@wire_type = name.to_s
|
|
15
|
+
AsciiChem::Wire::REGISTRY[name.to_s] = self
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attribute :type, :string
|
|
20
|
+
json do
|
|
21
|
+
map "type", to: :type
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Wire
|
|
5
|
+
# Chemistry document wire classes: formula (root), reactions,
|
|
6
|
+
# electron configurations, embedded math, text.
|
|
7
|
+
class Formula < Base
|
|
8
|
+
wire_type "formula"
|
|
9
|
+
|
|
10
|
+
attribute :nodes, Base, collection: true
|
|
11
|
+
json do
|
|
12
|
+
map "nodes", to: :nodes
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ReactionConditions < Lutaml::Model::Serializable
|
|
17
|
+
attribute :above, :string
|
|
18
|
+
attribute :below, :string
|
|
19
|
+
json do
|
|
20
|
+
map "above", to: :above
|
|
21
|
+
map "below", to: :below
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Reaction < Base
|
|
26
|
+
wire_type "reaction"
|
|
27
|
+
|
|
28
|
+
attribute :reactants, "AsciiChem::Wire::Molecule", collection: true
|
|
29
|
+
attribute :products, "AsciiChem::Wire::Molecule", collection: true
|
|
30
|
+
attribute :arrow, :string
|
|
31
|
+
attribute :conditions, ReactionConditions
|
|
32
|
+
json do
|
|
33
|
+
map "reactants", to: :reactants
|
|
34
|
+
map "products", to: :products
|
|
35
|
+
map "arrow", to: :arrow
|
|
36
|
+
map "conditions", to: :conditions
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class ReactionCascade < Base
|
|
41
|
+
wire_type "reaction-cascade"
|
|
42
|
+
|
|
43
|
+
attribute :steps, "AsciiChem::Wire::Reaction", collection: true
|
|
44
|
+
json do
|
|
45
|
+
map "steps", to: :steps
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class OrbitalOccupancy < Lutaml::Model::Serializable
|
|
50
|
+
attribute :orbital, :string
|
|
51
|
+
attribute :occupancy, :string
|
|
52
|
+
json do
|
|
53
|
+
map "orbital", to: :orbital
|
|
54
|
+
map "occupancy", to: :occupancy
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class TermSymbol < Lutaml::Model::Serializable
|
|
59
|
+
attribute :multiplicity, :string
|
|
60
|
+
attribute :letter, :string
|
|
61
|
+
attribute :j_value, :string
|
|
62
|
+
json do
|
|
63
|
+
map "multiplicity", to: :multiplicity
|
|
64
|
+
map "letter", to: :letter
|
|
65
|
+
map "jValue", to: :j_value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class ElectronConfiguration < Base
|
|
70
|
+
wire_type "electron-configuration"
|
|
71
|
+
|
|
72
|
+
attribute :orbitals, OrbitalOccupancy, collection: true
|
|
73
|
+
attribute :term_symbol, TermSymbol
|
|
74
|
+
json do
|
|
75
|
+
map "orbitals", to: :orbitals
|
|
76
|
+
map "termSymbol", to: :term_symbol
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class EmbeddedMath < Base
|
|
81
|
+
wire_type "embedded-math"
|
|
82
|
+
|
|
83
|
+
attribute :source, :string
|
|
84
|
+
json do
|
|
85
|
+
map "source", to: :source
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class Text < Base
|
|
90
|
+
wire_type "text"
|
|
91
|
+
|
|
92
|
+
attribute :content, :string
|
|
93
|
+
json do
|
|
94
|
+
map "content", to: :content
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Wire
|
|
5
|
+
# Core node wire classes: atom, identifier, name, molecule,
|
|
6
|
+
# group, bond. Field-for-field with schemas/v1. Order matters:
|
|
7
|
+
# referenced classes are declared before their referrers.
|
|
8
|
+
class Atom < Base
|
|
9
|
+
wire_type "atom"
|
|
10
|
+
|
|
11
|
+
attribute :element, :string
|
|
12
|
+
attribute :isotope, :string
|
|
13
|
+
attribute :charge, :string
|
|
14
|
+
attribute :subscript, :string
|
|
15
|
+
attribute :oxidation_state, :string
|
|
16
|
+
attribute :lone_pairs, :integer
|
|
17
|
+
attribute :radical_electrons, :integer
|
|
18
|
+
attribute :ring_closures, :string
|
|
19
|
+
json do
|
|
20
|
+
map "element", to: :element
|
|
21
|
+
map "isotope", to: :isotope
|
|
22
|
+
map "charge", to: :charge
|
|
23
|
+
map "subscript", to: :subscript
|
|
24
|
+
map "oxidationState", to: :oxidation_state
|
|
25
|
+
map "lonePairs", to: :lone_pairs
|
|
26
|
+
map "radicalElectrons", to: :radical_electrons
|
|
27
|
+
map "ringClosures", to: :ring_closures
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Identifier < Base
|
|
32
|
+
wire_type "identifier"
|
|
33
|
+
|
|
34
|
+
attribute :value, :string
|
|
35
|
+
attribute :convention, :string
|
|
36
|
+
attribute :dict_ref, :string
|
|
37
|
+
json do
|
|
38
|
+
map "value", to: :value
|
|
39
|
+
map "convention", to: :convention
|
|
40
|
+
map "dictRef", to: :dict_ref
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Name < Base
|
|
45
|
+
wire_type "name"
|
|
46
|
+
|
|
47
|
+
attribute :content, :string
|
|
48
|
+
attribute :convention, :string
|
|
49
|
+
attribute :dict_ref, :string
|
|
50
|
+
json do
|
|
51
|
+
map "content", to: :content
|
|
52
|
+
map "convention", to: :convention
|
|
53
|
+
map "dictRef", to: :dict_ref
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Molecule < Base
|
|
58
|
+
wire_type "molecule"
|
|
59
|
+
|
|
60
|
+
attribute :nodes, Base, collection: true
|
|
61
|
+
attribute :coefficient, :string
|
|
62
|
+
attribute :identifiers, Identifier, collection: true
|
|
63
|
+
json do
|
|
64
|
+
map "nodes", to: :nodes
|
|
65
|
+
map "coefficient", to: :coefficient
|
|
66
|
+
map "identifiers", to: :identifiers
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class Group < Base
|
|
71
|
+
wire_type "group"
|
|
72
|
+
|
|
73
|
+
attribute :nodes, Base, collection: true
|
|
74
|
+
attribute :multiplicity, :string
|
|
75
|
+
attribute :bracket, :string
|
|
76
|
+
json do
|
|
77
|
+
map "nodes", to: :nodes
|
|
78
|
+
map "multiplicity", to: :multiplicity
|
|
79
|
+
map "bracket", to: :bracket
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Bond < Base
|
|
84
|
+
wire_type "bond"
|
|
85
|
+
|
|
86
|
+
attribute :kind, :string
|
|
87
|
+
json do
|
|
88
|
+
map "kind", to: :kind
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Wire
|
|
5
|
+
# Beyond-formulas wire classes: mechanism, spectrum, crystal,
|
|
6
|
+
# zmatrix, calculation. Emission covers the full v1 schemas;
|
|
7
|
+
# ingestion (from_wire) is currently implemented for the core
|
|
8
|
+
# chemistry set only — see WireAdapter.
|
|
9
|
+
class MechanismStep < Lutaml::Model::Serializable
|
|
10
|
+
attribute :label, :string
|
|
11
|
+
attribute :reaction, "AsciiChem::Wire::Reaction"
|
|
12
|
+
json do
|
|
13
|
+
map "label", to: :label
|
|
14
|
+
map "reaction", to: :reaction
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Mechanism < Base
|
|
19
|
+
wire_type "mechanism"
|
|
20
|
+
|
|
21
|
+
attribute :steps, MechanismStep, collection: true
|
|
22
|
+
attribute :spectators, "AsciiChem::Wire::Molecule", collection: true
|
|
23
|
+
json do
|
|
24
|
+
map "steps", to: :steps
|
|
25
|
+
map "spectators", to: :spectators
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class SpectrumPeak < Lutaml::Model::Serializable
|
|
30
|
+
attribute :position, :string
|
|
31
|
+
attribute :intensity, :string
|
|
32
|
+
attribute :multiplicity, :string
|
|
33
|
+
attribute :assignment, :string
|
|
34
|
+
json do
|
|
35
|
+
map "position", to: :position
|
|
36
|
+
map "intensity", to: :intensity
|
|
37
|
+
map "multiplicity", to: :multiplicity
|
|
38
|
+
map "assignment", to: :assignment
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Spectrum < Base
|
|
43
|
+
wire_type "spectrum"
|
|
44
|
+
|
|
45
|
+
attribute :technique, :string
|
|
46
|
+
attribute :params, Lutaml::Model::Type::Hash
|
|
47
|
+
attribute :peaks, SpectrumPeak, collection: true
|
|
48
|
+
json do
|
|
49
|
+
map "technique", to: :technique
|
|
50
|
+
map "params", to: :params
|
|
51
|
+
map "peaks", to: :peaks
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Crystal < Base
|
|
56
|
+
wire_type "crystal"
|
|
57
|
+
|
|
58
|
+
attribute :name, :string
|
|
59
|
+
attribute :a, :float
|
|
60
|
+
attribute :b, :float
|
|
61
|
+
attribute :c, :float
|
|
62
|
+
attribute :alpha, :float
|
|
63
|
+
attribute :beta, :float
|
|
64
|
+
attribute :gamma, :float
|
|
65
|
+
attribute :spacegroup, :string
|
|
66
|
+
attribute :atoms, Atom, collection: true
|
|
67
|
+
json do
|
|
68
|
+
map "name", to: :name
|
|
69
|
+
map "a", to: :a
|
|
70
|
+
map "b", to: :b
|
|
71
|
+
map "c", to: :c
|
|
72
|
+
map "alpha", to: :alpha
|
|
73
|
+
map "beta", to: :beta
|
|
74
|
+
map "gamma", to: :gamma
|
|
75
|
+
map "spacegroup", to: :spacegroup
|
|
76
|
+
map "atoms", to: :atoms
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class ZRow < Lutaml::Model::Serializable
|
|
81
|
+
attribute :atom, :string
|
|
82
|
+
attribute :ref1, :string
|
|
83
|
+
attribute :distance, :string
|
|
84
|
+
attribute :ref2, :string
|
|
85
|
+
attribute :angle, :string
|
|
86
|
+
attribute :ref3, :string
|
|
87
|
+
attribute :dihedral, :string
|
|
88
|
+
json do
|
|
89
|
+
map "atom", to: :atom
|
|
90
|
+
map "ref1", to: :ref1
|
|
91
|
+
map "distance", to: :distance
|
|
92
|
+
map "ref2", to: :ref2
|
|
93
|
+
map "angle", to: :angle
|
|
94
|
+
map "ref3", to: :ref3
|
|
95
|
+
map "dihedral", to: :dihedral
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class ZMatrix < Base
|
|
100
|
+
wire_type "zmatrix"
|
|
101
|
+
|
|
102
|
+
attribute :rows, ZRow, collection: true
|
|
103
|
+
json do
|
|
104
|
+
map "rows", to: :rows
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class CalculatedProperty < Lutaml::Model::Serializable
|
|
109
|
+
attribute :title, :string
|
|
110
|
+
attribute :value, :string
|
|
111
|
+
attribute :units, :string
|
|
112
|
+
attribute :dict_ref, :string
|
|
113
|
+
attribute :convention, :string
|
|
114
|
+
json do
|
|
115
|
+
map "title", to: :title
|
|
116
|
+
map "value", to: :value
|
|
117
|
+
map "units", to: :units
|
|
118
|
+
map "dictRef", to: :dict_ref
|
|
119
|
+
map "convention", to: :convention
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Calculation < Base
|
|
124
|
+
wire_type "calculation"
|
|
125
|
+
|
|
126
|
+
attribute :method, :string
|
|
127
|
+
attribute :basis, :string
|
|
128
|
+
attribute :properties, CalculatedProperty, collection: true
|
|
129
|
+
json do
|
|
130
|
+
map "method", to: :method
|
|
131
|
+
map "basis", to: :basis
|
|
132
|
+
map "properties", to: :properties
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Wire
|
|
5
|
+
# Identity wire classes: provenance and the resolved substance
|
|
6
|
+
# record produced by resolver sources (TODO.impl 38+).
|
|
7
|
+
class Provenance < Base
|
|
8
|
+
wire_type "provenance"
|
|
9
|
+
|
|
10
|
+
attribute :source, :string
|
|
11
|
+
attribute :retrieved_at, :string
|
|
12
|
+
attribute :source_version, :string
|
|
13
|
+
attribute :attribution, :string
|
|
14
|
+
json do
|
|
15
|
+
map "source", to: :source
|
|
16
|
+
map "retrievedAt", to: :retrieved_at
|
|
17
|
+
map "sourceVersion", to: :source_version
|
|
18
|
+
map "attribution", to: :attribution
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class ProvenancedIdentifier < Lutaml::Model::Serializable
|
|
23
|
+
attribute :identifier, Identifier
|
|
24
|
+
attribute :provenance, Provenance
|
|
25
|
+
json do
|
|
26
|
+
map "identifier", to: :identifier
|
|
27
|
+
map "provenance", to: :provenance
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ProvenancedProperty < Lutaml::Model::Serializable
|
|
32
|
+
attribute :name, :string
|
|
33
|
+
attribute :value, :string
|
|
34
|
+
attribute :units, :string
|
|
35
|
+
attribute :provenance, Provenance
|
|
36
|
+
json do
|
|
37
|
+
map "name", to: :name
|
|
38
|
+
map "value", to: :value
|
|
39
|
+
map "units", to: :units
|
|
40
|
+
map "provenance", to: :provenance
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class SubstanceRecord < Base
|
|
45
|
+
wire_type "substance-record"
|
|
46
|
+
|
|
47
|
+
attribute :preferred_name, :string
|
|
48
|
+
attribute :synonyms, :string, collection: true
|
|
49
|
+
attribute :formula, :string
|
|
50
|
+
attribute :molecular_weight, :float
|
|
51
|
+
attribute :identifiers, ProvenancedIdentifier, collection: true
|
|
52
|
+
attribute :properties, ProvenancedProperty, collection: true
|
|
53
|
+
json do
|
|
54
|
+
map "preferredName", to: :preferred_name
|
|
55
|
+
map "synonyms", to: :synonyms
|
|
56
|
+
map "formula", to: :formula
|
|
57
|
+
map "molecularWeight", to: :molecular_weight
|
|
58
|
+
map "identifiers", to: :identifiers
|
|
59
|
+
map "properties", to: :properties
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module AsciiChem
|
|
6
|
+
# Canonical wire form of the semantic model (the JSON interchange
|
|
7
|
+
# format defined by asciichem-model v1). Wire classes are
|
|
8
|
+
# lutaml-model Serializables: every (de)serialization goes through
|
|
9
|
+
# the framework's `json do map ... end` declarations — never
|
|
10
|
+
# hand-rolled key manipulation. AsciiChem::WireAdapter converts
|
|
11
|
+
# between the domain Model::* tree and this wire tree, exactly as
|
|
12
|
+
# ModelAdapter bridges AsciiChem::Model and Chemicalml::Model.
|
|
13
|
+
module Wire
|
|
14
|
+
# Discriminator ("atom", "molecule", ...) => wire class. Filled
|
|
15
|
+
# by Base.wire_type at class-definition time.
|
|
16
|
+
REGISTRY = {}
|
|
17
|
+
|
|
18
|
+
autoload :Base, "asciichem/wire/base"
|
|
19
|
+
autoload :Atom, "asciichem/wire/core"
|
|
20
|
+
autoload :Molecule, "asciichem/wire/core"
|
|
21
|
+
autoload :Group, "asciichem/wire/core"
|
|
22
|
+
autoload :Bond, "asciichem/wire/core"
|
|
23
|
+
autoload :Identifier, "asciichem/wire/core"
|
|
24
|
+
autoload :Name, "asciichem/wire/core"
|
|
25
|
+
autoload :Formula, "asciichem/wire/chemistry"
|
|
26
|
+
autoload :Reaction, "asciichem/wire/chemistry"
|
|
27
|
+
autoload :ReactionConditions, "asciichem/wire/chemistry"
|
|
28
|
+
autoload :ReactionCascade, "asciichem/wire/chemistry"
|
|
29
|
+
autoload :ElectronConfiguration, "asciichem/wire/chemistry"
|
|
30
|
+
autoload :OrbitalOccupancy, "asciichem/wire/chemistry"
|
|
31
|
+
autoload :TermSymbol, "asciichem/wire/chemistry"
|
|
32
|
+
autoload :EmbeddedMath, "asciichem/wire/chemistry"
|
|
33
|
+
autoload :Text, "asciichem/wire/chemistry"
|
|
34
|
+
autoload :Mechanism, "asciichem/wire/extended"
|
|
35
|
+
autoload :MechanismStep, "asciichem/wire/extended"
|
|
36
|
+
autoload :Spectrum, "asciichem/wire/extended"
|
|
37
|
+
autoload :SpectrumPeak, "asciichem/wire/extended"
|
|
38
|
+
autoload :Crystal, "asciichem/wire/extended"
|
|
39
|
+
autoload :ZMatrix, "asciichem/wire/extended"
|
|
40
|
+
autoload :ZRow, "asciichem/wire/extended"
|
|
41
|
+
autoload :Calculation, "asciichem/wire/extended"
|
|
42
|
+
autoload :CalculatedProperty, "asciichem/wire/extended"
|
|
43
|
+
autoload :Provenance, "asciichem/wire/identity"
|
|
44
|
+
autoload :SubstanceRecord, "asciichem/wire/identity"
|
|
45
|
+
autoload :ProvenancedIdentifier, "asciichem/wire/identity"
|
|
46
|
+
autoload :ProvenancedProperty, "asciichem/wire/identity"
|
|
47
|
+
|
|
48
|
+
# Eagerly trigger every autoload so wire classes register their
|
|
49
|
+
# discriminators before any lookup (same pattern as Linter).
|
|
50
|
+
constants.each { |name| const_get(name) unless name == :REGISTRY }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
# Converts between the domain semantic model (AsciiChem::Model::*)
|
|
5
|
+
# and the canonical wire form (AsciiChem::Wire::*). This is a
|
|
6
|
+
# model-to-model translation in the spirit of ModelAdapter — every
|
|
7
|
+
# (de)serialisation to/from JSON itself is performed by lutaml-model
|
|
8
|
+
# on the Wire classes; this adapter only routes types and maps
|
|
9
|
+
# fields.
|
|
10
|
+
#
|
|
11
|
+
# Emission (to_wire/to_model_json) covers every wire type.
|
|
12
|
+
# Ingestion (from_wire/from_model_json) currently covers the
|
|
13
|
+
# lossless core set: formula, atom, molecule, group, bond,
|
|
14
|
+
# identifier, name, reaction (+conditions), reaction-cascade,
|
|
15
|
+
# electron-configuration, embedded-math, text. The beyond-formulas
|
|
16
|
+
# nodes (mechanism, spectrum, crystal, zmatrix, calculation) are
|
|
17
|
+
# emission-only until their round-trip acceptance lands in the
|
|
18
|
+
# corpus.
|
|
19
|
+
module WireAdapter
|
|
20
|
+
CORE_INGEST_TYPES = %w[
|
|
21
|
+
formula atom molecule group bond identifier name reaction
|
|
22
|
+
reaction-cascade electron-configuration embedded-math text
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def to_model_json(formula)
|
|
27
|
+
to_wire(formula).to_json
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def from_model_json(json)
|
|
31
|
+
from_wire(wire_tree_from_hash(JSON.parse(json)))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# domain -> wire ------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def to_wire(node)
|
|
37
|
+
case node
|
|
38
|
+
when AsciiChem::Model::Formula then formula_to_wire(node)
|
|
39
|
+
when AsciiChem::Model::Molecule then molecule_to_wire(node)
|
|
40
|
+
when AsciiChem::Model::Group then group_to_wire(node)
|
|
41
|
+
when AsciiChem::Model::Atom then atom_to_wire(node)
|
|
42
|
+
when AsciiChem::Model::Bond then bond_to_wire(node)
|
|
43
|
+
when AsciiChem::Model::Identifier then identifier_to_wire(node)
|
|
44
|
+
when AsciiChem::Model::Name then name_to_wire(node)
|
|
45
|
+
when AsciiChem::Model::Reaction then reaction_to_wire(node)
|
|
46
|
+
when AsciiChem::Model::ReactionCascade then cascade_to_wire(node)
|
|
47
|
+
when AsciiChem::Model::ElectronConfiguration then econfig_to_wire(node)
|
|
48
|
+
when AsciiChem::Model::EmbeddedMath then math_to_wire(node)
|
|
49
|
+
when AsciiChem::Model::Text then text_to_wire(node)
|
|
50
|
+
when AsciiChem::Model::Mechanism then mechanism_to_wire(node)
|
|
51
|
+
when AsciiChem::Model::Spectrum then spectrum_to_wire(node)
|
|
52
|
+
when AsciiChem::Model::Crystal then crystal_to_wire(node)
|
|
53
|
+
when AsciiChem::Model::ZMatrix then zmatrix_to_wire(node)
|
|
54
|
+
when AsciiChem::Model::Calculation then calculation_to_wire(node)
|
|
55
|
+
else raise ArgumentError, "no wire form for #{node.class}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# wire -> domain ------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def from_wire(wire)
|
|
62
|
+
type = wire.type
|
|
63
|
+
unless CORE_INGEST_TYPES.include?(type)
|
|
64
|
+
raise ArgumentError, "wire ingestion not implemented for #{type.inspect}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
send(:"#{type.tr('-', '_')}_from_wire", wire)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Discriminator-routed tree construction from a parsed JSON
|
|
71
|
+
# hash. Per-node deserialisation stays in lutaml-model; this
|
|
72
|
+
# only routes `type` and re-injects routed children into
|
|
73
|
+
# polymorphic collections (which lutaml deserialises flat).
|
|
74
|
+
def wire_tree_from_hash(hash)
|
|
75
|
+
type = hash.fetch("type") { raise KeyError, "wire node missing type discriminator" }
|
|
76
|
+
klass = AsciiChem::Wire::REGISTRY.fetch(type) do
|
|
77
|
+
raise KeyError, "unknown wire node type #{type.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
wire = klass.from_json(JSON.generate(hash))
|
|
81
|
+
case type
|
|
82
|
+
when "formula", "molecule", "group"
|
|
83
|
+
wire.nodes = hash.fetch("nodes", []).map { |child| wire_tree_from_hash(child) }
|
|
84
|
+
when "reaction"
|
|
85
|
+
wire.reactants = hash.fetch("reactants", []).map { |child| wire_tree_from_hash(child) }
|
|
86
|
+
wire.products = hash.fetch("products", []).map { |child| wire_tree_from_hash(child) }
|
|
87
|
+
when "reaction-cascade"
|
|
88
|
+
wire.steps = hash.fetch("steps", []).map { |child| wire_tree_from_hash(child) }
|
|
89
|
+
end
|
|
90
|
+
wire
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def formula_to_wire(node)
|
|
96
|
+
AsciiChem::Wire::Formula.new(type: "formula", nodes: node.nodes.map { |n| to_wire(n) })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def atom_to_wire(atom)
|
|
100
|
+
AsciiChem::Wire::Atom.new(
|
|
101
|
+
type: "atom",
|
|
102
|
+
element: atom.element,
|
|
103
|
+
isotope: digits_or_nil(atom.isotope), charge: str_or_nil(atom.charge),
|
|
104
|
+
subscript: digits_or_nil(atom.subscript),
|
|
105
|
+
oxidation_state: str_or_nil(atom.oxidation_state),
|
|
106
|
+
lone_pairs: int_or_nil(atom.lone_pairs),
|
|
107
|
+
radical_electrons: int_or_nil(atom.radical_electrons),
|
|
108
|
+
ring_closures: str_or_nil(atom.ring_closures)
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Fuzz corpus inputs like `H_{2a}O` parse with the raw braced
|
|
113
|
+
# text in the subscript field (the Text formatter round-trips it
|
|
114
|
+
# verbatim by design). The canonical wire form carries only
|
|
115
|
+
# schema-clean values; junk never enters the wire.
|
|
116
|
+
def digits_or_nil(value)
|
|
117
|
+
str_or_nil(value)&.then { |s| s.match?(/\A\d+\z/) ? s : nil }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def str_or_nil(value)
|
|
121
|
+
value.is_a?(String) ? value : nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def int_or_nil(value)
|
|
125
|
+
value.is_a?(Integer) ? value : nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def molecule_to_wire(node)
|
|
129
|
+
AsciiChem::Wire::Molecule.new(
|
|
130
|
+
type: "molecule",
|
|
131
|
+
nodes: node.nodes.map { |n| to_wire(n) },
|
|
132
|
+
coefficient: node.coefficient,
|
|
133
|
+
identifiers: node.identifiers.map { |i| identifier_to_wire(i) }
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def group_to_wire(node)
|
|
138
|
+
AsciiChem::Wire::Group.new(
|
|
139
|
+
type: "group",
|
|
140
|
+
nodes: node.nodes.map { |n| to_wire(n) },
|
|
141
|
+
multiplicity: node.multiplicity,
|
|
142
|
+
bracket: node.bracket.to_s
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def bond_to_wire(node)
|
|
147
|
+
AsciiChem::Wire::Bond.new(type: "bond", kind: node.kind.to_s)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def identifier_to_wire(node)
|
|
151
|
+
AsciiChem::Wire::Identifier.new(
|
|
152
|
+
type: "identifier",
|
|
153
|
+
value: node.value, convention: node.convention.to_s, dict_ref: node.dict_ref
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def name_to_wire(node)
|
|
158
|
+
AsciiChem::Wire::Name.new(
|
|
159
|
+
type: "name",
|
|
160
|
+
content: node.content, convention: node.convention, dict_ref: node.dict_ref
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def reaction_to_wire(node)
|
|
165
|
+
AsciiChem::Wire::Reaction.new(
|
|
166
|
+
type: "reaction",
|
|
167
|
+
reactants: node.reactants.map { |m| molecule_to_wire(m) },
|
|
168
|
+
products: node.products.map { |m| molecule_to_wire(m) },
|
|
169
|
+
arrow: node.arrow.to_s,
|
|
170
|
+
conditions: conditions_to_wire(node.conditions)
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def conditions_to_wire(conditions)
|
|
175
|
+
return nil unless conditions
|
|
176
|
+
|
|
177
|
+
AsciiChem::Wire::ReactionConditions.new(above: conditions.above, below: conditions.below)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def cascade_to_wire(node)
|
|
181
|
+
AsciiChem::Wire::ReactionCascade.new(
|
|
182
|
+
type: "reaction-cascade",
|
|
183
|
+
steps: node.steps.map { |r| reaction_to_wire(r) }
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def econfig_to_wire(node)
|
|
188
|
+
AsciiChem::Wire::ElectronConfiguration.new(
|
|
189
|
+
type: "electron-configuration",
|
|
190
|
+
orbitals: node.orbitals.map do |(label, count)|
|
|
191
|
+
AsciiChem::Wire::OrbitalOccupancy.new(orbital: label.to_s, occupancy: count.to_s)
|
|
192
|
+
end,
|
|
193
|
+
term_symbol: term_symbol_to_wire(node.term_symbol)
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def term_symbol_to_wire(term)
|
|
198
|
+
return nil unless term
|
|
199
|
+
|
|
200
|
+
AsciiChem::Wire::TermSymbol.new(
|
|
201
|
+
multiplicity: term.multiplicity.to_s, letter: term.letter.to_s, j_value: term.j_value.to_s
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def math_to_wire(node)
|
|
206
|
+
AsciiChem::Wire::EmbeddedMath.new(type: "embedded-math", source: node.source)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def text_to_wire(node)
|
|
210
|
+
AsciiChem::Wire::Text.new(type: "text", content: node.content)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def mechanism_to_wire(node)
|
|
214
|
+
AsciiChem::Wire::Mechanism.new(
|
|
215
|
+
type: "mechanism",
|
|
216
|
+
steps: node.steps.map do |step|
|
|
217
|
+
AsciiChem::Wire::MechanismStep.new(label: step.label, reaction: reaction_to_wire(step.reaction))
|
|
218
|
+
end,
|
|
219
|
+
spectators: node.spectators.map { |m| molecule_to_wire(m) }
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def spectrum_to_wire(node)
|
|
224
|
+
AsciiChem::Wire::Spectrum.new(
|
|
225
|
+
type: "spectrum",
|
|
226
|
+
technique: node.type,
|
|
227
|
+
params: node.params,
|
|
228
|
+
peaks: node.peaks.map do |peak|
|
|
229
|
+
AsciiChem::Wire::SpectrumPeak.new(
|
|
230
|
+
position: peak.position, intensity: peak.intensity,
|
|
231
|
+
multiplicity: peak.multiplicity, assignment: peak.assignment
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def crystal_to_wire(node)
|
|
238
|
+
AsciiChem::Wire::Crystal.new(
|
|
239
|
+
type: "crystal",
|
|
240
|
+
name: node.name, a: node.a, b: node.b, c: node.c,
|
|
241
|
+
alpha: node.alpha, beta: node.beta, gamma: node.gamma,
|
|
242
|
+
spacegroup: node.spacegroup,
|
|
243
|
+
atoms: node.atoms.map { |atom| atom_to_wire(atom) }
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def zmatrix_to_wire(node)
|
|
248
|
+
AsciiChem::Wire::ZMatrix.new(
|
|
249
|
+
type: "zmatrix",
|
|
250
|
+
rows: node.rows.map do |row|
|
|
251
|
+
AsciiChem::Wire::ZRow.new(
|
|
252
|
+
atom: row.atom, ref1: row.ref1, distance: row.distance,
|
|
253
|
+
ref2: row.ref2, angle: row.angle, ref3: row.ref3, dihedral: row.dihedral
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def calculation_to_wire(node)
|
|
260
|
+
AsciiChem::Wire::Calculation.new(
|
|
261
|
+
type: "calculation",
|
|
262
|
+
method: node.method, basis: node.basis,
|
|
263
|
+
properties: node.properties.map do |prop|
|
|
264
|
+
AsciiChem::Wire::CalculatedProperty.new(
|
|
265
|
+
title: prop.title, value: prop.value, units: prop.units,
|
|
266
|
+
dict_ref: prop.dict_ref, convention: prop.convention
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# -- ingestion (core set) ----------------------------------------
|
|
273
|
+
|
|
274
|
+
def formula_from_wire(wire)
|
|
275
|
+
AsciiChem::Model::Formula.new(nodes: wire.nodes.map { |n| from_wire(n) })
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def atom_from_wire(wire)
|
|
279
|
+
AsciiChem::Model::Atom.new(
|
|
280
|
+
element: wire.element, isotope: wire.isotope, subscript: wire.subscript,
|
|
281
|
+
charge: wire.charge, oxidation_state: wire.oxidation_state,
|
|
282
|
+
lone_pairs: wire.lone_pairs, radical_electrons: wire.radical_electrons,
|
|
283
|
+
ring_closures: wire.ring_closures
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def molecule_from_wire(wire)
|
|
288
|
+
AsciiChem::Model::Molecule.new(
|
|
289
|
+
nodes: Array(wire.nodes).map { |n| from_wire(n) },
|
|
290
|
+
coefficient: wire.coefficient,
|
|
291
|
+
identifiers: Array(wire.identifiers).map { |i| identifier_from_wire(i) }
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def group_from_wire(wire)
|
|
296
|
+
AsciiChem::Model::Group.new(
|
|
297
|
+
nodes: wire.nodes.map { |n| from_wire(n) },
|
|
298
|
+
multiplicity: wire.multiplicity,
|
|
299
|
+
bracket: wire.bracket&.to_sym
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def bond_from_wire(wire)
|
|
304
|
+
AsciiChem::Model::Bond.new(kind: wire.kind.to_sym)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def identifier_from_wire(wire)
|
|
308
|
+
AsciiChem::Model::Identifier.new(
|
|
309
|
+
value: wire.value, convention: wire.convention, dict_ref: wire.dict_ref
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def name_from_wire(wire)
|
|
314
|
+
AsciiChem::Model::Name.new(
|
|
315
|
+
content: wire.content, convention: wire.convention, dict_ref: wire.dict_ref
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def reaction_from_wire(wire)
|
|
320
|
+
conditions = wire.conditions &&
|
|
321
|
+
AsciiChem::Model::Reaction::Conditions.new(above: wire.conditions.above,
|
|
322
|
+
below: wire.conditions.below)
|
|
323
|
+
AsciiChem::Model::Reaction.new(
|
|
324
|
+
reactants: Array(wire.reactants).map { |m| molecule_from_wire(m) },
|
|
325
|
+
products: Array(wire.products).map { |m| molecule_from_wire(m) },
|
|
326
|
+
arrow: wire.arrow.to_sym,
|
|
327
|
+
conditions: conditions
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def reaction_cascade_from_wire(wire)
|
|
332
|
+
AsciiChem::Model::ReactionCascade.new(
|
|
333
|
+
steps: Array(wire.steps).map { |r| reaction_from_wire(r) }
|
|
334
|
+
)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def electron_configuration_from_wire(wire)
|
|
338
|
+
AsciiChem::Model::ElectronConfiguration.new(
|
|
339
|
+
orbitals: Array(wire.orbitals).map { |o| [o.orbital, o.occupancy] },
|
|
340
|
+
term_symbol: wire.term_symbol &&
|
|
341
|
+
AsciiChem::Model::ElectronConfiguration::TermSymbol.new(
|
|
342
|
+
multiplicity: wire.term_symbol.multiplicity,
|
|
343
|
+
letter: wire.term_symbol.letter,
|
|
344
|
+
j_value: wire.term_symbol.j_value
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def embedded_math_from_wire(wire)
|
|
350
|
+
AsciiChem::Model::EmbeddedMath.new(
|
|
351
|
+
formula: Plurimath::Asciimath.new(wire.source).to_formula,
|
|
352
|
+
source: wire.source
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def text_from_wire(wire)
|
|
357
|
+
AsciiChem::Model::Text.new(content: wire.content)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
data/lib/asciichem.rb
CHANGED
|
@@ -27,9 +27,18 @@ module AsciiChem
|
|
|
27
27
|
autoload :RingBonds, "asciichem/ring_bonds"
|
|
28
28
|
autoload :Transform, "asciichem/transform"
|
|
29
29
|
autoload :VERSION, "asciichem/version"
|
|
30
|
+
autoload :Wire, "asciichem/wire"
|
|
31
|
+
autoload :WireAdapter, "asciichem/wire_adapter"
|
|
30
32
|
autoload :XmlBuilder, "asciichem/xml_builder"
|
|
31
33
|
|
|
32
34
|
def self.parse(text)
|
|
33
35
|
Parser.new(text).parse
|
|
34
36
|
end
|
|
37
|
+
|
|
38
|
+
# Rebuilds the semantic model from the canonical JSON wire form
|
|
39
|
+
# (asciichem-model v1) — the interchange format every
|
|
40
|
+
# implementation emits.
|
|
41
|
+
def self.from_model_json(json)
|
|
42
|
+
WireAdapter.from_model_json(json)
|
|
43
|
+
end
|
|
35
44
|
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
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -37,6 +37,26 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: lutaml-model
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.8'
|
|
47
|
+
- - "<"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '2'
|
|
50
|
+
type: :runtime
|
|
51
|
+
prerelease: false
|
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0.8'
|
|
57
|
+
- - "<"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2'
|
|
40
60
|
- !ruby/object:Gem::Dependency
|
|
41
61
|
name: nokogiri
|
|
42
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +113,20 @@ dependencies:
|
|
|
93
113
|
- - "~>"
|
|
94
114
|
- !ruby/object:Gem::Version
|
|
95
115
|
version: '1.3'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: asciichem-model
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: 0.3.4
|
|
123
|
+
type: :development
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: 0.3.4
|
|
96
130
|
description: AsciiChem is an ASCII syntax for representing chemical formulae, reactions,
|
|
97
131
|
electron configurations, bonds, crystals, spectra, calculations, Z-matrices, and
|
|
98
132
|
reaction mechanisms. It parses to a semantic model and renders to MathML, HTML,
|
|
@@ -194,6 +228,13 @@ files:
|
|
|
194
228
|
- lib/asciichem/ring_bonds.rb
|
|
195
229
|
- lib/asciichem/transform.rb
|
|
196
230
|
- lib/asciichem/version.rb
|
|
231
|
+
- lib/asciichem/wire.rb
|
|
232
|
+
- lib/asciichem/wire/base.rb
|
|
233
|
+
- lib/asciichem/wire/chemistry.rb
|
|
234
|
+
- lib/asciichem/wire/core.rb
|
|
235
|
+
- lib/asciichem/wire/extended.rb
|
|
236
|
+
- lib/asciichem/wire/identity.rb
|
|
237
|
+
- lib/asciichem/wire_adapter.rb
|
|
197
238
|
- lib/asciichem/xml_builder.rb
|
|
198
239
|
homepage: https://www.asciichem.org
|
|
199
240
|
licenses:
|
|
@@ -218,7 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
218
259
|
- !ruby/object:Gem::Version
|
|
219
260
|
version: '0'
|
|
220
261
|
requirements: []
|
|
221
|
-
rubygems_version:
|
|
262
|
+
rubygems_version: 3.6.9
|
|
222
263
|
specification_version: 4
|
|
223
264
|
summary: ASCII syntax for chemistry formulae, reactions, structures, and spectroscopy.
|
|
224
265
|
test_files: []
|