smiles-js 2.0.2 → 2.1.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.
- package/API.md +206 -9
- package/README.md +39 -0
- package/docs/MIRROR_PLAN.md +204 -0
- package/package.json +1 -1
- package/scripts/coverage-summary.js +1 -1
- package/src/codegen/branch-crossing-ring.js +27 -6
- package/src/codegen/interleaved-fused-ring.js +24 -0
- package/src/decompiler.js +53 -123
- package/src/decompiler.test.js +56 -8
- package/src/manipulation.js +443 -17
- package/src/manipulation.test.js +359 -1
- package/src/method-attachers.js +29 -0
- package/src/node-creators.js +7 -0
- package/src/parser/ast-builder.js +23 -8
- package/src/parser/ring-group-builder.js +14 -2
- package/src/parser/ring-utils.js +28 -0
- package/test-integration/__snapshots__/adjuvant-analgesics.test.js.snap +1 -1
- package/test-integration/__snapshots__/cholesterol-drugs.test.js.snap +189 -23
- package/test-integration/__snapshots__/endocannabinoids.test.js.snap +2 -2
- package/test-integration/__snapshots__/hypertension-medication.test.js.snap +1 -1
- package/test-integration/__snapshots__/nsaids-otc.test.js.snap +1 -1
- package/test-integration/__snapshots__/nsaids-prescription.test.js.snap +2 -2
- package/test-integration/__snapshots__/opioids.test.js.snap +4 -4
- package/test-integration/__snapshots__/steroids.test.js.snap +2 -2
- package/test-integration/cholesterol-drugs.test.js +41 -0
- package/test-integration/cholesterol.test.js +112 -0
- package/test-integration/mirror.test.js +151 -0
- package/test-integration/polymer.test.js +148 -0
- package/todo +0 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { Ring, Linear, Molecule } from '../src/constructors.js';
|
|
3
|
+
|
|
4
|
+
describe('Mirror Integration - Linear', () => {
|
|
5
|
+
test('diethyl ether: CCO mirrored → CCOCC', () => {
|
|
6
|
+
const half = Linear(['C', 'C', 'O']);
|
|
7
|
+
const ether = half.mirror();
|
|
8
|
+
expect(ether.smiles).toBe('CCOCC');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('diethylamine: CCN mirrored → CCNCC', () => {
|
|
12
|
+
const half = Linear(['C', 'C', 'N']);
|
|
13
|
+
const result = half.mirror();
|
|
14
|
+
expect(result.smiles).toBe('CCNCC');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('glycol-like: OCCO mirrored at pivot=3 → OCCOCCCO', () => {
|
|
18
|
+
const half = Linear(['O', 'C', 'C', 'O']);
|
|
19
|
+
const result = half.mirror(3);
|
|
20
|
+
// Left: [O,C,C], Right: reverse of [O,C] → [C,O]
|
|
21
|
+
expect(result.smiles).toBe('OCCCO');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('symmetric alkene: C=C=O mirrored → C=COC=C', () => {
|
|
25
|
+
const half = Linear(['C', 'C', 'O'], ['=']);
|
|
26
|
+
const result = half.mirror();
|
|
27
|
+
expect(result.smiles).toBe('C=COC=C');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('symmetric chain with phenyl pendant', () => {
|
|
31
|
+
const half = Linear(['C', 'C', 'O']).attach(
|
|
32
|
+
1,
|
|
33
|
+
Ring({ atoms: 'c', size: 6 }),
|
|
34
|
+
);
|
|
35
|
+
const result = half.mirror();
|
|
36
|
+
expect(result.smiles).toBe('C(c1ccccc1)COCC(c2ccccc2)');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('long palindromic chain', () => {
|
|
40
|
+
const half = Linear(['C', 'C', 'C', 'C', 'N']);
|
|
41
|
+
const result = half.mirror();
|
|
42
|
+
expect(result.smiles).toBe('CCCCNCCCC');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('Mirror Integration - Molecule (ABA copolymers)', () => {
|
|
47
|
+
test('ABA: polyethylene-benzene-polyethylene', () => {
|
|
48
|
+
const A = Linear(['C', 'C']);
|
|
49
|
+
const B = Ring({ atoms: 'c', size: 6 });
|
|
50
|
+
const AB = Molecule([A, B]);
|
|
51
|
+
const ABA = AB.mirror();
|
|
52
|
+
expect(ABA.smiles).toBe('CCc1ccccc1CC');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('ABCBA: ethyl-benzene-oxygen-benzene-ethyl', () => {
|
|
56
|
+
const block = Molecule([
|
|
57
|
+
Linear(['C', 'C']),
|
|
58
|
+
Ring({ atoms: 'c', size: 6 }),
|
|
59
|
+
Linear(['O']),
|
|
60
|
+
]);
|
|
61
|
+
const result = block.mirror();
|
|
62
|
+
expect(result.smiles).toBe('CCc1ccccc1Oc2ccccc2CC');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('ABA with polystyrene arms', () => {
|
|
66
|
+
const styrene = Linear(['C', 'C']).attach(
|
|
67
|
+
2,
|
|
68
|
+
Ring({ atoms: 'c', size: 6 }),
|
|
69
|
+
);
|
|
70
|
+
const core = Linear(['O', 'C', 'O']);
|
|
71
|
+
const halfBlock = Molecule([styrene, core]);
|
|
72
|
+
const symmetric = halfBlock.mirror();
|
|
73
|
+
// styrene-OCO-styrene (with shifted ring numbers)
|
|
74
|
+
expect(symmetric.type).toBe('molecule');
|
|
75
|
+
expect(symmetric.components).toHaveLength(3);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('repeat + mirror: homopolymer arm with symmetric core', () => {
|
|
79
|
+
const arm = Linear(['C', 'C']).repeat(2, 1, 2);
|
|
80
|
+
const core = Ring({ atoms: 'c', size: 6 });
|
|
81
|
+
const halfBlock = Molecule([arm, core]);
|
|
82
|
+
const symmetric = halfBlock.mirror();
|
|
83
|
+
expect(symmetric.smiles).toBe('CCCCc1ccccc1CCCC');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('Mirror Integration - Ring', () => {
|
|
88
|
+
test('meta-xylene: methyl at 2, mirror pivot=3', () => {
|
|
89
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
90
|
+
const mono = benzene.attach(2, Linear(['C']));
|
|
91
|
+
const result = mono.mirror(3);
|
|
92
|
+
expect(result.smiles).toBe('c1c(C)cc(C)cc1');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('1,3-disubstituted pyridine: N at 2, mirror pivot=1', () => {
|
|
96
|
+
const ring = Ring({
|
|
97
|
+
atoms: 'c',
|
|
98
|
+
size: 6,
|
|
99
|
+
substitutions: { 2: 'n' },
|
|
100
|
+
});
|
|
101
|
+
const result = ring.mirror(1);
|
|
102
|
+
expect(result.substitutions[2]).toBe('n');
|
|
103
|
+
expect(result.substitutions[6]).toBe('n');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('symmetric ring with two pendant groups', () => {
|
|
107
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
108
|
+
const mono = benzene.attach(2, Linear(['O']));
|
|
109
|
+
const result = mono.mirror(3);
|
|
110
|
+
// Attachment at 2 and mirror at 4
|
|
111
|
+
expect(result.attachments[2]).toHaveLength(1);
|
|
112
|
+
expect(result.attachments[4]).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Mirror edge cases', () => {
|
|
117
|
+
test('mirror preserves immutability', () => {
|
|
118
|
+
const chain = Linear(['C', 'C', 'O']);
|
|
119
|
+
const original = chain.smiles;
|
|
120
|
+
chain.mirror();
|
|
121
|
+
expect(chain.smiles).toBe(original);
|
|
122
|
+
|
|
123
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
124
|
+
const mono = benzene.attach(2, Linear(['C']));
|
|
125
|
+
const originalSmiles = mono.smiles;
|
|
126
|
+
mono.mirror(1);
|
|
127
|
+
expect(mono.smiles).toBe(originalSmiles);
|
|
128
|
+
|
|
129
|
+
const mol = Molecule([Linear(['C']), Linear(['N'])]);
|
|
130
|
+
const originalMol = mol.smiles;
|
|
131
|
+
mol.mirror();
|
|
132
|
+
expect(mol.smiles).toBe(originalMol);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('mirror of single atom is identity', () => {
|
|
136
|
+
const single = Linear(['N']);
|
|
137
|
+
expect(single.mirror().smiles).toBe('N');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('mirror of two-atom chain', () => {
|
|
141
|
+
const two = Linear(['C', 'N']);
|
|
142
|
+
expect(two.mirror().smiles).toBe('CNC');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('mirror of single-component molecule', () => {
|
|
146
|
+
const mol = Molecule([Linear(['C', 'C'])]);
|
|
147
|
+
const result = mol.mirror();
|
|
148
|
+
expect(result.smiles).toBe('CC');
|
|
149
|
+
expect(result.components).toHaveLength(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { Ring, Linear, Molecule } from '../src/constructors.js';
|
|
3
|
+
|
|
4
|
+
describe('Polymer repeat() Integration', () => {
|
|
5
|
+
test('polyethylene trimer: -[CH2CH2]3-', () => {
|
|
6
|
+
const ethylene = Linear(['C', 'C']);
|
|
7
|
+
const pe = ethylene.repeat(3, 1, 2);
|
|
8
|
+
expect(pe.smiles).toBe('CCCCCC');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('polystyrene dimer: styrene with phenyl pendant', () => {
|
|
12
|
+
const styrene = Linear(['C', 'C']).attach(
|
|
13
|
+
2,
|
|
14
|
+
Ring({ atoms: 'c', size: 6 }),
|
|
15
|
+
);
|
|
16
|
+
const ps = styrene.repeat(2, 1, 2);
|
|
17
|
+
expect(ps.smiles).toBe('CC(c1ccccc1)CC(c2ccccc2)');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('polystyrene trimer', () => {
|
|
21
|
+
const styrene = Linear(['C', 'C']).attach(
|
|
22
|
+
2,
|
|
23
|
+
Ring({ atoms: 'c', size: 6 }),
|
|
24
|
+
);
|
|
25
|
+
const ps = styrene.repeat(3, 1, 2);
|
|
26
|
+
expect(ps.smiles).toBe('CC(c1ccccc1)CC(c2ccccc2)CC(c3ccccc3)');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('poly(vinyl alcohol) dimer: -[CH2CH(OH)]2-', () => {
|
|
30
|
+
const vinylAlcohol = Linear(['C', 'C']).attach(
|
|
31
|
+
2,
|
|
32
|
+
Linear(['O']),
|
|
33
|
+
);
|
|
34
|
+
const pva = vinylAlcohol.repeat(2, 1, 2);
|
|
35
|
+
expect(pva.smiles).toBe('CC(O)CC(O)');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('biphenyl: two linked benzene rings', () => {
|
|
39
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
40
|
+
const biphenyl = benzene.repeat(2, 1, 6);
|
|
41
|
+
expect(biphenyl.smiles).toBe('c1ccccc1c2ccccc2');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('terphenyl: three linked benzene rings', () => {
|
|
45
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
46
|
+
const terphenyl = benzene.repeat(3, 1, 6);
|
|
47
|
+
expect(terphenyl.smiles).toBe('c1ccccc1c2ccccc2c3ccccc3');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('bipyridine: two linked pyridine rings', () => {
|
|
51
|
+
const pyridine = Ring({
|
|
52
|
+
atoms: 'c',
|
|
53
|
+
size: 6,
|
|
54
|
+
substitutions: { 3: 'n' },
|
|
55
|
+
});
|
|
56
|
+
const bipyridine = pyridine.repeat(2, 1, 6);
|
|
57
|
+
expect(bipyridine.smiles).toBe('c1cnccc1c2cnccc2');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('polyoxymethylene trimer: -[CH2O]3-', () => {
|
|
61
|
+
const oxymethylene = Linear(['C', 'O']);
|
|
62
|
+
const pom = oxymethylene.repeat(3, 1, 2);
|
|
63
|
+
expect(pom.smiles).toBe('COCOCO');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('nylon 6,6 repeating unit dimer', () => {
|
|
67
|
+
const unit = Molecule([
|
|
68
|
+
Linear(['N', 'C'], [null, '=']),
|
|
69
|
+
Linear(['C', 'C', 'C', 'C']),
|
|
70
|
+
Linear(['C', 'N'], ['=', null]),
|
|
71
|
+
Linear(['C', 'C', 'C', 'C', 'C', 'C']),
|
|
72
|
+
]);
|
|
73
|
+
const dimer = unit.repeat(2, 1, 1);
|
|
74
|
+
expect(dimer.type).toBe('molecule');
|
|
75
|
+
expect(dimer.components).toHaveLength(2);
|
|
76
|
+
expect(dimer.smiles).toBe('N=CCCCC=CNCCCCCCN=CCCCC=CNCCCCCC');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Polymer fusedRepeat() Integration', () => {
|
|
81
|
+
test('naphthalene: 2 fused aromatic rings', () => {
|
|
82
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
83
|
+
const naphthalene = benzene.fusedRepeat(2, 4);
|
|
84
|
+
expect(naphthalene.type).toBe('fused_ring');
|
|
85
|
+
expect(naphthalene.rings).toHaveLength(2);
|
|
86
|
+
expect(naphthalene.rings[0].ringNumber).toBe(1);
|
|
87
|
+
expect(naphthalene.rings[1].ringNumber).toBe(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('anthracene: 3 fused aromatic rings', () => {
|
|
91
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
92
|
+
const anthracene = benzene.fusedRepeat(3, 4);
|
|
93
|
+
expect(anthracene.type).toBe('fused_ring');
|
|
94
|
+
expect(anthracene.rings).toHaveLength(3);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('tetracene: 4 fused aromatic rings', () => {
|
|
98
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
99
|
+
const tetracene = benzene.fusedRepeat(4, 4);
|
|
100
|
+
expect(tetracene.type).toBe('fused_ring');
|
|
101
|
+
expect(tetracene.rings).toHaveLength(4);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('fused cyclohexane dimer (decalin-like)', () => {
|
|
105
|
+
const cyclohexane = Ring({ atoms: 'C', size: 6 });
|
|
106
|
+
const decalin = cyclohexane.fusedRepeat(2, 4);
|
|
107
|
+
expect(decalin.type).toBe('fused_ring');
|
|
108
|
+
expect(decalin.rings).toHaveLength(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('fused 5-membered rings (pentalene-like)', () => {
|
|
112
|
+
const cp = Ring({ atoms: 'C', size: 5 });
|
|
113
|
+
const pentalene = cp.fusedRepeat(2, 3);
|
|
114
|
+
expect(pentalene.type).toBe('fused_ring');
|
|
115
|
+
expect(pentalene.rings).toHaveLength(2);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Polymer edge cases', () => {
|
|
120
|
+
test('repeat n=1 returns clone, not original', () => {
|
|
121
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
122
|
+
const clone = benzene.repeat(1, 1, 6);
|
|
123
|
+
expect(clone.smiles).toBe('c1ccccc1');
|
|
124
|
+
expect(clone).not.toBe(benzene);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('fusedRepeat n=1 returns ring clone', () => {
|
|
128
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
129
|
+
const clone = benzene.fusedRepeat(1, 4);
|
|
130
|
+
expect(clone.smiles).toBe('c1ccccc1');
|
|
131
|
+
expect(clone.type).toBe('ring');
|
|
132
|
+
expect(clone).not.toBe(benzene);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('large repeat count (n=10)', () => {
|
|
136
|
+
const ethylene = Linear(['C', 'C']);
|
|
137
|
+
const pe10 = ethylene.repeat(10, 1, 2);
|
|
138
|
+
expect(pe10.smiles).toBe('C'.repeat(20));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('repeat preserves immutability of original', () => {
|
|
142
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
143
|
+
const original = benzene.smiles;
|
|
144
|
+
benzene.repeat(5, 1, 6);
|
|
145
|
+
benzene.fusedRepeat(3, 4);
|
|
146
|
+
expect(benzene.smiles).toBe(original);
|
|
147
|
+
});
|
|
148
|
+
});
|
package/todo
CHANGED