smiles-js 0.2.0 → 0.2.2

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.
@@ -19,5 +19,8 @@ jobs:
19
19
  with:
20
20
  node-version: '20'
21
21
 
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
22
25
  - name: Run tests
23
26
  run: npm test
@@ -29,6 +29,9 @@ jobs:
29
29
  node-version: '20'
30
30
  registry-url: 'https://registry.npmjs.org'
31
31
 
32
+ - name: Install dependencies
33
+ run: npm ci
34
+
32
35
  - name: Run tests
33
36
  run: npm test
34
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smiles-js",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A JavaScript library for building molecules using composable fragments",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -22,5 +22,8 @@
22
22
  "dsl"
23
23
  ],
24
24
  "author": "",
25
- "license": "MIT"
25
+ "license": "MIT",
26
+ "devDependencies": {
27
+ "@rdkit/rdkit": "^2025.3.4-1.0.0"
28
+ }
26
29
  }
package/src/common.js CHANGED
@@ -29,5 +29,5 @@ export const furan = Ring('c', 5, { replace: { 0: 'o' } });
29
29
  export const thiophene = Ring('c', 5, { replace: { 0: 's' } });
30
30
 
31
31
  export const naphthalene = FusedRings([6, 6], 'c');
32
- export const indole = FusedRings([6, 5], 'c', { hetero: { 7: '[nH]' } });
32
+ export const indole = FusedRings([6, 5], 'c', { hetero: { 4: '[nH]' } });
33
33
  export const quinoline = FusedRings([6, 6], 'c', { hetero: { 0: 'n' } });
package/src/ring.js CHANGED
@@ -1,20 +1,113 @@
1
1
  import { Fragment } from './fragment.js';
2
+ import { findUsedRingNumbers, getNextRingNumber } from './utils.js';
2
3
 
3
4
  export function Ring(atom, size, options = {}) {
4
5
  const { replace = {} } = options;
5
6
 
6
- let smiles = '';
7
- for (let i = 0; i < size; i++) {
8
- const currentAtom = replace[i] !== undefined ? replace[i] : atom;
7
+ // Build the ring SMILES
8
+ const buildRingSmiles = (replacements) => {
9
+ const parts = [];
9
10
 
10
- if (i === 0) {
11
- smiles += currentAtom + '1';
12
- } else if (i === size - 1) {
13
- smiles += currentAtom + '1';
14
- } else {
15
- smiles += currentAtom;
11
+ for (let i = 0; i < size; i++) {
12
+ const currentAtom = replacements[i] !== undefined ? replacements[i] : atom;
13
+
14
+ if (i === 0) {
15
+ parts.push(currentAtom + '1');
16
+ } else if (i === size - 1) {
17
+ parts.push(currentAtom + '1');
18
+ } else {
19
+ parts.push(currentAtom);
20
+ }
16
21
  }
17
- }
18
22
 
19
- return Fragment(smiles);
23
+ return parts.join('');
24
+ };
25
+
26
+ const smiles = buildRingSmiles(replace);
27
+ const fragment = Fragment(smiles);
28
+
29
+ // Track substituents for chaining
30
+ const substituents = {};
31
+
32
+ // Add attachAt method to the fragment
33
+ const createAttachAt = (currentSubstituents) => {
34
+ return function(position, substituent) {
35
+ if (position < 1 || position > size) {
36
+ throw new Error(`Position ${position} is out of range for ring of size ${size}`);
37
+ }
38
+
39
+ // Convert 1-based position to 0-based index
40
+ const index = position - 1;
41
+
42
+ // Create new substituents map with this addition
43
+ const newSubstituents = { ...currentSubstituents };
44
+
45
+ // Handle substituent - could be string, Fragment, or Ring
46
+ let subSmiles;
47
+ if (typeof substituent === 'string') {
48
+ subSmiles = substituent;
49
+ } else if (substituent.smiles) {
50
+ subSmiles = substituent.smiles;
51
+ } else {
52
+ subSmiles = String(substituent);
53
+ }
54
+
55
+ newSubstituents[index] = subSmiles;
56
+
57
+ // Build SMILES with all substituents at the specified positions
58
+ const parts = [];
59
+
60
+ for (let i = 0; i < size; i++) {
61
+ const currentAtom = replace[i] !== undefined ? replace[i] : atom;
62
+
63
+ if (newSubstituents[i]) {
64
+ // Remap ring numbers in substituent to avoid conflicts
65
+ const currentSmiles = parts.join('');
66
+ const usedInCurrent = findUsedRingNumbers(currentSmiles);
67
+ const usedInSubstituent = findUsedRingNumbers(newSubstituents[i]);
68
+
69
+ let remappedSubstituent = newSubstituents[i];
70
+ for (const ringNum of usedInSubstituent) {
71
+ if (usedInCurrent.has(ringNum)) {
72
+ const newNum = getNextRingNumber(currentSmiles + remappedSubstituent);
73
+ // For %NN format, replace the whole %NN
74
+ // For single digit, replace all occurrences (this works because each ring number appears exactly twice)
75
+ if (ringNum.length > 1) {
76
+ remappedSubstituent = remappedSubstituent.replaceAll(`%${ringNum}`, newNum);
77
+ } else {
78
+ remappedSubstituent = remappedSubstituent.replaceAll(ringNum, newNum.replace('%', ''));
79
+ }
80
+ }
81
+ }
82
+
83
+ // Add substituent at this position
84
+ if (i === 0) {
85
+ parts.push(currentAtom + '1(' + remappedSubstituent + ')');
86
+ } else if (i === size - 1) {
87
+ parts.push(currentAtom + '(' + remappedSubstituent + ')1');
88
+ } else {
89
+ parts.push(currentAtom + '(' + remappedSubstituent + ')');
90
+ }
91
+ } else if (i === 0) {
92
+ parts.push(currentAtom + '1');
93
+ } else if (i === size - 1) {
94
+ parts.push(currentAtom + '1');
95
+ } else {
96
+ parts.push(currentAtom);
97
+ }
98
+ }
99
+
100
+ const newSmiles = parts.join('');
101
+ const newFragment = Fragment(newSmiles);
102
+
103
+ // Recursively add attachAt to the new fragment with updated substituents
104
+ newFragment.attachAt = createAttachAt(newSubstituents);
105
+
106
+ return newFragment;
107
+ };
108
+ };
109
+
110
+ fragment.attachAt = createAttachAt(substituents);
111
+
112
+ return fragment;
20
113
  }
@@ -1,5 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
+ import { isValidSMILES } from './test-utils.js';
3
4
  import {
4
5
  methyl, ethyl, propyl, isopropyl, butyl, tbutyl,
5
6
  hydroxyl, amino, carboxyl, carbonyl, nitro, cyano,
@@ -8,53 +9,81 @@ import {
8
9
  naphthalene, indole, quinoline
9
10
  } from '../src/common.js';
10
11
 
11
- test('Common alkyls are defined', () => {
12
+ test('Common alkyls are defined', async () => {
12
13
  assert.strictEqual(methyl.smiles, 'C');
14
+ assert.ok(await isValidSMILES(methyl.smiles));
13
15
  assert.strictEqual(ethyl.smiles, 'CC');
16
+ assert.ok(await isValidSMILES(ethyl.smiles));
14
17
  assert.strictEqual(propyl.smiles, 'CCC');
18
+ assert.ok(await isValidSMILES(propyl.smiles));
15
19
  assert.strictEqual(isopropyl.smiles, 'C(C)C');
20
+ assert.ok(await isValidSMILES(isopropyl.smiles));
16
21
  assert.strictEqual(butyl.smiles, 'CCCC');
22
+ assert.ok(await isValidSMILES(butyl.smiles));
17
23
  assert.strictEqual(tbutyl.smiles, 'C(C)(C)C');
24
+ assert.ok(await isValidSMILES(tbutyl.smiles));
18
25
  });
19
26
 
20
- test('Common functional groups are defined', () => {
27
+ test('Common functional groups are defined', async () => {
21
28
  assert.strictEqual(hydroxyl.smiles, 'O');
29
+ assert.ok(await isValidSMILES(hydroxyl.smiles));
22
30
  assert.strictEqual(amino.smiles, 'N');
31
+ assert.ok(await isValidSMILES(amino.smiles));
23
32
  assert.strictEqual(carboxyl.smiles, 'C(=O)O');
33
+ assert.ok(await isValidSMILES(carboxyl.smiles));
24
34
  assert.strictEqual(carbonyl.smiles, 'C=O');
35
+ assert.ok(await isValidSMILES(carbonyl.smiles));
25
36
  assert.strictEqual(nitro.smiles, '[N+](=O)[O-]');
37
+ assert.ok(await isValidSMILES(nitro.smiles));
26
38
  assert.strictEqual(cyano.smiles, 'C#N');
39
+ assert.ok(await isValidSMILES(cyano.smiles));
27
40
  });
28
41
 
29
- test('Common halogens are defined', () => {
42
+ test('Common halogens are defined', async () => {
30
43
  assert.strictEqual(fluoro.smiles, 'F');
44
+ assert.ok(await isValidSMILES(fluoro.smiles));
31
45
  assert.strictEqual(chloro.smiles, 'Cl');
46
+ assert.ok(await isValidSMILES(chloro.smiles));
32
47
  assert.strictEqual(bromo.smiles, 'Br');
48
+ assert.ok(await isValidSMILES(bromo.smiles));
33
49
  assert.strictEqual(iodo.smiles, 'I');
50
+ assert.ok(await isValidSMILES(iodo.smiles));
34
51
  });
35
52
 
36
- test('Common rings are defined', () => {
53
+ test('Common rings are defined', async () => {
37
54
  assert.strictEqual(benzene.smiles, 'c1ccccc1');
55
+ assert.ok(await isValidSMILES(benzene.smiles));
38
56
  assert.strictEqual(cyclohexane.smiles, 'C1CCCCC1');
57
+ assert.ok(await isValidSMILES(cyclohexane.smiles));
39
58
  assert.strictEqual(pyridine.smiles, 'n1ccccc1');
59
+ assert.ok(await isValidSMILES(pyridine.smiles));
40
60
  assert.strictEqual(pyrrole.smiles, '[nH]1cccc1');
61
+ assert.ok(await isValidSMILES(pyrrole.smiles));
41
62
  assert.strictEqual(furan.smiles, 'o1cccc1');
63
+ assert.ok(await isValidSMILES(furan.smiles));
42
64
  assert.strictEqual(thiophene.smiles, 's1cccc1');
65
+ assert.ok(await isValidSMILES(thiophene.smiles));
43
66
  });
44
67
 
45
- test('Common fused rings are defined', () => {
68
+ test('Common fused rings are defined', async () => {
46
69
  assert.strictEqual(naphthalene.smiles, 'c1ccc2ccccc2c1');
47
- assert.strictEqual(indole.smiles, 'c1ccc2ccc[nH]2c1');
70
+ assert.ok(await isValidSMILES(naphthalene.smiles));
71
+ assert.strictEqual(indole.smiles, 'c1ccc2[nH]ccc2c1');
72
+ assert.ok(await isValidSMILES(indole.smiles));
48
73
  assert.strictEqual(quinoline.smiles, 'n1ccc2ccccc2c1');
74
+ assert.ok(await isValidSMILES(quinoline.smiles));
49
75
  });
50
76
 
51
- test('Common fragments can be composed', () => {
77
+ test('Common fragments can be composed', async () => {
52
78
  const toluene = benzene(methyl);
53
79
  assert.strictEqual(toluene.smiles, 'c1ccccc1(C)');
80
+ assert.ok(await isValidSMILES(toluene.smiles));
54
81
 
55
82
  const phenol = benzene(hydroxyl);
56
83
  assert.strictEqual(phenol.smiles, 'c1ccccc1(O)');
84
+ assert.ok(await isValidSMILES(phenol.smiles));
57
85
 
58
86
  const benzoicAcid = benzene(carboxyl);
59
87
  assert.strictEqual(benzoicAcid.smiles, 'c1ccccc1(C(=O)O)');
88
+ assert.ok(await isValidSMILES(benzoicAcid.smiles));
60
89
  });
@@ -1,190 +1,214 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { Fragment } from '../src/fragment.js';
4
+ import { isValidSMILES } from './test-utils.js';
4
5
 
5
- test('concat combines two simple fragments', () => {
6
+ test('concat combines two simple fragments', async () => {
6
7
  const a = Fragment('CC');
7
8
  const b = Fragment('CC');
8
9
  const result = a.concat(b);
9
10
  assert.strictEqual(result.smiles, 'CCCC');
11
+ assert.ok(await isValidSMILES(result.smiles));
10
12
  });
11
13
 
12
- test('concat works with method chaining', () => {
14
+ test('concat works with method chaining', async () => {
13
15
  const a = Fragment('C');
14
16
  const b = Fragment('C');
15
17
  const c = Fragment('C');
16
18
  const result = a.concat(b).concat(c);
17
19
  assert.strictEqual(result.smiles, 'CCC');
20
+ assert.ok(await isValidSMILES(result.smiles));
18
21
  });
19
22
 
20
- test('concat handles string arguments', () => {
23
+ test('concat handles string arguments', async () => {
21
24
  const a = Fragment('CC');
22
25
  const result = a.concat('CC');
23
26
  assert.strictEqual(result.smiles, 'CCCC');
27
+ assert.ok(await isValidSMILES(result.smiles));
24
28
  });
25
29
 
26
- test('concat preserves properties', () => {
30
+ test('concat preserves properties', async () => {
27
31
  const a = Fragment('C');
28
32
  const b = Fragment('C');
29
33
  const result = a.concat(b);
30
-
34
+
31
35
  assert.strictEqual(result.atoms, 2);
32
36
  assert.strictEqual(result.formula, 'C2H6');
37
+ assert.ok(await isValidSMILES(result.smiles));
33
38
  });
34
39
 
35
- test('concat handles fragments with branches', () => {
40
+ test('concat handles fragments with branches', async () => {
36
41
  const a = Fragment('C(C)C');
37
42
  const b = Fragment('CC');
38
43
  const result = a.concat(b);
39
44
  assert.strictEqual(result.smiles, 'C(C)CCC');
45
+ assert.ok(await isValidSMILES(result.smiles));
40
46
  });
41
47
 
42
- test('concat handles rings without conflicts', () => {
48
+ test('concat handles rings without conflicts', async () => {
43
49
  const a = Fragment('C1CCC1');
44
50
  const b = Fragment('CC');
45
51
  const result = a.concat(b);
46
52
  assert.strictEqual(result.smiles, 'C1CCC1CC');
53
+ assert.ok(await isValidSMILES(result.smiles));
47
54
  });
48
55
 
49
- test('concat remaps conflicting ring numbers', () => {
56
+ test('concat remaps conflicting ring numbers', async () => {
50
57
  const a = Fragment('C1CCC1');
51
58
  const b = Fragment('C1CCC1');
52
59
  const result = a.concat(b);
53
-
60
+
54
61
  // The second ring should be remapped to ring 2
55
62
  assert.strictEqual(result.smiles, 'C1CCC1C2CCC2');
63
+ assert.ok(await isValidSMILES(result.smiles));
56
64
  });
57
65
 
58
- test('concat handles multiple ring conflicts', () => {
66
+ test('concat handles multiple ring conflicts', async () => {
59
67
  const a = Fragment('C1CCC1C2CCC2');
60
68
  const b = Fragment('C1CCC1C2CCC2');
61
69
  const result = a.concat(b);
62
-
70
+
63
71
  // Rings should be remapped to 3 and 4
64
72
  assert.strictEqual(result.smiles, 'C1CCC1C2CCC2C3CCC3C4CCC4');
73
+ assert.ok(await isValidSMILES(result.smiles));
65
74
  });
66
75
 
67
- test('concat handles aromatic rings', () => {
76
+ test('concat handles aromatic rings', async () => {
68
77
  const benzene1 = Fragment('c1ccccc1');
69
78
  const benzene2 = Fragment('c1ccccc1');
70
79
  const result = benzene1.concat(benzene2);
71
-
80
+
72
81
  assert.strictEqual(result.smiles, 'c1ccccc1c2ccccc2');
82
+ assert.ok(await isValidSMILES(result.smiles));
73
83
  });
74
84
 
75
- test('concat works with complex molecules', () => {
85
+ test('concat works with complex molecules', async () => {
76
86
  const phenyl = Fragment('c1ccccc1');
77
87
  const methyl = Fragment('C');
78
88
  const result = phenyl.concat(methyl);
79
-
89
+
80
90
  assert.strictEqual(result.smiles, 'c1ccccc1C');
91
+ assert.ok(await isValidSMILES(result.smiles));
81
92
  });
82
93
 
83
- test('concat can be used multiple times', () => {
94
+ test('concat can be used multiple times', async () => {
84
95
  const c = Fragment('C');
85
96
  const result = c.concat('C').concat('C').concat('C');
86
-
97
+
87
98
  assert.strictEqual(result.smiles, 'CCCC');
88
99
  assert.strictEqual(result.atoms, 4);
100
+ assert.ok(await isValidSMILES(result.smiles));
89
101
  });
90
102
 
91
- test('Static Fragment.concat works', () => {
103
+ test('Static Fragment.concat works', async () => {
92
104
  const a = Fragment('CC');
93
105
  const b = Fragment('CC');
94
106
  const result = Fragment.concat(a, b);
95
-
107
+
96
108
  assert.strictEqual(result.smiles, 'CCCC');
109
+ assert.ok(await isValidSMILES(result.smiles));
97
110
  });
98
111
 
99
- test('Static Fragment.concat accepts strings', () => {
112
+ test('Static Fragment.concat accepts strings', async () => {
100
113
  const result = Fragment.concat('CC', 'CC');
101
114
  assert.strictEqual(result.smiles, 'CCCC');
115
+ assert.ok(await isValidSMILES(result.smiles));
102
116
  });
103
117
 
104
- test('concat with functional groups', () => {
118
+ test('concat with functional groups', async () => {
105
119
  const ethyl = Fragment('CC');
106
120
  const hydroxyl = Fragment('O');
107
121
  const result = ethyl.concat(hydroxyl);
108
-
122
+
109
123
  assert.strictEqual(result.smiles, 'CCO');
110
124
  assert.strictEqual(result.formula, 'C2H6O');
125
+ assert.ok(await isValidSMILES(result.smiles));
111
126
  });
112
127
 
113
- test('concat preserves original fragments', () => {
128
+ test('concat preserves original fragments', async () => {
114
129
  const a = Fragment('CC');
115
130
  const b = Fragment('CC');
116
131
  const result = a.concat(b);
117
-
132
+
118
133
  // Original fragments should be unchanged
119
134
  assert.strictEqual(a.smiles, 'CC');
120
135
  assert.strictEqual(b.smiles, 'CC');
121
136
  assert.strictEqual(result.smiles, 'CCCC');
137
+ assert.ok(await isValidSMILES(result.smiles));
122
138
  });
123
139
 
124
- test('concat with heterocycles', () => {
140
+ test('concat with heterocycles', async () => {
125
141
  const pyridine = Fragment('n1ccccc1');
126
142
  const methyl = Fragment('C');
127
143
  const result = pyridine.concat(methyl);
128
-
144
+
129
145
  assert.strictEqual(result.smiles, 'n1ccccc1C');
146
+ assert.ok(await isValidSMILES(result.smiles));
130
147
  });
131
148
 
132
- test('concat builds polymer-like chains', () => {
149
+ test('concat builds polymer-like chains', async () => {
133
150
  const ethylene = Fragment('CC');
134
151
  let polymer = ethylene;
135
-
152
+
136
153
  for (let i = 0; i < 4; i++) {
137
154
  polymer = polymer.concat(ethylene);
138
155
  }
139
-
156
+
140
157
  assert.strictEqual(polymer.smiles, 'CCCCCCCCCC'); // 5 ethylene units = 10 carbons
141
158
  assert.strictEqual(polymer.atoms, 10);
159
+ assert.ok(await isValidSMILES(polymer.smiles));
142
160
  });
143
161
 
144
- test('concat with charged species', () => {
145
- const a = Fragment('[NH4+]');
146
- const b = Fragment('[O-]');
162
+ test('concat with charged species', async () => {
163
+ const a = Fragment('[NH3+]');
164
+ const b = Fragment('C');
147
165
  const result = a.concat(b);
148
-
149
- assert.strictEqual(result.smiles, '[NH4+][O-]');
166
+
167
+ assert.strictEqual(result.smiles, '[NH3+]C');
168
+ assert.ok(await isValidSMILES(result.smiles));
150
169
  });
151
170
 
152
- test('concat handles double and triple bonds', () => {
171
+ test('concat handles double and triple bonds', async () => {
153
172
  const ethylene = Fragment('C=C');
154
173
  const methyl = Fragment('C');
155
174
  const result = ethylene.concat(methyl);
156
-
175
+
157
176
  assert.strictEqual(result.smiles, 'C=CC');
177
+ assert.ok(await isValidSMILES(result.smiles));
158
178
  });
159
179
 
160
- test('concat with stereochemistry', () => {
180
+ test('concat with stereochemistry', async () => {
161
181
  const a = Fragment('C[C@H](O)C');
162
182
  const b = Fragment('C');
163
183
  const result = a.concat(b);
164
-
184
+
165
185
  assert.strictEqual(result.smiles, 'C[C@H](O)CC');
186
+ assert.ok(await isValidSMILES(result.smiles));
166
187
  });
167
188
 
168
- test('concat example from requirement: Fragment("CC") + Fragment("CC")', () => {
189
+ test('concat example from requirement: Fragment("CC") + Fragment("CC")', async () => {
169
190
  const a = Fragment('CC');
170
191
  const b = Fragment('CC');
171
192
  const result = a.concat(b);
172
-
193
+
173
194
  assert.strictEqual(result.smiles, 'CCCC');
174
195
  assert.strictEqual(String(result), 'CCCC');
196
+ assert.ok(await isValidSMILES(result.smiles));
175
197
  });
176
198
 
177
- test('concat returns a new Fragment with all methods', () => {
199
+ test('concat returns a new Fragment with all methods', async () => {
178
200
  const a = Fragment('C');
179
201
  const b = Fragment('C');
180
202
  const result = a.concat(b);
181
-
203
+
182
204
  // Should have all Fragment methods
183
205
  assert.strictEqual(typeof result.concat, 'function');
184
206
  assert.strictEqual(typeof result.toString, 'function');
185
207
  assert.strictEqual(typeof result, 'function'); // Can be called as a function for branching
186
-
208
+
187
209
  // Can still use for branching
188
210
  const branched = result(Fragment('O'));
189
211
  assert.strictEqual(branched.smiles, 'CC(O)');
212
+ assert.ok(await isValidSMILES(result.smiles));
213
+ assert.ok(await isValidSMILES(branched.smiles));
190
214
  });
@@ -2,46 +2,55 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { Fragment, Ring } from '../src/index.js';
4
4
  import { benzene, methyl, hydroxyl, carboxyl } from '../src/common.js';
5
+ import { isValidSMILES } from './test-utils.js';
5
6
 
6
- test('README example: toluene', () => {
7
+ test('README example: toluene', async () => {
7
8
  const benzeneRing = Ring('c', 6);
8
9
  const methylGroup = Fragment('C');
9
10
  const toluene = benzeneRing(methylGroup);
11
+ assert.ok(await isValidSMILES(toluene.smiles));
10
12
  assert.strictEqual(String(toluene), 'c1ccccc1(C)');
11
13
  });
12
14
 
13
- test('README example: ethanol', () => {
15
+ test('README example: ethanol', async () => {
14
16
  const ethyl = Fragment('CC');
15
17
  const hydroxylGroup = Fragment('O');
16
18
  const ethanol = ethyl(hydroxylGroup);
19
+ assert.ok(await isValidSMILES(ethanol.smiles));
17
20
  assert.strictEqual(ethanol.smiles, 'CC(O)');
18
21
  });
19
22
 
20
- test('README example: acetone', () => {
23
+ test('README example: acetone', async () => {
21
24
  const methylGroup = Fragment('C');
22
25
  const acetone = methylGroup(Fragment('=O'))(methylGroup);
26
+ assert.ok(await isValidSMILES(acetone.smiles));
23
27
  assert.strictEqual(acetone.smiles, 'C(=O)(C)');
24
28
  });
25
29
 
26
- test('README example: nested branching', () => {
30
+ test('README example: nested branching', async () => {
27
31
  const a = Fragment('C');
28
32
  const b = Fragment('CC');
29
33
  const c = Fragment('CCC');
30
34
  const molecule = a(b(c));
35
+ assert.ok(await isValidSMILES(molecule.smiles));
31
36
  assert.strictEqual(molecule.smiles, 'C(CC(CCC))');
32
37
  });
33
38
 
34
- test('README example: aspirin', () => {
35
- const aspirin = benzene(carboxyl)(Fragment('OC(=O)C'));
36
- assert.strictEqual(aspirin.smiles, 'c1ccccc1(C(=O)O)(OC(=O)C)');
39
+ test('README example: aspirin', async () => {
40
+ const benzeneRing = Ring('c', 6);
41
+ const aspirin = benzeneRing.attachAt(2, carboxyl).attachAt(3, Fragment('OC(=O)C'));
42
+ assert.ok(await isValidSMILES(aspirin.smiles));
43
+ assert.strictEqual(aspirin.smiles, 'c1c(C(=O)O)c(OC(=O)C)ccc1');
37
44
  });
38
45
 
39
- test('README example: ibuprofen', () => {
40
- const ibuprofen = benzene(Fragment('CC(C)C'))(Fragment('CC(C)C(=O)O'));
41
- assert.strictEqual(ibuprofen.smiles, 'c1ccccc1(CC(C)C)(CC(C)C(=O)O)');
46
+ test('README example: ibuprofen', async () => {
47
+ const benzeneRing = Ring('c', 6);
48
+ const ibuprofen = benzeneRing.attachAt(2, Fragment('CC(C)C')).attachAt(5, Fragment('CC(C)C(=O)O'));
49
+ assert.ok(await isValidSMILES(ibuprofen.smiles));
50
+ assert.strictEqual(ibuprofen.smiles, 'c1c(CC(C)C)ccc(CC(C)C(=O)O)c1');
42
51
  });
43
52
 
44
- test('README example: molecule library', () => {
53
+ test('README example: molecule library', async () => {
45
54
  const acetyl = Fragment('C(=O)C');
46
55
  const phenyl = benzene;
47
56
 
@@ -52,6 +61,10 @@ test('README example: molecule library', () => {
52
61
  acetophenone: phenyl(acetyl),
53
62
  };
54
63
 
64
+ assert.ok(await isValidSMILES(molecules.toluene.smiles));
65
+ assert.ok(await isValidSMILES(molecules.phenol.smiles));
66
+ assert.ok(await isValidSMILES(molecules.benzoicAcid.smiles));
67
+ assert.ok(await isValidSMILES(molecules.acetophenone.smiles));
55
68
  assert.strictEqual(String(molecules.toluene), 'c1ccccc1(C)');
56
69
  assert.strictEqual(String(molecules.phenol), 'c1ccccc1(O)');
57
70
  assert.strictEqual(String(molecules.benzoicAcid), 'c1ccccc1(C(=O)O)');
@@ -1,10 +1,12 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { Fragment } from '../src/fragment.js';
4
+ import { isValidSMILES } from './test-utils.js';
4
5
 
5
- test('Fragment creates basic fragments', () => {
6
+ test('Fragment creates basic fragments', async () => {
6
7
  const methyl = Fragment('C');
7
8
  assert.strictEqual(methyl.smiles, 'C');
9
+ assert.ok(await isValidSMILES(methyl.smiles));
8
10
  assert.strictEqual(String(methyl), 'C');
9
11
  });
10
12
 
@@ -20,78 +22,94 @@ test('Fragment.validate returns validation result', () => {
20
22
  assert.deepStrictEqual(result2, { valid: true });
21
23
  });
22
24
 
23
- test('Fragment supports branching', () => {
25
+ test('Fragment supports branching', async () => {
24
26
  const methyl = Fragment('C');
25
27
  const ethyl = Fragment('CC');
26
28
  const result = methyl(ethyl);
27
29
  assert.strictEqual(result.smiles, 'C(CC)');
30
+ assert.ok(await isValidSMILES(result.smiles));
28
31
  });
29
32
 
30
- test('Fragment supports multiple branches', () => {
33
+ test('Fragment supports multiple branches', async () => {
31
34
  const methyl = Fragment('C');
32
35
  const ethyl = Fragment('CC');
33
36
  const result = methyl(ethyl)(ethyl);
34
37
  assert.strictEqual(result.smiles, 'C(CC)(CC)');
38
+ assert.ok(await isValidSMILES(result.smiles));
35
39
  });
36
40
 
37
- test('Fragment supports nested branches', () => {
41
+ test('Fragment supports nested branches', async () => {
38
42
  const a = Fragment('C');
39
43
  const b = Fragment('CC');
40
44
  const c = Fragment('CCC');
41
45
  const result = a(b(c));
42
46
  assert.strictEqual(result.smiles, 'C(CC(CCC))');
47
+ assert.ok(await isValidSMILES(result.smiles));
43
48
  });
44
49
 
45
- test('Fragment counts atoms correctly', () => {
50
+ test('Fragment counts atoms correctly', async () => {
46
51
  const methyl = Fragment('C');
47
52
  assert.strictEqual(methyl.atoms, 1);
53
+ assert.ok(await isValidSMILES(methyl.smiles));
48
54
 
49
55
  const ethyl = Fragment('CC');
50
56
  assert.strictEqual(ethyl.atoms, 2);
57
+ assert.ok(await isValidSMILES(ethyl.smiles));
51
58
 
52
59
  const benzene = Fragment('c1ccccc1');
53
60
  assert.strictEqual(benzene.atoms, 6);
61
+ assert.ok(await isValidSMILES(benzene.smiles));
54
62
  });
55
63
 
56
- test('Fragment counts rings correctly', () => {
64
+ test('Fragment counts rings correctly', async () => {
57
65
  const methyl = Fragment('C');
58
66
  assert.strictEqual(methyl.rings, 0);
67
+ assert.ok(await isValidSMILES(methyl.smiles));
59
68
 
60
69
  const benzene = Fragment('c1ccccc1');
61
70
  assert.strictEqual(benzene.rings, 1);
71
+ assert.ok(await isValidSMILES(benzene.smiles));
62
72
 
63
73
  const naphthalene = Fragment('c1ccc2ccccc2c1');
64
74
  assert.strictEqual(naphthalene.rings, 2);
75
+ assert.ok(await isValidSMILES(naphthalene.smiles));
65
76
  });
66
77
 
67
- test('Fragment calculates formula correctly', () => {
78
+ test('Fragment calculates formula correctly', async () => {
68
79
  const methyl = Fragment('C');
69
80
  assert.strictEqual(methyl.formula, 'CH4');
81
+ assert.ok(await isValidSMILES(methyl.smiles));
70
82
 
71
83
  const ethyl = Fragment('CC');
72
84
  assert.strictEqual(ethyl.formula, 'C2H6');
85
+ assert.ok(await isValidSMILES(ethyl.smiles));
73
86
 
74
87
  const carbonyl = Fragment('C=O');
75
88
  assert.strictEqual(carbonyl.formula, 'CH2O');
89
+ assert.ok(await isValidSMILES(carbonyl.smiles));
76
90
  });
77
91
 
78
- test('Fragment calculates molecular weight correctly', () => {
92
+ test('Fragment calculates molecular weight correctly', async () => {
79
93
  const methyl = Fragment('C');
80
94
  assert.strictEqual(methyl.molecularWeight, 16.04);
95
+ assert.ok(await isValidSMILES(methyl.smiles));
81
96
 
82
97
  const ethyl = Fragment('CC');
83
98
  assert.strictEqual(ethyl.molecularWeight, 30.07);
99
+ assert.ok(await isValidSMILES(ethyl.smiles));
84
100
  });
85
101
 
86
- test('Fragment works with complex molecules', () => {
102
+ test('Fragment works with complex molecules', async () => {
87
103
  const benzene = Fragment('c1ccccc1');
88
104
  const methyl = Fragment('C');
89
105
  const toluene = benzene(methyl);
90
106
  assert.strictEqual(toluene.smiles, 'c1ccccc1(C)');
107
+ assert.ok(await isValidSMILES(toluene.smiles));
91
108
  });
92
109
 
93
- test('Fragment toString and toPrimitive work correctly', () => {
110
+ test('Fragment toString and toPrimitive work correctly', async () => {
94
111
  const methyl = Fragment('C');
95
112
  assert.strictEqual(methyl.toString(), 'C');
113
+ assert.ok(await isValidSMILES(methyl.smiles));
96
114
  assert.strictEqual(`${methyl}`, 'C');
97
115
  });
@@ -1,24 +1,29 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { FusedRings } from '../src/fused-rings.js';
4
+ import { isValidSMILES } from './test-utils.js';
4
5
 
5
- test('FusedRings creates naphthalene', () => {
6
+ test('FusedRings creates naphthalene', async () => {
6
7
  const naphthalene = FusedRings([6, 6], 'c');
8
+ assert.ok(await isValidSMILES(naphthalene.smiles));
7
9
  assert.strictEqual(naphthalene.smiles, 'c1ccc2ccccc2c1');
8
10
  });
9
11
 
10
- test('FusedRings creates indene', () => {
11
- const indene = FusedRings([6, 5], 'c');
12
- assert.strictEqual(indene.smiles, 'c1ccc2cccc2c1');
12
+ test('FusedRings creates fused 6-5 ring system', async () => {
13
+ const fused = FusedRings([6, 5], 'C');
14
+ assert.ok(await isValidSMILES(fused.smiles));
15
+ assert.strictEqual(fused.smiles, 'C1CCC2CCCC2C1');
13
16
  });
14
17
 
15
- test('FusedRings with hetero creates indole-like structure', () => {
16
- const indole = FusedRings([6, 5], 'c', { hetero: { 7: '[nH]' } });
17
- assert.strictEqual(indole.smiles, 'c1ccc2ccc[nH]2c1');
18
+ test('FusedRings with hetero creates indole-like structure', async () => {
19
+ const indole = FusedRings([6, 5], 'c', { hetero: { 4: '[nH]' } });
20
+ assert.ok(await isValidSMILES(indole.smiles));
21
+ assert.strictEqual(indole.smiles, 'c1ccc2[nH]ccc2c1');
18
22
  });
19
23
 
20
- test('FusedRings with hetero creates quinoline', () => {
24
+ test('FusedRings with hetero creates quinoline', async () => {
21
25
  const quinoline = FusedRings([6, 6], 'c', { hetero: { 0: 'n' } });
26
+ assert.ok(await isValidSMILES(quinoline.smiles));
22
27
  assert.strictEqual(quinoline.smiles, 'n1ccc2ccccc2c1');
23
28
  });
24
29
 
@@ -30,12 +35,14 @@ test('FusedRings throws on 3+ rings', () => {
30
35
  assert.throws(() => FusedRings([6, 6, 6], 'c'), { message: 'FusedRings currently only supports 2 rings' });
31
36
  });
32
37
 
33
- test('FusedRings counts atoms correctly', () => {
38
+ test('FusedRings counts atoms correctly', async () => {
34
39
  const naphthalene = FusedRings([6, 6], 'c');
40
+ assert.ok(await isValidSMILES(naphthalene.smiles));
35
41
  assert.strictEqual(naphthalene.atoms, 10);
36
42
  });
37
43
 
38
- test('FusedRings counts rings correctly', () => {
44
+ test('FusedRings counts rings correctly', async () => {
39
45
  const naphthalene = FusedRings([6, 6], 'c');
46
+ assert.ok(await isValidSMILES(naphthalene.smiles));
40
47
  assert.strictEqual(naphthalene.rings, 2);
41
48
  });
@@ -2,29 +2,35 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { Repeat } from '../src/repeat.js';
4
4
  import { Fragment } from '../src/fragment.js';
5
+ import { isValidSMILES } from './test-utils.js';
5
6
 
6
- test('Repeat creates hexane', () => {
7
+ test('Repeat creates hexane', async () => {
7
8
  const hexane = Repeat('C', 6);
9
+ assert.ok(await isValidSMILES(hexane.smiles));
8
10
  assert.strictEqual(hexane.smiles, 'CCCCCC');
9
11
  });
10
12
 
11
- test('Repeat creates PEG-like chain', () => {
13
+ test('Repeat creates PEG-like chain', async () => {
12
14
  const peg = Repeat('CCO', 4);
15
+ assert.ok(await isValidSMILES(peg.smiles));
13
16
  assert.strictEqual(peg.smiles, 'CCOCCOCCOCCO');
14
17
  });
15
18
 
16
- test('Repeat creates polyethylene', () => {
19
+ test('Repeat creates polyethylene', async () => {
17
20
  const polyethylene = Repeat('CC', 100);
21
+ assert.ok(await isValidSMILES(polyethylene.smiles));
18
22
  assert.strictEqual(polyethylene.smiles, 'CC'.repeat(100));
19
23
  });
20
24
 
21
- test('Repeat works with Fragment objects', () => {
25
+ test('Repeat works with Fragment objects', async () => {
22
26
  const methyl = Fragment('C');
23
27
  const chain = Repeat(methyl, 5);
28
+ assert.ok(await isValidSMILES(chain.smiles));
24
29
  assert.strictEqual(chain.smiles, 'CCCCC');
25
30
  });
26
31
 
27
- test('Repeat counts atoms correctly', () => {
32
+ test('Repeat counts atoms correctly', async () => {
28
33
  const hexane = Repeat('C', 6);
34
+ assert.ok(await isValidSMILES(hexane.smiles));
29
35
  assert.strictEqual(hexane.atoms, 6);
30
36
  });
@@ -2,53 +2,73 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { Ring } from '../src/ring.js';
4
4
  import { Fragment } from '../src/fragment.js';
5
+ import { isValidSMILES } from './test-utils.js';
5
6
 
6
- test('Ring composition remaps ring numbers correctly', () => {
7
+ test('Ring composition remaps ring numbers correctly', async () => {
7
8
  const benzene = Ring('c', 6);
8
9
  const biphenyl = benzene(benzene);
10
+ assert.ok(await isValidSMILES(biphenyl.smiles));
9
11
  assert.strictEqual(biphenyl.smiles, 'c1ccccc1(c2ccccc2)');
10
12
  });
11
13
 
12
- test('Multiple ring compositions remap correctly', () => {
14
+ test('Multiple ring compositions remap correctly', async () => {
13
15
  const benzene = Ring('c', 6);
14
- const triphenyl = benzene(benzene)(benzene);
15
- assert.strictEqual(triphenyl.smiles, 'c1ccccc1(c2ccccc2)(c3ccccc3)');
16
+ const triphenyl = benzene.attachAt(2, benzene).attachAt(4, benzene);
17
+ assert.ok(await isValidSMILES(triphenyl.smiles));
18
+ assert.strictEqual(triphenyl.smiles, 'c1c(c2ccccc2)cc(c3ccccc3)cc1');
16
19
  });
17
20
 
18
- test('Different sized rings compose correctly', () => {
21
+ test('Different sized rings compose correctly', async () => {
19
22
  const benzene = Ring('c', 6);
20
23
  const cyclopentane = Ring('C', 5);
21
24
  const result = benzene(cyclopentane);
25
+ assert.ok(await isValidSMILES(result.smiles));
22
26
  assert.strictEqual(result.smiles, 'c1ccccc1(C2CCCC2)');
23
27
  });
24
28
 
25
- test('Ring with existing branches composes correctly', () => {
29
+ test('Ring with existing branches composes correctly', async () => {
26
30
  const benzene = Ring('c', 6);
27
- const methyl = Fragment('C');
28
- const toluene = benzene(methyl);
29
- const result = toluene(benzene);
30
- assert.strictEqual(result.smiles, 'c1ccccc1(C)(c2ccccc2)');
31
+ const result = benzene.attachAt(2, 'C').attachAt(3, benzene);
32
+ assert.ok(await isValidSMILES(result.smiles));
33
+ assert.strictEqual(result.smiles, 'c1c(C)c(c2ccccc2)ccc1');
31
34
  });
32
35
 
33
- test('Nested ring compositions work correctly', () => {
36
+ test('Nested ring compositions work correctly', async () => {
34
37
  const benzene = Ring('c', 6);
35
38
  const cyclohexane = Ring('C', 6);
36
39
  const inner = benzene(cyclohexane);
37
40
  const outer = benzene(inner);
41
+ assert.ok(await isValidSMILES(outer.smiles));
38
42
  assert.strictEqual(outer.smiles, 'c1ccccc1(c3ccccc3(C2CCCCC2))');
39
43
  });
40
44
 
41
- test('Ring composition counts atoms correctly', () => {
45
+ test('Ring composition counts atoms correctly', async () => {
42
46
  const benzene = Ring('c', 6);
43
47
  const biphenyl = benzene(benzene);
48
+ assert.ok(await isValidSMILES(biphenyl.smiles));
44
49
  assert.strictEqual(biphenyl.atoms, 12);
45
50
  });
46
51
 
47
- test('Ring composition counts rings correctly', () => {
52
+ test('Ring composition counts rings correctly', async () => {
48
53
  const benzene = Ring('c', 6);
49
54
  const biphenyl = benzene(benzene);
55
+ assert.ok(await isValidSMILES(biphenyl.smiles));
50
56
  assert.strictEqual(biphenyl.rings, 2);
51
57
 
52
- const triphenyl = benzene(benzene)(benzene);
58
+ const triphenyl = benzene.attachAt(2, benzene).attachAt(4, benzene);
59
+ assert.ok(await isValidSMILES(triphenyl.smiles));
53
60
  assert.strictEqual(triphenyl.rings, 3);
54
61
  });
62
+
63
+ test('Phenol composition works correctly', async () => {
64
+ const benzene = Ring('c', 6);
65
+ const phenol = benzene.attachAt(1, 'O');
66
+ assert.ok(await isValidSMILES(phenol.smiles));
67
+ assert.strictEqual(phenol.smiles, 'c1(O)ccccc1');
68
+ });
69
+
70
+ test('Para-quinone (2,5-cyclohexadiene-1,4-dione) composition works correctly', async () => {
71
+ const result = Ring('c', 6).attachAt(1, '=O').attachAt(4, '=O');
72
+ assert.ok(await isValidSMILES(result.smiles));
73
+ assert.strictEqual(result.smiles, 'c1(=O)ccc(=O)cc1');
74
+ });
package/test/ring.test.js CHANGED
@@ -2,39 +2,45 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import { Ring } from '../src/ring.js';
4
4
  import { Fragment } from '../src/fragment.js';
5
+ import { isValidSMILES } from './test-utils.js';
5
6
 
6
- test('Ring creates benzene', () => {
7
+ test('Ring creates benzene', async () => {
7
8
  const benzene = Ring('c', 6);
8
9
  assert.strictEqual(benzene.smiles, 'c1ccccc1');
10
+ assert.ok(await isValidSMILES(benzene.smiles));
9
11
  });
10
12
 
11
- test('Ring creates cyclohexane', () => {
13
+ test('Ring creates cyclohexane', async () => {
12
14
  const cyclohexane = Ring('C', 6);
13
15
  assert.strictEqual(cyclohexane.smiles, 'C1CCCCC1');
16
+ assert.ok(await isValidSMILES(cyclohexane.smiles));
14
17
  });
15
18
 
16
- test('Ring creates cyclopentane', () => {
19
+ test('Ring creates cyclopentane', async () => {
17
20
  const cyclopentane = Ring('C', 5);
18
21
  assert.strictEqual(cyclopentane.smiles, 'C1CCCC1');
22
+ assert.ok(await isValidSMILES(cyclopentane.smiles));
19
23
  });
20
24
 
21
- test('Ring with replace option creates pyridine', () => {
25
+ test('Ring with replace option creates pyridine', async () => {
22
26
  const pyridine = Ring('c', 6, { replace: { 0: 'n' } });
23
27
  assert.strictEqual(pyridine.smiles, 'n1ccccc1');
28
+ assert.ok(await isValidSMILES(pyridine.smiles));
24
29
  });
25
30
 
26
- test('Ring supports substitution', () => {
31
+ test('Ring supports substitution', async () => {
27
32
  const benzene = Ring('c', 6);
28
33
  const methyl = Fragment('C');
29
34
  const toluene = benzene(methyl);
30
35
  assert.strictEqual(toluene.smiles, 'c1ccccc1(C)');
36
+ assert.ok(await isValidSMILES(toluene.smiles));
31
37
  });
32
38
 
33
- test('Ring supports multiple substitutions', () => {
39
+ test('Ring supports multiple substitutions', async () => {
34
40
  const benzene = Ring('c', 6);
35
- const methyl = Fragment('C');
36
- const xylene = benzene(methyl)(methyl);
37
- assert.strictEqual(xylene.smiles, 'c1ccccc1(C)(C)');
41
+ const xylene = benzene.attachAt(2, 'C').attachAt(5, 'C');
42
+ assert.strictEqual(xylene.smiles, 'c1c(C)ccc(C)c1');
43
+ assert.ok(await isValidSMILES(xylene.smiles));
38
44
  });
39
45
 
40
46
  test('Ring counts atoms correctly', () => {
@@ -49,3 +55,73 @@ test('Ring counts rings correctly', () => {
49
55
  const benzene = Ring('c', 6);
50
56
  assert.strictEqual(benzene.rings, 1);
51
57
  });
58
+
59
+ test('Ring.attachAt creates toluene at position 1', async () => {
60
+ const benzene = Ring('c', 6);
61
+ const toluene = benzene.attachAt(1, 'C');
62
+ assert.strictEqual(toluene.smiles, 'c1(C)ccccc1');
63
+ assert.ok(await isValidSMILES(toluene.smiles));
64
+ });
65
+
66
+ test('Ring.attachAt creates toluene at position 2', async () => {
67
+ const benzene = Ring('c', 6);
68
+ const toluene = benzene.attachAt(2, 'C');
69
+ assert.strictEqual(toluene.smiles, 'c1c(C)cccc1');
70
+ assert.ok(await isValidSMILES(toluene.smiles));
71
+ });
72
+
73
+ test('Ring.attachAt creates para-xylene', async () => {
74
+ const benzene = Ring('c', 6);
75
+ const toluene = benzene.attachAt(2, 'C');
76
+ const xylene = toluene.attachAt(5, 'C');
77
+ assert.strictEqual(xylene.smiles, 'c1c(C)ccc(C)c1');
78
+ assert.ok(await isValidSMILES(xylene.smiles));
79
+ });
80
+
81
+ test('Ring.attachAt creates ibuprofen', async () => {
82
+ const benzene = Ring('c', 6);
83
+ const ibuprofen = benzene.attachAt(2, 'CC(C)C').attachAt(5, 'CC(C)C(=O)O');
84
+ assert.strictEqual(ibuprofen.smiles, 'c1c(CC(C)C)ccc(CC(C)C(=O)O)c1');
85
+ assert.ok(await isValidSMILES(ibuprofen.smiles));
86
+ });
87
+
88
+ test('Ring.attachAt throws on invalid position', () => {
89
+ const benzene = Ring('c', 6);
90
+ assert.throws(() => benzene.attachAt(0, 'C'), /out of range/);
91
+ assert.throws(() => benzene.attachAt(7, 'C'), /out of range/);
92
+ });
93
+
94
+ test('Ring.attachAt works with Fragment substituents', async () => {
95
+ const benzene = Ring('c', 6);
96
+ const methyl = Fragment('C');
97
+ const toluene = benzene.attachAt(2, methyl);
98
+ assert.strictEqual(toluene.smiles, 'c1c(C)cccc1');
99
+ assert.ok(await isValidSMILES(toluene.smiles));
100
+ });
101
+
102
+ test('Ring.attachAt works with Ring substituents', async () => {
103
+ const benzene = Ring('c', 6);
104
+ const biphenyl = benzene.attachAt(2, benzene);
105
+ assert.strictEqual(biphenyl.smiles, 'c1c(c2ccccc2)cccc1');
106
+ assert.ok(await isValidSMILES(biphenyl.smiles));
107
+ });
108
+
109
+ test('Ring.attachAt chains multiple rings', async () => {
110
+ const benzene = Ring('c', 6);
111
+ const terphenyl = benzene.attachAt(2, benzene).attachAt(5, benzene);
112
+ assert.strictEqual(terphenyl.smiles, 'c1c(c2ccccc2)ccc(c3ccccc3)c1');
113
+ assert.ok(await isValidSMILES(terphenyl.smiles));
114
+ });
115
+
116
+ test('Ring.attachAt validates with RDKit', async () => {
117
+ const { isValidSMILES } = await import('./test-utils.js');
118
+ const benzene = Ring('c', 6);
119
+
120
+ // Test simple ring attachment
121
+ const biphenyl = benzene.attachAt(2, benzene);
122
+ assert.ok(await isValidSMILES(biphenyl.smiles));
123
+
124
+ // Test chained ring attachments
125
+ const terphenyl = benzene.attachAt(2, benzene).attachAt(5, benzene);
126
+ assert.ok(await isValidSMILES(terphenyl.smiles));
127
+ });
@@ -0,0 +1,35 @@
1
+ import initRDKitModule from '@rdkit/rdkit';
2
+
3
+ let RDKitModule = null;
4
+
5
+ /**
6
+ * Initialize RDKit module
7
+ */
8
+ async function initRDKit() {
9
+ if (!RDKitModule) {
10
+ RDKitModule = await initRDKitModule();
11
+ }
12
+ return RDKitModule;
13
+ }
14
+
15
+ /**
16
+ * Check if SMILES is valid using RDKit
17
+ * @param {string} smiles - SMILES string
18
+ * @returns {Promise<boolean>}
19
+ */
20
+ export async function isValidSMILES(smiles) {
21
+ const rdkit = await initRDKit();
22
+
23
+ let mol = null;
24
+ try {
25
+ mol = rdkit.get_mol(smiles);
26
+ if (!mol) return false;
27
+ return mol.is_valid();
28
+ } catch {
29
+ return false;
30
+ } finally {
31
+ if (mol) {
32
+ mol.delete();
33
+ }
34
+ }
35
+ }