smiles-js 2.0.3 → 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 +162 -0
- 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 +27 -2
- package/src/manipulation.js +409 -4
- 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 +176 -0
- 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
package/API.md
CHANGED
|
@@ -190,6 +190,70 @@ Fuse this ring with another ring. `offset` is how many positions into this ring
|
|
|
190
190
|
|
|
191
191
|
Return a deep copy of the ring.
|
|
192
192
|
|
|
193
|
+
#### `ring.repeat(n, leftId, rightId)`
|
|
194
|
+
|
|
195
|
+
Repeat the ring `n` times to build polymer chains. Each copy gets unique ring numbers automatically.
|
|
196
|
+
|
|
197
|
+
| Parameter | Type | Description |
|
|
198
|
+
|-----------|------|-------------|
|
|
199
|
+
| `n` | `number` | Number of repeating units (>= 1) |
|
|
200
|
+
| `leftId` | `number` | 1-indexed left (incoming) attachment point |
|
|
201
|
+
| `rightId` | `number` | 1-indexed right (outgoing) attachment point |
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
// Biphenyl (two linked benzene rings)
|
|
205
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
206
|
+
const biphenyl = benzene.repeat(2, 1, 6);
|
|
207
|
+
console.log(biphenyl.smiles); // c1ccccc1c2ccccc2
|
|
208
|
+
|
|
209
|
+
// Terphenyl (three linked benzene rings)
|
|
210
|
+
const terphenyl = benzene.repeat(3, 1, 6);
|
|
211
|
+
console.log(terphenyl.smiles); // c1ccccc1c2ccccc2c3ccccc3
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### `ring.fusedRepeat(n, offset)`
|
|
215
|
+
|
|
216
|
+
Repeat a ring `n` times by fusing, creating acene-like edge-sharing systems (naphthalene, anthracene, tetracene).
|
|
217
|
+
|
|
218
|
+
| Parameter | Type | Description |
|
|
219
|
+
|-----------|------|-------------|
|
|
220
|
+
| `n` | `number` | Total number of rings (>= 1) |
|
|
221
|
+
| `offset` | `number` | Fusion offset (number of shared atom positions between adjacent rings) |
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
225
|
+
|
|
226
|
+
// Naphthalene (2 fused rings)
|
|
227
|
+
const naphthalene = benzene.fusedRepeat(2, 4);
|
|
228
|
+
|
|
229
|
+
// Anthracene (3 fused rings)
|
|
230
|
+
const anthracene = benzene.fusedRepeat(3, 4);
|
|
231
|
+
|
|
232
|
+
// Tetracene (4 fused rings)
|
|
233
|
+
const tetracene = benzene.fusedRepeat(4, 4);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `ring.mirror(pivotId?)`
|
|
237
|
+
|
|
238
|
+
Mirror a ring's attachments and substitutions to create symmetric patterns. The pivot defines the axis of symmetry on the ring.
|
|
239
|
+
|
|
240
|
+
| Parameter | Type | Default | Description |
|
|
241
|
+
|-----------|------|---------|-------------|
|
|
242
|
+
| `pivotId` | `number` | `1` | 1-indexed ring position that serves as the symmetry axis |
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
246
|
+
|
|
247
|
+
// meta-dimethylbenzene: attach at 2, mirror around pivot 3
|
|
248
|
+
const mono = benzene.attach(2, Linear(['C']));
|
|
249
|
+
const meta = mono.mirror(3);
|
|
250
|
+
console.log(meta.smiles); // c1c(C)cc(C)cc1
|
|
251
|
+
|
|
252
|
+
// Symmetric nitrogen substitution
|
|
253
|
+
const pyridine = Ring({ atoms: 'c', size: 6, substitutions: { 2: 'n' } });
|
|
254
|
+
const diazine = pyridine.mirror(1); // n at 2 and 6
|
|
255
|
+
```
|
|
256
|
+
|
|
193
257
|
### Linear Methods
|
|
194
258
|
|
|
195
259
|
```javascript
|
|
@@ -225,6 +289,53 @@ Attach one or more branches at a position.
|
|
|
225
289
|
|
|
226
290
|
Attach branches at multiple positions. `branchMap` is `{ position: node | [nodes] }`.
|
|
227
291
|
|
|
292
|
+
#### `linear.repeat(n, leftId, rightId)`
|
|
293
|
+
|
|
294
|
+
Repeat the linear chain `n` times to build polymer chains.
|
|
295
|
+
|
|
296
|
+
| Parameter | Type | Description |
|
|
297
|
+
|-----------|------|-------------|
|
|
298
|
+
| `n` | `number` | Number of repeating units (>= 1) |
|
|
299
|
+
| `leftId` | `number` | 1-indexed left (incoming) attachment point |
|
|
300
|
+
| `rightId` | `number` | 1-indexed right (outgoing) attachment point |
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
// Polyethylene trimer
|
|
304
|
+
const ethylene = Linear(['C', 'C']);
|
|
305
|
+
const PE = ethylene.repeat(3, 1, 2);
|
|
306
|
+
console.log(PE.smiles); // CCCCCC
|
|
307
|
+
|
|
308
|
+
// Polystyrene dimer
|
|
309
|
+
const styrene = Linear(['C', 'C']).attach(2, Ring({ atoms: 'c', size: 6 }));
|
|
310
|
+
const PS = styrene.repeat(2, 1, 2);
|
|
311
|
+
console.log(PS.smiles); // CC(c1ccccc1)CC(c2ccccc2)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `linear.mirror(pivotId?)`
|
|
315
|
+
|
|
316
|
+
Mirror a linear chain around a pivot atom to create palindromic (A-B-A) patterns. The pivot atom appears once at the center.
|
|
317
|
+
|
|
318
|
+
| Parameter | Type | Default | Description |
|
|
319
|
+
|-----------|------|---------|-------------|
|
|
320
|
+
| `pivotId` | `number` | `atoms.length` | 1-indexed pivot position (center of symmetry) |
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
// Diethyl ether: mirror ethanol around the oxygen
|
|
324
|
+
const chain = Linear(['C', 'C', 'O']);
|
|
325
|
+
const ether = chain.mirror(); // pivot defaults to last atom
|
|
326
|
+
console.log(ether.smiles); // CCOCC
|
|
327
|
+
|
|
328
|
+
// Symmetric alkene: mirror C=C around position 2
|
|
329
|
+
const vinyl = Linear(['C', 'C'], ['=']);
|
|
330
|
+
const symAlkene = vinyl.mirror(2);
|
|
331
|
+
console.log(symAlkene.smiles); // C=CC
|
|
332
|
+
|
|
333
|
+
// With attachments: phenyl pendants are mirrored too
|
|
334
|
+
const base = Linear(['C', 'C', 'C']).attach(1, Ring({ atoms: 'c', size: 6 }));
|
|
335
|
+
const mirrored = base.mirror();
|
|
336
|
+
console.log(mirrored.smiles); // C(c1ccccc1)CCC(c2ccccc2)C
|
|
337
|
+
```
|
|
338
|
+
|
|
228
339
|
### Molecule Methods
|
|
229
340
|
|
|
230
341
|
```javascript
|
|
@@ -244,6 +355,41 @@ const modified = mol.replaceComponent(0, Linear(['N', 'N']));
|
|
|
244
355
|
|
|
245
356
|
// Concatenate molecules
|
|
246
357
|
const combined = mol.concat(Molecule([Ring({ atoms: 'c', size: 6 })]));
|
|
358
|
+
|
|
359
|
+
// Repeat the molecule as a polymer unit
|
|
360
|
+
const unit = Molecule([Linear(['C']), Ring({ atoms: 'c', size: 6 })]);
|
|
361
|
+
const dimer = unit.repeat(2, 1, 1);
|
|
362
|
+
console.log(dimer.smiles); // Cc1ccccc1Cc2ccccc2
|
|
363
|
+
|
|
364
|
+
// Mirror molecule for ABA patterns
|
|
365
|
+
const A = Linear(['C', 'C']);
|
|
366
|
+
const B = Ring({ atoms: 'c', size: 6 });
|
|
367
|
+
const AB = Molecule([A, B]);
|
|
368
|
+
const ABA = AB.mirror(); // pivot defaults to last component
|
|
369
|
+
console.log(ABA.smiles); // CCc1ccccc1CC
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### `molecule.mirror(pivotComponent?)`
|
|
373
|
+
|
|
374
|
+
Mirror a molecule's component sequence to create ABA or ABCBA patterns. The pivot component appears once at the center.
|
|
375
|
+
|
|
376
|
+
| Parameter | Type | Default | Description |
|
|
377
|
+
|-----------|------|---------|-------------|
|
|
378
|
+
| `pivotComponent` | `number` | `components.length - 1` | 0-indexed pivot component (center of symmetry) |
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
// ABA triblock: Linear-Ring-Linear
|
|
382
|
+
const A = Linear(['C', 'C']);
|
|
383
|
+
const B = Ring({ atoms: 'c', size: 6 });
|
|
384
|
+
const AB = Molecule([A, B]);
|
|
385
|
+
const ABA = AB.mirror();
|
|
386
|
+
console.log(ABA.smiles); // CCc1ccccc1CC
|
|
387
|
+
|
|
388
|
+
// ABCBA pentablock
|
|
389
|
+
const C = Linear(['N']);
|
|
390
|
+
const ABC = Molecule([A, B, C]);
|
|
391
|
+
const ABCBA = ABC.mirror();
|
|
392
|
+
console.log(ABCBA.smiles); // CCc1ccccc1NCCc2ccccc2CC... (mirrored)
|
|
247
393
|
```
|
|
248
394
|
|
|
249
395
|
### FusedRing Methods
|
|
@@ -276,6 +422,9 @@ const withSeq = fused.addSequentialRings([{ ring: ring3, depth: 1 }, { ring: rin
|
|
|
276
422
|
|
|
277
423
|
// Add attachment to a sequential atom position
|
|
278
424
|
const withAtt = fused.addSequentialAtomAttachment(25, Linear(['O']));
|
|
425
|
+
|
|
426
|
+
// Repeat the fused ring system as a polymer unit
|
|
427
|
+
const fusedDimer = fused.repeat(2, 1, 1);
|
|
279
428
|
```
|
|
280
429
|
|
|
281
430
|
#### `fusedRing.addSequentialRings(rings, options?)`
|
|
@@ -431,10 +580,23 @@ import {
|
|
|
431
580
|
moleculeConcat,
|
|
432
581
|
moleculeGetComponent,
|
|
433
582
|
moleculeReplaceComponent,
|
|
583
|
+
repeat,
|
|
584
|
+
fusedRepeat,
|
|
585
|
+
linearMirror,
|
|
586
|
+
moleculeMirror,
|
|
587
|
+
ringMirror,
|
|
434
588
|
} from 'smiles-js/manipulation';
|
|
435
589
|
|
|
436
590
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
437
591
|
const toluene = ringAttach(benzene, 1, Linear(['C']));
|
|
592
|
+
|
|
593
|
+
// Polymer construction
|
|
594
|
+
const biphenyl = repeat(benzene, 2, 1, 6);
|
|
595
|
+
const naphthalene = fusedRepeat(benzene, 2, 4);
|
|
596
|
+
|
|
597
|
+
// Mirror symmetry
|
|
598
|
+
const chain = Linear(['C', 'C', 'O']);
|
|
599
|
+
const ether = linearMirror(chain); // CCOCC
|
|
438
600
|
```
|
|
439
601
|
|
|
440
602
|
---
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ Build complex molecules programmatically with an intuitive, composable API. Pars
|
|
|
16
16
|
|
|
17
17
|
- **Parse complex SMILES** - Handles real-world pharmaceutical molecules (60-80+ characters)
|
|
18
18
|
- **Programmatic construction** - Build molecules using composable Ring, Linear, and Molecule constructors
|
|
19
|
+
- **Polymer construction** - Build repeating units with `.repeat()` and fused acene systems with `.fusedRepeat()`
|
|
20
|
+
- **Mirror symmetry** - Create palindromic chains and ABA block patterns with `.mirror()`
|
|
19
21
|
- **Round-trip fidelity** - Parse SMILES -> AST -> SMILES with structure preservation
|
|
20
22
|
- **Code generation** - Auto-generate JavaScript construction code from SMILES strings
|
|
21
23
|
- **Pharmaceutical validated** - Tested with Atorvastatin, Sildenafil, Ritonavir, and 30+ other drugs
|
|
@@ -85,6 +87,43 @@ const pyridine = benzene.substitute(5, 'n');
|
|
|
85
87
|
console.log(pyridine.smiles); // c1cccnc1
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
### Build Polymers
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import { Ring, Linear } from 'smiles-js';
|
|
94
|
+
|
|
95
|
+
// Polyethylene trimer: repeat ethylene unit 3 times
|
|
96
|
+
const ethylene = Linear(['C', 'C']);
|
|
97
|
+
const PE = ethylene.repeat(3, 1, 2);
|
|
98
|
+
console.log(PE.smiles); // CCCCCC
|
|
99
|
+
|
|
100
|
+
// Polystyrene dimer: repeat styrene unit with phenyl branch
|
|
101
|
+
const styrene = Linear(['C', 'C']).attach(2, Ring({ atoms: 'c', size: 6 }));
|
|
102
|
+
const PS = styrene.repeat(2, 1, 2);
|
|
103
|
+
console.log(PS.smiles); // CC(c1ccccc1)CC(c2ccccc2)
|
|
104
|
+
|
|
105
|
+
// Acene series via fused repeat
|
|
106
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
107
|
+
const naphthalene = benzene.fusedRepeat(2, 4); // 2 fused rings
|
|
108
|
+
const anthracene = benzene.fusedRepeat(3, 4); // 3 fused rings
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Mirror Symmetry
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
import { Ring, Linear, Molecule } from 'smiles-js';
|
|
115
|
+
|
|
116
|
+
// Diethyl ether: mirror C-C-O around oxygen
|
|
117
|
+
const ether = Linear(['C', 'C', 'O']).mirror();
|
|
118
|
+
console.log(ether.smiles); // CCOCC
|
|
119
|
+
|
|
120
|
+
// ABA triblock copolymer
|
|
121
|
+
const A = Linear(['C', 'C']);
|
|
122
|
+
const B = Ring({ atoms: 'c', size: 6 });
|
|
123
|
+
const ABA = Molecule([A, B]).mirror();
|
|
124
|
+
console.log(ABA.smiles); // CCc1ccccc1CC
|
|
125
|
+
```
|
|
126
|
+
|
|
88
127
|
### Generate Construction Code
|
|
89
128
|
|
|
90
129
|
```javascript
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Plan: `.mirror()` API for Symmetric Molecule Construction
|
|
2
|
+
|
|
3
|
+
## Motivation
|
|
4
|
+
|
|
5
|
+
Many molecules and polymers are symmetric — their structure reads the same forwards and backwards around a central point. Building these today requires manually constructing both halves. A `.mirror()` method would let users define one half and automatically produce the symmetric whole.
|
|
6
|
+
|
|
7
|
+
### Use Cases
|
|
8
|
+
|
|
9
|
+
1. **ABA triblock copolymers** — The most common symmetric polymer architecture. Define the A and B blocks, get A-B-A automatically.
|
|
10
|
+
2. **Palindromic linear chains** — e.g., `CCCOCCC` from `CCCO` mirrored at the O.
|
|
11
|
+
3. **Symmetric branched molecules** — e.g., diethyl ether `CCOCC` from `CCO` mirrored.
|
|
12
|
+
4. **Symmetric ring-bearing chains** — e.g., a chain with a ring in the center and identical arms on each side.
|
|
13
|
+
5. **Dendrimers** — Symmetric branching structures built outward from a core.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## API Design
|
|
18
|
+
|
|
19
|
+
### `linear.mirror(pivotId?)`
|
|
20
|
+
|
|
21
|
+
Mirror a linear chain around a pivot atom to create a palindromic structure.
|
|
22
|
+
|
|
23
|
+
| Parameter | Type | Default | Description |
|
|
24
|
+
|-----------|------|---------|-------------|
|
|
25
|
+
| `pivotId` | `number` | `atoms.length` | 1-indexed position of the pivot atom (included once in the output) |
|
|
26
|
+
|
|
27
|
+
**Returns:** A new `Linear` (for simple chains) or `Molecule` (if attachments are present).
|
|
28
|
+
|
|
29
|
+
**Behavior:** Takes atoms `[1..pivotId]`, then appends atoms `[pivotId-1..1]` in reverse. The pivot atom appears once in the center. Attachments on mirrored atoms are also mirrored.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
// Diethyl ether: CCO + mirror → CCOCC
|
|
33
|
+
const half = Linear(['C', 'C', 'O']);
|
|
34
|
+
const ether = half.mirror(); // pivot defaults to last atom (O)
|
|
35
|
+
console.log(ether.smiles); // CCOCC
|
|
36
|
+
|
|
37
|
+
// Palindromic chain: CCCNCCC
|
|
38
|
+
const half2 = Linear(['C', 'C', 'C', 'N']);
|
|
39
|
+
const palindrome = half2.mirror(); // pivot at N (position 4)
|
|
40
|
+
console.log(palindrome.smiles); // CCCNCCC
|
|
41
|
+
|
|
42
|
+
// Mirror at a specific pivot
|
|
43
|
+
const chain = Linear(['C', 'C', 'C', 'O', 'C']);
|
|
44
|
+
const mirrored = chain.mirror(4); // pivot at O (position 4)
|
|
45
|
+
console.log(mirrored.smiles); // CCCOCCC
|
|
46
|
+
// atoms [1,2,3,4] + reverse of [3,2,1] → C,C,C,O,C,C,C
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `ring.mirror(pivotId?)`
|
|
50
|
+
|
|
51
|
+
Mirror a ring's attachments to create symmetric substitution patterns.
|
|
52
|
+
|
|
53
|
+
| Parameter | Type | Default | Description |
|
|
54
|
+
|-----------|------|---------|-------------|
|
|
55
|
+
| `pivotId` | `number` | `1` | 1-indexed ring position that serves as the symmetry axis |
|
|
56
|
+
|
|
57
|
+
**Returns:** A new `Ring` with attachments mirrored around the pivot.
|
|
58
|
+
|
|
59
|
+
**Behavior:** For a ring with an attachment at position `p`, also adds the same attachment at the "mirror" position relative to the pivot. The mirror position for `p` around pivot `v` on a ring of size `s` is: `((2*v - p) mod s)`, adjusted to 1-indexed range.
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// Symmetric toluene → xylene (1,4-dimethylbenzene)
|
|
63
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
64
|
+
const toluene = benzene.attach(1, Linear(['C']));
|
|
65
|
+
const xylene = toluene.mirror(1); // mirror around position 1
|
|
66
|
+
// Attachment at position 1 stays, mirrored attachment appears at position 4
|
|
67
|
+
console.log(xylene.smiles); // c1(C)ccc(C)cc1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `molecule.mirror(pivotComponent?)`
|
|
71
|
+
|
|
72
|
+
Mirror a molecule's component sequence to create an ABA-like structure.
|
|
73
|
+
|
|
74
|
+
| Parameter | Type | Default | Description |
|
|
75
|
+
|-----------|------|---------|-------------|
|
|
76
|
+
| `pivotComponent` | `number` | `components.length - 1` | 0-indexed component that serves as the center |
|
|
77
|
+
|
|
78
|
+
**Returns:** A new `Molecule` with components mirrored.
|
|
79
|
+
|
|
80
|
+
**Behavior:** Takes components `[0..pivot]`, then appends components `[pivot-1..0]` in reverse (with ring renumbering to avoid collisions).
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
// ABA triblock copolymer
|
|
84
|
+
const A = Linear(['C', 'C']);
|
|
85
|
+
const B = Ring({ atoms: 'c', size: 6 });
|
|
86
|
+
const AB = Molecule([A, B]);
|
|
87
|
+
const ABA = AB.mirror(); // pivot at last component (B)
|
|
88
|
+
console.log(ABA.smiles); // CCc1ccccc1CC
|
|
89
|
+
|
|
90
|
+
// More complex: A-B-C-B-A from A-B-C
|
|
91
|
+
const block = Molecule([
|
|
92
|
+
Linear(['C', 'C']),
|
|
93
|
+
Ring({ atoms: 'c', size: 6 }),
|
|
94
|
+
Linear(['O']),
|
|
95
|
+
]);
|
|
96
|
+
const symmetric = block.mirror(); // pivot at last (O)
|
|
97
|
+
console.log(symmetric.smiles); // CCc1ccccc1Oc2ccccc2CC
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `fusedRing.mirror()`
|
|
101
|
+
|
|
102
|
+
For fused rings, mirror could create a symmetric fused system by adding rings on both sides of the base. This is more complex and may be deferred to a later iteration.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Functional API
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
import {
|
|
110
|
+
linearMirror,
|
|
111
|
+
ringMirror,
|
|
112
|
+
moleculeMirror,
|
|
113
|
+
} from 'smiles-js/manipulation';
|
|
114
|
+
|
|
115
|
+
const half = Linear(['C', 'C', 'O']);
|
|
116
|
+
const ether = linearMirror(half); // CCOCC
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Implementation Strategy
|
|
122
|
+
|
|
123
|
+
### Phase 1: `linear.mirror(pivotId?)`
|
|
124
|
+
|
|
125
|
+
The simplest and highest-value case. Pure atom/bond array manipulation.
|
|
126
|
+
|
|
127
|
+
**Algorithm:**
|
|
128
|
+
1. Validate `pivotId` is within `[1, atoms.length]`.
|
|
129
|
+
2. Take `leftAtoms = atoms[0..pivotId-1]` (the left half including pivot).
|
|
130
|
+
3. Take `rightAtoms = atoms[0..pivotId-2].reverse()` (the left half excluding pivot, reversed).
|
|
131
|
+
4. Concatenate: `leftAtoms + rightAtoms`.
|
|
132
|
+
5. Handle bonds: mirror the bond array similarly. Bond between atoms `i` and `i+1` maps to the corresponding mirrored position.
|
|
133
|
+
6. Handle attachments: clone attachments from mirrored positions. Attachments at position `p` (where `p < pivotId`) also appear at the mirror position `2*pivotId - p`.
|
|
134
|
+
|
|
135
|
+
**Files:**
|
|
136
|
+
- `src/manipulation.js` — Add `linearMirror()` function
|
|
137
|
+
- `src/method-attachers.js` — Attach `.mirror()` to Linear
|
|
138
|
+
|
|
139
|
+
### Phase 2: `molecule.mirror(pivotComponent?)`
|
|
140
|
+
|
|
141
|
+
Component-level mirroring for ABA block copolymers.
|
|
142
|
+
|
|
143
|
+
**Algorithm:**
|
|
144
|
+
1. Take `leftComponents = components[0..pivot]`.
|
|
145
|
+
2. Take `rightComponents = components[0..pivot-1].reverse()`, each deep-cloned with shifted ring numbers.
|
|
146
|
+
3. Return `Molecule([...leftComponents, ...rightComponents])`.
|
|
147
|
+
|
|
148
|
+
**Files:**
|
|
149
|
+
- `src/manipulation.js` — Add `moleculeMirror()` function (reuses `shiftRingNumbers` and `maxRingNumber` from the `repeat` implementation)
|
|
150
|
+
- `src/method-attachers.js` — Attach `.mirror()` to Molecule
|
|
151
|
+
|
|
152
|
+
### Phase 3: `ring.mirror(pivotId?)`
|
|
153
|
+
|
|
154
|
+
Attachment-level mirroring for symmetric ring substitution patterns.
|
|
155
|
+
|
|
156
|
+
**Algorithm:**
|
|
157
|
+
1. For each attachment at position `p`, compute mirror position `mp = 2*pivotId - p` (mod ring size, 1-indexed).
|
|
158
|
+
2. If `mp` doesn't already have the attachment, add it.
|
|
159
|
+
3. Similarly mirror substitutions.
|
|
160
|
+
|
|
161
|
+
**Files:**
|
|
162
|
+
- `src/manipulation.js` — Add `ringMirror()` function
|
|
163
|
+
- `src/method-attachers.js` — Attach `.mirror()` to Ring
|
|
164
|
+
|
|
165
|
+
### Phase 4 (Future): `fusedRing.mirror()`
|
|
166
|
+
|
|
167
|
+
Defer to a later iteration. FusedRing has complex position metadata that makes mirroring non-trivial.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Edge Cases
|
|
172
|
+
|
|
173
|
+
1. **Odd-length palindrome:** `mirror()` on `Linear(['C', 'O', 'C'])` with `pivotId=2` → `COCOC` (the pivot O appears once)
|
|
174
|
+
2. **Single atom:** `Linear(['O']).mirror()` → `Linear(['O'])` (nothing to mirror)
|
|
175
|
+
3. **Already symmetric:** Mirroring an already-symmetric molecule should produce the same result (idempotent on symmetric inputs)
|
|
176
|
+
4. **Bonds in mirror:** Double bonds in the left half should appear in the mirrored right half at the corresponding position
|
|
177
|
+
5. **Attachments with rings:** Mirrored ring attachments need unique ring numbers (reuse `shiftRingNumbers`)
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Test Plan
|
|
182
|
+
|
|
183
|
+
### Unit Tests (`manipulation.test.js`)
|
|
184
|
+
- `linear.mirror()` — simple chain, with pivot, with bonds, with attachments
|
|
185
|
+
- `molecule.mirror()` — ABA, ABCBA, with rings
|
|
186
|
+
- `ring.mirror()` — symmetric substitutions, symmetric attachments
|
|
187
|
+
- Edge cases: n=1, already symmetric, single atom
|
|
188
|
+
|
|
189
|
+
### Integration Tests (`test-integration/mirror.test.js`)
|
|
190
|
+
- Diethyl ether: `CCO.mirror()` → `CCOCC`
|
|
191
|
+
- ABA block copolymer with polystyrene/polyethylene blocks
|
|
192
|
+
- Symmetric biphenyl-bridged molecule
|
|
193
|
+
- Palindromic peptide-like chain
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Relationship to `.repeat()`
|
|
198
|
+
|
|
199
|
+
`.mirror()` and `.repeat()` are complementary:
|
|
200
|
+
- `.repeat(n, left, right)` — Produces **A-A-A-A** (homopolymer)
|
|
201
|
+
- `.mirror()` — Produces **A-B-A** (symmetric structure)
|
|
202
|
+
- Combined: `.repeat(2).mirror()` could produce **A-A-B-A-A** (repeated then mirrored)
|
|
203
|
+
|
|
204
|
+
Together they cover the main polymer architectures: homopolymer, block copolymer, and symmetric block copolymer.
|
package/package.json
CHANGED
|
@@ -76,14 +76,35 @@ export function buildBranchCrossingRingSMILES(ring, buildSMILES) {
|
|
|
76
76
|
|
|
77
77
|
if (attachments[i] && attachments[i].length > 0) {
|
|
78
78
|
if (hasInlineBranchAfter) {
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// Determine the branchId of the inline continuation (next ring position)
|
|
80
|
+
const metaBranchIds = ring.metaBranchIds || [];
|
|
81
|
+
const inlineBranchId = i < size ? (metaBranchIds[i] || Infinity) : Infinity;
|
|
82
|
+
|
|
83
|
+
// Separate: siblings that come BEFORE the inline branch (emit now)
|
|
84
|
+
// vs siblings that come AFTER (delay), and non-siblings (delay)
|
|
85
|
+
const emitNow = [];
|
|
86
|
+
const delay = [];
|
|
87
|
+
attachments[i].forEach((att) => {
|
|
88
|
+
const isSibling = att.metaIsSibling !== undefined ? att.metaIsSibling : true;
|
|
89
|
+
if (!isSibling) {
|
|
90
|
+
delay.push(att);
|
|
91
|
+
} else if (att.metaBranchId !== undefined && att.metaBranchId < inlineBranchId) {
|
|
92
|
+
emitNow.push(att);
|
|
93
|
+
} else if (att.metaBeforeInline === true) {
|
|
94
|
+
emitNow.push(att);
|
|
95
|
+
} else {
|
|
96
|
+
// Default: delay sibling (beforeInline defaults to false)
|
|
97
|
+
delay.push({ ...att, metaIsSibling: true });
|
|
83
98
|
}
|
|
84
|
-
return { ...att, metaIsSibling: true };
|
|
85
99
|
});
|
|
86
|
-
|
|
100
|
+
// Emit siblings that come before the inline branch
|
|
101
|
+
emitNow.forEach((attachment) => {
|
|
102
|
+
emitAttachment(parts, attachment, buildSMILES);
|
|
103
|
+
});
|
|
104
|
+
// Delay the rest
|
|
105
|
+
if (delay.length > 0) {
|
|
106
|
+
pendingAttachments.set(i, { depth: posDepth, attachments: delay });
|
|
107
|
+
}
|
|
87
108
|
} else {
|
|
88
109
|
// Output attachments immediately
|
|
89
110
|
attachments[i].forEach((attachment) => {
|
|
@@ -103,11 +103,33 @@ export function buildInterleavedFusedRingSMILES(fusedRing, buildSMILES) {
|
|
|
103
103
|
// Pending attachments: depth -> attachment[]
|
|
104
104
|
const pendingAttachments = new Map();
|
|
105
105
|
|
|
106
|
+
const branchIdMap = fusedRing.metaBranchIdMap || new Map();
|
|
107
|
+
let prevBranchId = null;
|
|
108
|
+
|
|
106
109
|
allPositions.forEach((pos, idx) => {
|
|
107
110
|
const entry = atomSequence[pos];
|
|
108
111
|
if (!entry) return;
|
|
109
112
|
|
|
110
113
|
const posDepth = branchDepthMap.get(pos) || 0;
|
|
114
|
+
const posBranchId = branchIdMap.get(pos);
|
|
115
|
+
|
|
116
|
+
// Detect sibling branch switch: same depth, different branch ID
|
|
117
|
+
// Need to close current branch and reopen a new one
|
|
118
|
+
if (posDepth > 0 && posDepth === depthRef.value && prevBranchId !== null
|
|
119
|
+
&& posBranchId !== prevBranchId && posBranchId !== null) {
|
|
120
|
+
// Close the current branch and reopen
|
|
121
|
+
parts.push(')');
|
|
122
|
+
// Emit any pending attachments at the parent depth
|
|
123
|
+
const parentDepth = posDepth - 1;
|
|
124
|
+
if (pendingAttachments.has(parentDepth)) {
|
|
125
|
+
const attachmentsToOutput = pendingAttachments.get(parentDepth);
|
|
126
|
+
attachmentsToOutput.forEach((attachment) => {
|
|
127
|
+
emitAttachment(parts, attachment, buildSMILES);
|
|
128
|
+
});
|
|
129
|
+
pendingAttachments.delete(parentDepth);
|
|
130
|
+
}
|
|
131
|
+
parts.push('(');
|
|
132
|
+
}
|
|
111
133
|
|
|
112
134
|
// Handle branch depth changes
|
|
113
135
|
openBranches(parts, depthRef, posDepth);
|
|
@@ -164,6 +186,8 @@ export function buildInterleavedFusedRingSMILES(fusedRing, buildSMILES) {
|
|
|
164
186
|
});
|
|
165
187
|
}
|
|
166
188
|
}
|
|
189
|
+
|
|
190
|
+
prevBranchId = posBranchId;
|
|
167
191
|
});
|
|
168
192
|
|
|
169
193
|
// Close any remaining open branches
|
package/src/decompiler.js
CHANGED
|
@@ -115,17 +115,38 @@ function generateAttachmentCode(ring, indent, nextVar, initialVar) {
|
|
|
115
115
|
let currentVar = initialVar;
|
|
116
116
|
|
|
117
117
|
if (ring.attachments && Object.keys(ring.attachments).length > 0) {
|
|
118
|
+
// Determine inline branchId for sibling ordering
|
|
119
|
+
const metaBranchIds = ring.metaBranchIds || [];
|
|
120
|
+
const normalizedDepths = ring.metaBranchDepths || [];
|
|
121
|
+
|
|
118
122
|
Object.entries(ring.attachments).forEach(([pos, attachmentList]) => {
|
|
123
|
+
const posIdx = Number(pos) - 1; // 0-indexed
|
|
124
|
+
const nextIdx = posIdx + 1;
|
|
125
|
+
const posDepth = normalizedDepths[posIdx] || 0;
|
|
126
|
+
const nextDepth = nextIdx < normalizedDepths.length ? (normalizedDepths[nextIdx] || 0) : 0;
|
|
127
|
+
const hasInlineBranchAfter = nextDepth > posDepth;
|
|
128
|
+
const inlineBranchId = hasInlineBranchAfter ? (metaBranchIds[nextIdx] || Infinity) : Infinity;
|
|
129
|
+
|
|
119
130
|
attachmentList.forEach((attachment) => {
|
|
120
131
|
const attachResult = decompileChildNode(attachment, indent, nextVar);
|
|
121
132
|
lines.push(attachResult.code);
|
|
122
133
|
|
|
123
134
|
const newVar = nextVar();
|
|
124
135
|
const isSibling = attachment.metaIsSibling;
|
|
136
|
+
const optParts = [];
|
|
125
137
|
if (isSibling === true) {
|
|
126
|
-
|
|
138
|
+
optParts.push('sibling: true');
|
|
139
|
+
// Encode beforeInline for sibling ordering in branch-crossing rings
|
|
140
|
+
// Only emit when true since false is the default
|
|
141
|
+
if (hasInlineBranchAfter && attachment.metaBranchId !== undefined
|
|
142
|
+
&& attachment.metaBranchId < inlineBranchId) {
|
|
143
|
+
optParts.push('beforeInline: true');
|
|
144
|
+
}
|
|
127
145
|
} else if (isSibling === false) {
|
|
128
|
-
|
|
146
|
+
optParts.push('sibling: false');
|
|
147
|
+
}
|
|
148
|
+
if (optParts.length > 0) {
|
|
149
|
+
lines.push(`${indent}const ${newVar} = ${currentVar}.attach(${pos}, ${attachResult.finalVar}, { ${optParts.join(', ')} });`);
|
|
129
150
|
} else {
|
|
130
151
|
lines.push(`${indent}const ${newVar} = ${currentVar}.attach(${pos}, ${attachResult.finalVar});`);
|
|
131
152
|
}
|
|
@@ -758,6 +779,7 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
758
779
|
const atomValueMap = fusedRing.metaAtomValueMap || new Map();
|
|
759
780
|
const bondMap = fusedRing.metaBondMap || new Map();
|
|
760
781
|
const ringOrderMap = fusedRing.metaRingOrderMap;
|
|
782
|
+
const branchIdMap = fusedRing.metaBranchIdMap || new Map();
|
|
761
783
|
|
|
762
784
|
// Helper to format a single atom entry with all available metadata
|
|
763
785
|
function formatAtomEntry(pos) {
|
|
@@ -768,6 +790,9 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
768
790
|
if (ringOrderMap && ringOrderMap.has(pos)) {
|
|
769
791
|
parts.push(`rings: [${ringOrderMap.get(pos).join(', ')}]`);
|
|
770
792
|
}
|
|
793
|
+
if (branchIdMap.has(pos) && branchIdMap.get(pos) !== null) {
|
|
794
|
+
parts.push(`branchId: ${branchIdMap.get(pos)}`);
|
|
795
|
+
}
|
|
771
796
|
if (seqAtomAttachmentVarMap.has(pos)) {
|
|
772
797
|
parts.push(`attachments: [${seqAtomAttachmentVarMap.get(pos).join(', ')}]`);
|
|
773
798
|
}
|