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.
- package/.github/workflows/ci.yml +3 -0
- package/.github/workflows/publish.yml +3 -0
- package/package.json +5 -2
- package/src/common.js +1 -1
- package/src/ring.js +104 -11
- package/test/common.test.js +36 -7
- package/test/concat.test.js +67 -43
- package/test/examples.test.js +24 -11
- package/test/fragment.test.js +28 -10
- package/test/fused-rings.test.js +17 -10
- package/test/repeat.test.js +11 -5
- package/test/ring-composition.test.js +34 -14
- package/test/ring.test.js +85 -9
- package/test/test-utils.js +35 -0
package/.github/workflows/ci.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smiles-js",
|
|
3
|
-
"version": "0.2.
|
|
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: {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
// Build the ring SMILES
|
|
8
|
+
const buildRingSmiles = (replacements) => {
|
|
9
|
+
const parts = [];
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
}
|
package/test/common.test.js
CHANGED
|
@@ -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.
|
|
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
|
});
|
package/test/concat.test.js
CHANGED
|
@@ -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('[
|
|
146
|
-
const b = Fragment('
|
|
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, '[
|
|
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
|
});
|
package/test/examples.test.js
CHANGED
|
@@ -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
|
|
36
|
-
|
|
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
|
|
41
|
-
|
|
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)');
|
package/test/fragment.test.js
CHANGED
|
@@ -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
|
});
|
package/test/fused-rings.test.js
CHANGED
|
@@ -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
|
|
11
|
-
const
|
|
12
|
-
assert.
|
|
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: {
|
|
17
|
-
assert.
|
|
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
|
});
|
package/test/repeat.test.js
CHANGED
|
@@ -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.
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
assert.
|
|
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
|
+
}
|