$darkmode
DENOPTIM
MoleculeUtilsTest.java
Go to the documentation of this file.
1package denoptim.utils;
2
3/*
4 * DENOPTIM
5 * Copyright (C) 2019 Vishwesh Venkatraman <vishwesh.venkatraman@ntnu.no>
6 * and Marco Foscato <marco.foscato@uib.no>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published
10 * by the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21import static denoptim.utils.MoleculeUtils.getPoint3d;
22import static org.junit.jupiter.api.Assertions.assertEquals;
23import static org.junit.jupiter.api.Assertions.assertFalse;
24import static org.junit.jupiter.api.Assertions.assertNotNull;
25import static org.junit.jupiter.api.Assertions.assertTrue;
26
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.List;
30import java.util.Map;
31import java.util.Set;
32import java.util.logging.Logger;
33
34import javax.vecmath.Point2d;
35import javax.vecmath.Point3d;
36
37import org.junit.jupiter.api.Test;
38import org.openscience.cdk.Atom;
39import org.openscience.cdk.Bond;
40import org.openscience.cdk.interfaces.IAtom;
41import org.openscience.cdk.interfaces.IAtomContainer;
42import org.openscience.cdk.interfaces.IBond;
43import org.openscience.cdk.interfaces.IChemObjectBuilder;
44import org.openscience.cdk.silent.SilentChemObjectBuilder;
45
53{
54 private IChemObjectBuilder builder = SilentChemObjectBuilder.getInstance();
55
56//------------------------------------------------------------------------------
57
58 @Test
59 public void testGetPoint3d() throws Exception
60 {
61 IAtom a = new Atom();
62 assertTrue(areCloseEnough(getPoint3d(a).x,0.0));
63 assertTrue(areCloseEnough(getPoint3d(a).y,0.0));
64 assertTrue(areCloseEnough(getPoint3d(a).z,0.0));
65
66 a.setPoint2d(new Point2d(2.6, -4.2));
67 assertTrue(areCloseEnough(getPoint3d(a).x,2.6));
68 assertTrue(areCloseEnough(getPoint3d(a).y,-4.2));
69 assertTrue(areCloseEnough(getPoint3d(a).z,0.0));
70
71 a.setPoint3d(new Point3d(2.6, -4.2, 6.4));
72 assertTrue(areCloseEnough(getPoint3d(a).x,2.6));
73 assertTrue(areCloseEnough(getPoint3d(a).y,-4.2));
74 assertTrue(areCloseEnough(getPoint3d(a).z,6.4));
75
76 assertFalse(areCloseEnough(1.00001, 1.00002));
77 }
78
79//------------------------------------------------------------------------------
80
81 private boolean areCloseEnough(double a, double b)
82 {
83 double delta = 0.0000001;
84 return Math.abs(a-b) <= delta;
85 }
86
87//------------------------------------------------------------------------------
88
89 @Test
90 public void testCalculateCentroid() throws Exception
91 {
92 IAtomContainer mol = builder.newAtomContainer();
93 mol.addAtom(new Atom("H", new Point3d(1.2,-2.9,2.5)));
94 mol.addAtom(new Atom("H", new Point3d(2.3,-4.8,4.2)));
95 mol.addAtom(new Atom("H", new Point3d(4.5,2.6,-5.4)));
96
97 Point3d c = MoleculeUtils.calculateCentroid(mol);
98 assertTrue(areCloseEnough(2.6666666,c.x), "Wrong value in X");
99 assertTrue(areCloseEnough(-1.7,c.y), "Wrong value in Y");
100 assertTrue(areCloseEnough(0.4333333,c.z), "Wrong value in Z");
101 }
102
103//------------------------------------------------------------------------------
104
105 @Test
106 public void testCalculateBondAngleAgreement() throws Exception
107 {
108 // Create a simple water-like molecule (H-O-H) with known angles
109 IAtomContainer template = builder.newAtomContainer();
110 IAtom o1 = new Atom("O", new Point3d(0.0, 0.0, 0.0));
111 IAtom h1 = new Atom("H", new Point3d(1.0, 0.0, 0.0));
112 IAtom h2 = new Atom("H", new Point3d(0.0, 1.0, 0.0));
113 template.addAtom(o1);
114 template.addAtom(h1);
115 template.addAtom(h2);
116 template.addBond(new Bond(o1, h1));
117 template.addBond(new Bond(o1, h2));
118
119 // Create a molecule with the same structure but slightly different angle
120 IAtomContainer mol = builder.newAtomContainer();
121 IAtom o2 = new Atom("O", new Point3d(0.0, 0.0, 0.0));
122 IAtom h3 = new Atom("H", new Point3d(1.0, 0.0, 0.0));
123 IAtom h4 = new Atom("H", new Point3d(0.1, 0.995, 0.0)); // Slightly different angle
124 mol.addAtom(o2);
125 mol.addAtom(h3);
126 mol.addAtom(h4);
127 mol.addBond(new Bond(o2, h3));
128 mol.addBond(new Bond(o2, h4));
129
130 // Create mapping
131 Map<IAtom, IAtom> mapping = new HashMap<>();
132 mapping.put(o1, o2);
133 mapping.put(h1, h3);
134 mapping.put(h2, h4);
135
136 // Calculate bond angle agreement
137 double[] rawData = MoleculeUtils.calculateBondAngleAgreement(template, mol, mapping);
138 double score = MoleculeUtils.normalizeBondAngleScore(rawData);
139
140 // Score should be small (good agreement) but not zero
141 assertTrue(score < 10.0, "Bond angle agreement score should be small for similar structures");
142 assertTrue(score >= 0.0, "Score should be non-negative");
143
144 // Test with identical structures (should have very low score)
145 IAtomContainer mol2 = builder.newAtomContainer();
146 IAtom o3 = new Atom("O", new Point3d(0.0, 0.0, 0.0));
147 IAtom h5 = new Atom("H", new Point3d(1.0, 0.0, 0.0));
148 IAtom h6 = new Atom("H", new Point3d(0.0, 1.0, 0.0));
149 mol2.addAtom(o3);
150 mol2.addAtom(h5);
151 mol2.addAtom(h6);
152 mol2.addBond(new Bond(o3, h5));
153 mol2.addBond(new Bond(o3, h6));
154
155 Map<IAtom, IAtom> mapping2 = new HashMap<>();
156 mapping2.put(o1, o3);
157 mapping2.put(h1, h5);
158 mapping2.put(h2, h6);
159
160 double[] rawData2 = MoleculeUtils.calculateBondAngleAgreement(template, mol2, mapping2);
161 double score2 = MoleculeUtils.normalizeBondAngleScore(rawData2);
162 assertTrue(score2 < 0.1, "Identical structures should have very low score");
163
164 // Test with incomplete mapping (should return MAX_VALUE)
165 Map<IAtom, IAtom> incompleteMapping = new HashMap<>();
166 incompleteMapping.put(o1, o2);
167 // Missing h1 and h2 mappings
168
169 double[] rawData3 = MoleculeUtils.calculateBondAngleAgreement(template, mol, incompleteMapping);
170 double score3 = MoleculeUtils.normalizeBondAngleScore(rawData3);
171 assertEquals(Double.MAX_VALUE, score3, "Incomplete mapping should return MAX_VALUE, but got " + score3);
172
173 // Test CH4 with different mappings
174 // Create CH4 template with tetrahedral geometry
175 // Carbon at center, 4 hydrogens at tetrahedral positions
176 IAtomContainer ch4Template = builder.newAtomContainer();
177 IAtom cTemplate = new Atom("C", new Point3d(0.0, 0.0, 0.0));
178 // Tetrahedral positions: normalized to unit distance
179 double sqrt3 = Math.sqrt(1.0/3.0);
180 IAtom h1Template = new Atom("H", new Point3d(sqrt3, sqrt3, sqrt3));
181 IAtom h2Template = new Atom("H", new Point3d(sqrt3, -sqrt3, -sqrt3));
182 IAtom h3Template = new Atom("H", new Point3d(-sqrt3, sqrt3, -sqrt3));
183 IAtom h4Template = new Atom("H", new Point3d(-sqrt3, -sqrt3, sqrt3));
184 ch4Template.addAtom(cTemplate);
185 ch4Template.addAtom(h1Template);
186 ch4Template.addAtom(h2Template);
187 ch4Template.addAtom(h3Template);
188 ch4Template.addAtom(h4Template);
189 ch4Template.addBond(new Bond(cTemplate, h1Template));
190 ch4Template.addBond(new Bond(cTemplate, h2Template));
191 ch4Template.addBond(new Bond(cTemplate, h3Template));
192 ch4Template.addBond(new Bond(cTemplate, h4Template));
193
194 // Create CH4 target with same geometry (perfect match)
195 IAtomContainer ch4Target = builder.newAtomContainer();
196 IAtom cTarget = new Atom("C", new Point3d(0.0, 0.0, 0.0));
197 IAtom h1Target = new Atom("H", new Point3d(sqrt3, sqrt3, sqrt3));
198 IAtom h2Target = new Atom("H", new Point3d(sqrt3, -sqrt3, -sqrt3));
199 IAtom h3Target = new Atom("H", new Point3d(-sqrt3, sqrt3, -sqrt3));
200 IAtom h4Target = new Atom("H", new Point3d(-sqrt3, -sqrt3, sqrt3));
201 ch4Target.addAtom(cTarget);
202 ch4Target.addAtom(h1Target);
203 ch4Target.addAtom(h2Target);
204 ch4Target.addAtom(h3Target);
205 ch4Target.addAtom(h4Target);
206 ch4Target.addBond(new Bond(cTarget, h1Target));
207 ch4Target.addBond(new Bond(cTarget, h2Target));
208 ch4Target.addBond(new Bond(cTarget, h3Target));
209 ch4Target.addBond(new Bond(cTarget, h4Target));
210
211 // Test correct mapping (identity mapping)
212 Map<IAtom, IAtom> correctMapping = new HashMap<>();
213 correctMapping.put(cTemplate, cTarget);
214 correctMapping.put(h1Template, h1Target);
215 correctMapping.put(h2Template, h2Target);
216 correctMapping.put(h3Template, h3Target);
217 correctMapping.put(h4Template, h4Target);
218
219 double[] rawDataIdentical = MoleculeUtils.calculateBondAngleAgreement(ch4Template, ch4Target, correctMapping);
220 double identicalMappingScore = MoleculeUtils.normalizeBondAngleScore(rawDataIdentical);
221 assertTrue(identicalMappingScore < 0.1, "Correct mapping should have very low score: " + identicalMappingScore);
222
223 // Test swapped mapping (h1 <-> h2)
224 Map<IAtom, IAtom> swappedMapping = new HashMap<>();
225 swappedMapping.put(cTemplate, cTarget);
226 swappedMapping.put(h1Template, h2Target); // Swapped
227 swappedMapping.put(h2Template, h1Target); // Swapped
228 swappedMapping.put(h3Template, h3Target);
229 swappedMapping.put(h4Template, h4Target);
230
231 double[] rawDataSwapped = MoleculeUtils.calculateBondAngleAgreement(ch4Template, ch4Target, swappedMapping);
232 double swappedScore = MoleculeUtils.normalizeBondAngleScore(rawDataSwapped);
233 // Since all H are equivalent in perfect tetrahedron, score should be similar
234 assertTrue(swappedScore > identicalMappingScore,
235 "Swapped mapping should give higher score than identical mapping (> "
236 + identicalMappingScore + "): " + swappedScore);
237 }
238
239//------------------------------------------------------------------------------
240
241 @Test
242 public void testFindAtomMapping() throws Exception
243 {
244 Logger logger = Logger.getLogger("TestLogger");
245
246 // Create a simple linear molecule: H-C-C-H
247 IAtomContainer substructure = builder.newAtomContainer();
248 IAtom c1 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
249 IAtom c2 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
250 substructure.addAtom(c1);
251 substructure.addAtom(c2);
252 substructure.addBond(new Bond(c1, c2));
253
254 // Create a larger molecule containing the substructure: H-C-C-C-H
255 IAtomContainer mol = builder.newAtomContainer();
256 IAtom h1 = new Atom("H", new Point3d(-1.0, 0.0, 0.0));
257 IAtom c3 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
258 IAtom c4 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
259 IAtom c5 = new Atom("C", new Point3d(3.0, 0.0, 0.0));
260 IAtom h2 = new Atom("H", new Point3d(4.0, 0.0, 0.0));
261 mol.addAtom(h1);
262 mol.addAtom(c3);
263 mol.addAtom(c4);
264 mol.addAtom(c5);
265 mol.addAtom(h2);
266 mol.addBond(new Bond(h1, c3));
267 mol.addBond(new Bond(c3, c4));
268 mol.addBond(new Bond(c4, c5));
269 mol.addBond(new Bond(c5, h2));
270
271 // Find mapping
272 Map<IAtom, IAtom> mapping = MoleculeUtils.findBestAtomMapping(substructure, mol, logger);
273
274 // Should find a mapping
275 assertNotNull(mapping, "Mapping should not be null");
276 assertEquals(2, mapping.size(), "Mapping should contain 2 atoms");
277 assertTrue(mapping.containsKey(c1), "Mapping should contain first carbon");
278 assertTrue(mapping.containsKey(c2), "Mapping should contain second carbon");
279
280 // Verify the mapped atoms are carbons
281 IAtom mappedC1 = mapping.get(c1);
282 IAtom mappedC2 = mapping.get(c2);
283 assertNotNull(mappedC1, "Mapped atom should not be null");
284 assertNotNull(mappedC2, "Mapped atom should not be null");
285 assertEquals("C", mappedC1.getSymbol(), "Mapped atom should be carbon");
286 assertEquals("C", mappedC2.getSymbol(), "Mapped atom should be carbon");
287
288 // Verify they are bonded in the molecule
289 IBond bond = mol.getBond(mappedC1, mappedC2);
290 assertNotNull(bond, "Mapped atoms should be bonded");
291 }
292
293//------------------------------------------------------------------------------
294
295 @Test
296 public void testFindAtomMappingChiral() throws Exception
297 {
298 Logger logger = Logger.getLogger("TestLogger");
299
300 // Create a chiral center: C(H)(F)(Cl)(Cl) - two identical Cl atoms make it non-trivial
301 // This creates a chiral molecule where enantiomeric mappings should be distinguished
302 IAtomContainer chiralTemplate = builder.newAtomContainer();
303 IAtom cTemplate = new Atom("C", new Point3d(0.0, 0.0, 0.0));
304 // Create tetrahedral geometry
305 double sqrt3 = Math.sqrt(1.0/3.0);
306 IAtom hTemplate = new Atom("H", new Point3d(sqrt3, sqrt3, sqrt3));
307 IAtom fTemplate = new Atom("F", new Point3d(sqrt3, -sqrt3, -sqrt3));
308 IAtom cl1Template = new Atom("Cl", new Point3d(-sqrt3, sqrt3, -sqrt3));
309 IAtom cl2Template = new Atom("Cl", new Point3d(-sqrt3, -sqrt3, sqrt3));
310 chiralTemplate.addAtom(cTemplate);
311 chiralTemplate.addAtom(hTemplate);
312 chiralTemplate.addAtom(fTemplate);
313 chiralTemplate.addAtom(cl1Template);
314 chiralTemplate.addAtom(cl2Template);
315 chiralTemplate.addBond(new Bond(cTemplate, hTemplate));
316 chiralTemplate.addBond(new Bond(cTemplate, fTemplate));
317 chiralTemplate.addBond(new Bond(cTemplate, cl1Template));
318 chiralTemplate.addBond(new Bond(cTemplate, cl2Template));
319
320 // Create target molecule with same geometry (same chirality)
321 IAtomContainer chiralTarget = builder.newAtomContainer();
322 IAtom cTarget = new Atom("C", new Point3d(0.0, 0.0, 0.0));
323 IAtom hTarget = new Atom("H", new Point3d(sqrt3, sqrt3, sqrt3));
324 IAtom fTarget = new Atom("F", new Point3d(sqrt3, -sqrt3, -sqrt3));
325 IAtom cl1Target = new Atom("Cl", new Point3d(-sqrt3, sqrt3, -sqrt3));
326 IAtom cl2Target = new Atom("Cl", new Point3d(-sqrt3, -sqrt3, sqrt3));
327 chiralTarget.addAtom(cTarget);
328 chiralTarget.addAtom(hTarget);
329 chiralTarget.addAtom(fTarget);
330 chiralTarget.addAtom(cl1Target);
331 chiralTarget.addAtom(cl2Target);
332 chiralTarget.addBond(new Bond(cTarget, hTarget));
333 chiralTarget.addBond(new Bond(cTarget, fTarget));
334 chiralTarget.addBond(new Bond(cTarget, cl1Target));
335 chiralTarget.addBond(new Bond(cTarget, cl2Target));
336
337 // Find mapping - should prefer the correct enantiomeric mapping
338 Map<IAtom, IAtom> mapping = MoleculeUtils.findBestAtomMapping(chiralTemplate, chiralTarget, logger);
339
340 // Should find a mapping
341 assertNotNull(mapping, "Mapping should not be null");
342 assertEquals(5, mapping.size(), "Mapping should contain all 5 atoms");
343
344 // Verify correct mapping (should map H->H, F->F, and Cl->Cl with correct chirality)
345 assertEquals(hTarget, mapping.get(hTemplate), "H should map to H");
346 assertEquals(fTarget, mapping.get(fTemplate), "F should map to F");
347 // Both Cl atoms should map to Cl atoms
348 assertTrue(mapping.get(cl1Template).getSymbol().equals("Cl"), "cl1Template should map to Cl");
349 assertTrue(mapping.get(cl2Template).getSymbol().equals("Cl"), "cl2Template should map to Cl");
350
351 // Test with enantiomeric target (mirror image by swapping the two Cl positions)
352 IAtomContainer enantiomerTarget = builder.newAtomContainer();
353 IAtom cEnantiomer = new Atom("C", new Point3d(0.0, 0.0, 0.0));
354 IAtom hEnantiomer = new Atom("H", new Point3d(sqrt3, sqrt3, sqrt3));
355 IAtom fEnantiomer = new Atom("F", new Point3d(sqrt3, -sqrt3, -sqrt3));
356 // Swap the two Cl positions to create opposite chirality
357 IAtom cl1Enantiomer = new Atom("Cl", new Point3d(-sqrt3, -sqrt3, sqrt3)); // Swapped position
358 IAtom cl2Enantiomer = new Atom("Cl", new Point3d(-sqrt3, sqrt3, -sqrt3)); // Swapped position
359 enantiomerTarget.addAtom(cEnantiomer);
360 enantiomerTarget.addAtom(hEnantiomer);
361 enantiomerTarget.addAtom(fEnantiomer);
362 enantiomerTarget.addAtom(cl1Enantiomer);
363 enantiomerTarget.addAtom(cl2Enantiomer);
364 enantiomerTarget.addBond(new Bond(cEnantiomer, hEnantiomer));
365 enantiomerTarget.addBond(new Bond(cEnantiomer, fEnantiomer));
366 enantiomerTarget.addBond(new Bond(cEnantiomer, cl1Enantiomer));
367 enantiomerTarget.addBond(new Bond(cEnantiomer, cl2Enantiomer));
368
369 // Find mapping for enantiomer
370 Map<IAtom, IAtom> enantiomerMapping = MoleculeUtils.findBestAtomMapping(
371 chiralTemplate, enantiomerTarget, logger);
372
373 // Should still find a mapping (isomorphism exists)
374 assertNotNull(enantiomerMapping, "Enantiomer mapping should not be null");
375 assertEquals(5, enantiomerMapping.size(), "Enantiomer mapping should contain all 5 atoms");
376
377 // Verify that the mapping swaps the Cl atoms (chirality-sensitive mapping)
378 assertEquals(hEnantiomer, enantiomerMapping.get(hTemplate), "H should map to H");
379 assertEquals(fEnantiomer, enantiomerMapping.get(fTemplate), "F should map to F");
380 // The Cl atoms should be swapped in the mapping
381 assertEquals(cl1Enantiomer, enantiomerMapping.get(cl2Template),
382 "cl2Template should map to cl1Enantiomer (swapped due to opposite chirality)");
383 assertEquals(cl2Enantiomer, enantiomerMapping.get(cl1Template),
384 "cl1Template should map to cl2Enantiomer (swapped due to opposite chirality)");
385
386 // Calculate scores for both mappings to verify chirality sensitivity
387 Map<IAtom, IAtom> correctMapping = new HashMap<>();
388 correctMapping.put(cTemplate, cTarget);
389 correctMapping.put(hTemplate, hTarget);
390 correctMapping.put(fTemplate, fTarget);
391 correctMapping.put(cl1Template, cl1Target);
392 correctMapping.put(cl2Template, cl2Target);
393
394 // Mapping with swapped Cl (wrong chirality)
395 Map<IAtom, IAtom> swappedClMapping = new HashMap<>();
396 swappedClMapping.put(cTemplate, cTarget);
397 swappedClMapping.put(hTemplate, hTarget);
398 swappedClMapping.put(fTemplate, fTarget);
399 swappedClMapping.put(cl1Template, cl2Target); // Swapped
400 swappedClMapping.put(cl2Template, cl1Target); // Swapped
401
402 // Enantiomer mapping (should be similar to swapped mapping)
403 Map<IAtom, IAtom> enantiomerMappingForScore = new HashMap<>();
404 enantiomerMappingForScore.put(cTemplate, cEnantiomer);
405 enantiomerMappingForScore.put(hTemplate, hEnantiomer);
406 enantiomerMappingForScore.put(fTemplate, fEnantiomer);
407 enantiomerMappingForScore.put(cl1Template, cl1Enantiomer);
408 enantiomerMappingForScore.put(cl2Template, cl2Enantiomer);
409
410 double[] rawDataCorrect = MoleculeUtils.calculateBondAngleAgreement(
411 chiralTemplate, chiralTarget, correctMapping);
412 double correctScore = MoleculeUtils.normalizeBondAngleScore(rawDataCorrect);
413 double[] rawDataSwappedCl = MoleculeUtils.calculateBondAngleAgreement(
414 chiralTemplate, chiralTarget, swappedClMapping);
415 double swappedClScore = MoleculeUtils.normalizeBondAngleScore(rawDataSwappedCl);
416 double[] rawDataEnantiomer = MoleculeUtils.calculateBondAngleAgreement(
417 chiralTemplate, enantiomerTarget, enantiomerMappingForScore);
418 double enantiomerScore = MoleculeUtils.normalizeBondAngleScore(rawDataEnantiomer);
419
420 // The swapped Cl mapping should have a higher score due to opposite chirality
421 assertTrue(swappedClScore > correctScore,
422 "Swapped Cl mapping should have higher score due to opposite chirality. " +
423 "Correct: " + correctScore + ", Swapped: " + swappedClScore);
424
425 // The enantiomer should also have a higher score
426 assertTrue(enantiomerScore > correctScore,
427 "Enantiomer mapping should have higher score due to opposite chirality. " +
428 "Correct: " + correctScore + ", Enantiomer: " + enantiomerScore);
429 }
430
431//------------------------------------------------------------------------------
432
433 @Test
434 public void testFindAtomMappingNoMatch() throws Exception
435 {
436 Logger logger = Logger.getLogger("TestLogger");
437
438 // Create a substructure with nitrogen
439 IAtomContainer substructure = builder.newAtomContainer();
440 IAtom n1 = new Atom("N", new Point3d(0.0, 0.0, 0.0));
441 IAtom n2 = new Atom("N", new Point3d(1.5, 0.0, 0.0));
442 substructure.addAtom(n1);
443 substructure.addAtom(n2);
444 substructure.addBond(new Bond(n1, n2));
445
446 // Create a molecule with only carbons
447 IAtomContainer mol = builder.newAtomContainer();
448 IAtom c1 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
449 IAtom c2 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
450 mol.addAtom(c1);
451 mol.addAtom(c2);
452 mol.addBond(new Bond(c1, c2));
453
454 // Find mapping - should return empty map
455 Map<IAtom, IAtom> mapping = MoleculeUtils.findBestAtomMapping(substructure, mol, logger);
456
457 assertNotNull(mapping, "Mapping should not be null");
458 assertTrue(mapping.isEmpty(), "Mapping should be empty when no match found");
459 }
460
461//------------------------------------------------------------------------------
462
463 @Test
464 public void testFindAtomMappingWithMultipleMatches() throws Exception
465 {
466 Logger logger = Logger.getLogger("TestLogger");
467
468 // Create a simple substructure: C-C
469 IAtomContainer substructure = builder.newAtomContainer();
470 IAtom c1 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
471 IAtom c2 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
472 substructure.addAtom(c1);
473 substructure.addAtom(c2);
474 substructure.addBond(new Bond(c1, c2));
475
476 // Create a molecule with multiple C-C bonds: C-C-C-C
477 // Arrange atoms to have different angles so we can test angle-based selection
478 IAtomContainer mol = builder.newAtomContainer();
479 IAtom c3 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
480 IAtom c4 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
481 IAtom c5 = new Atom("C", new Point3d(3.0, 0.0, 0.0));
482 IAtom c6 = new Atom("C", new Point3d(4.5, 0.0, 0.0));
483 mol.addAtom(c3);
484 mol.addAtom(c4);
485 mol.addAtom(c5);
486 mol.addAtom(c6);
487 mol.addBond(new Bond(c3, c4));
488 mol.addBond(new Bond(c4, c5));
489 mol.addBond(new Bond(c5, c6));
490
491 // Find mapping - should select one of the possible matches
492 Map<IAtom, IAtom> mapping = MoleculeUtils.findBestAtomMapping(substructure, mol, logger);
493
494 assertNotNull(mapping, "Mapping should not be null");
495 assertEquals(2, mapping.size(), "Mapping should contain 2 atoms");
496 assertTrue(mapping.containsKey(c1), "Mapping should contain first carbon");
497 assertTrue(mapping.containsKey(c2), "Mapping should contain second carbon");
498
499 // Verify the mapped atoms are bonded
500 IAtom mappedC1 = mapping.get(c1);
501 IAtom mappedC2 = mapping.get(c2);
502 IBond bond = mol.getBond(mappedC1, mappedC2);
503 assertNotNull(bond, "Mapped atoms should be bonded");
504 }
505
506//------------------------------------------------------------------------------
507
508 @Test
510 {
511 Logger logger = Logger.getLogger("TestLogger");
512
513 // Create a simple substructure: C-C (carbon-carbon bond)
514 IAtomContainer substructure = builder.newAtomContainer();
515 IAtom c1 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
516 IAtom c2 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
517 substructure.addAtom(c1);
518 substructure.addAtom(c2);
519 substructure.addBond(new Bond(c1, c2));
520
521 // Create a larger molecule with multiple C-C bonds: C-C-C-C
522 // This molecule has 3 distinct C-C bonds that match the substructure:
523 // 1. c3-c4 (atoms 0-1)
524 // 2. c4-c5 (atoms 1-2)
525 // 3. c5-c6 (atoms 2-3)
526 IAtomContainer mol = builder.newAtomContainer();
527 IAtom c3 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
528 IAtom c4 = new Atom("C", new Point3d(1.5, 0.0, 0.0));
529 IAtom c5 = new Atom("C", new Point3d(3.0, 0.0, 0.0));
530 IAtom c6 = new Atom("C", new Point3d(4.5, 0.0, 0.0));
531 mol.addAtom(c3);
532 mol.addAtom(c4);
533 mol.addAtom(c5);
534 mol.addAtom(c6);
535 mol.addBond(new Bond(c3, c4));
536 mol.addBond(new Bond(c4, c5));
537 mol.addBond(new Bond(c5, c6));
538
539 // Find all unique atom mappings
540 List<Map<IAtom, IAtom>> mappings = MoleculeUtils.findUniqueAtomMappings(
541 substructure, mol, logger);
542
543 // Should find multiple mappings (at least 3, one for each C-C bond)
544 assertNotNull(mappings, "Mappings list should not be null");
545 assertTrue(mappings.size() > 1,
546 "Should find more than one mapping when substructure appears multiple times. " +
547 "Found: " + mappings.size());
548
549 // Verify each mapping contains 2 atoms (the C-C pair)
550 for (Map<IAtom, IAtom> mapping : mappings)
551 {
552 assertEquals(2, mapping.size(),
553 "Each mapping should contain exactly 2 atoms");
554 assertTrue(mapping.containsKey(c1),
555 "Each mapping should contain the first carbon from substructure");
556 assertTrue(mapping.containsKey(c2),
557 "Each mapping should contain the second carbon from substructure");
558
559 // Verify the mapped atoms are carbons and are bonded
560 IAtom mappedC1 = mapping.get(c1);
561 IAtom mappedC2 = mapping.get(c2);
562 assertNotNull(mappedC1, "Mapped atom should not be null");
563 assertNotNull(mappedC2, "Mapped atom should not be null");
564 assertEquals("C", mappedC1.getSymbol(), "Mapped atom should be carbon");
565 assertEquals("C", mappedC2.getSymbol(), "Mapped atom should be carbon");
566
567 IBond bond = mol.getBond(mappedC1, mappedC2);
568 assertNotNull(bond, "Mapped atoms should be bonded in the target molecule");
569 }
570
571 // Verify that we have distinct mappings (different atom pairs)
572 // Count unique pairs of mapped atoms
573 Set<String> uniquePairs = new HashSet<>();
574 for (Map<IAtom, IAtom> mapping : mappings)
575 {
576 IAtom mappedC1 = mapping.get(c1);
577 IAtom mappedC2 = mapping.get(c2);
578 // Create a unique identifier for the pair (sorted to handle both directions)
579 String pairId = mappedC1.getIndex() < mappedC2.getIndex()
580 ? mappedC1.getIndex() + "-" + mappedC2.getIndex()
581 : mappedC2.getIndex() + "-" + mappedC1.getIndex();
582 uniquePairs.add(pairId);
583 }
584
585 assertTrue(uniquePairs.size() > 1,
586 "Should have multiple distinct atom pair mappings. " +
587 "Found " + uniquePairs.size() + " unique pairs");
588 }
589
590//------------------------------------------------------------------------------
591
592 @Test
593 public void testFindShortestPath() throws Exception
594 {
595 IChemObjectBuilder builder = SilentChemObjectBuilder.getInstance();
596
597 // Create a simple chain molecule: C-C-C-C-C
598 IAtomContainer mol = builder.newAtomContainer();
599 IAtom c1 = new Atom("C", new Point3d(0.0, 0.0, 0.0));
600 IAtom c2 = new Atom("C", new Point3d(1.0, 0.0, 0.0));
601 IAtom c3 = new Atom("C", new Point3d(2.0, 0.0, 0.0));
602 IAtom c4 = new Atom("C", new Point3d(3.0, 0.0, 0.0));
603 IAtom c5 = new Atom("C", new Point3d(4.0, 0.0, 0.0));
604 mol.addAtom(c1);
605 mol.addAtom(c2);
606 mol.addAtom(c3);
607 mol.addAtom(c4);
608 mol.addAtom(c5);
609 mol.addBond(new Bond(c1, c2));
610 mol.addBond(new Bond(c2, c3));
611 mol.addBond(new Bond(c3, c4));
612 mol.addBond(new Bond(c4, c5));
613
614 // Create vertex ID map (all same vertex ID for simple path)
615 Map<IAtom, Long> atomToVertexId = new HashMap<>();
616 atomToVertexId.put(c1, 1L);
617 atomToVertexId.put(c2, 1L);
618 atomToVertexId.put(c3, 1L);
619 atomToVertexId.put(c4, 1L);
620 atomToVertexId.put(c5, 1L);
621
622 // Test path from c1 to c5 (should be c1-c2-c3-c4-c5)
623 List<IAtom> path = MoleculeUtils.findShortestPath(mol, c1, c5, atomToVertexId);
624 assertNotNull(path, "Path should not be null");
625 assertEquals(5, path.size(), "Path should contain 5 atoms");
626 assertEquals(c1, path.get(0), "First atom should be c1");
627 assertEquals(c2, path.get(1), "Second atom should be c2");
628 assertEquals(c3, path.get(2), "Third atom should be c3");
629 assertEquals(c4, path.get(3), "Fourth atom should be c4");
630 assertEquals(c5, path.get(4), "Fifth atom should be c5");
631
632 // Test path from c2 to c4 (should be c2-c3-c4)
633 List<IAtom> path2 = MoleculeUtils.findShortestPath(mol, c2, c4, atomToVertexId);
634 assertNotNull(path2, "Path should not be null");
635 assertEquals(3, path2.size(), "Path should contain 3 atoms");
636 assertEquals(c2, path2.get(0), "First atom should be c2");
637 assertEquals(c3, path2.get(1), "Second atom should be c3");
638 assertEquals(c4, path2.get(2), "Third atom should be c4");
639
640 // Test path from atom to itself (should return empty list)
641 List<IAtom> path3 = MoleculeUtils.findShortestPath(mol, c3, c3, atomToVertexId);
642 assertNotNull(path3, "Path should not be null");
643 assertTrue(path3.isEmpty(), "Path from atom to itself should be empty");
644
645 // Test with different vertex IDs (should still find path)
646 Map<IAtom, Long> atomToVertexId2 = new HashMap<>();
647 atomToVertexId2.put(c1, 1L);
648 atomToVertexId2.put(c2, 1L);
649 atomToVertexId2.put(c3, 2L); // Different vertex ID
650 atomToVertexId2.put(c4, 2L);
651 atomToVertexId2.put(c5, 2L);
652
653 // Path from c1 to c5 should still work (boundary transition allowed)
654 List<IAtom> path4 = MoleculeUtils.findShortestPath(mol, c1, c5, atomToVertexId2);
655 assertNotNull(path4, "Path should not be null");
656 assertEquals(5, path4.size(), "Path should contain 5 atoms even with different vertex IDs");
657 }
658
659//------------------------------------------------------------------------------
660
661}
Utilities for molecule conversion.
static double[] calculateBondAngleAgreement(IAtomContainer templateMol, IAtomContainer mol, Map< IAtom, IAtom > templateToMolMap)
Calculates bond angle agreement and returns raw data.
static double normalizeBondAngleScore(double[] rawData)
Normalizes the raw bond angle agreement data to a single score.
static Map< IAtom, IAtom > findBestAtomMapping(IAtomContainer substructure, IAtomContainer mol, Logger logger)
Finds the maximum common substructure (MCS) between two molecules.
static List< Map< IAtom, IAtom > > findUniqueAtomMappings(IAtomContainer substructure, IAtomContainer mol, Logger logger)
Finds the maximum common substructure (MCS) between two molecules.
static Point3d calculateCentroid(IAtomContainer mol)
Calculated the centroid of the given molecule.
static List< IAtom > findShortestPath(IAtomContainer mol, IAtom start, IAtom end, Map< IAtom, Long > atomToVertexId)
Finds the shortest path between two atoms in a molecule using BFS.
Unit test for DENOPTIMMoleculeUtils.
boolean areCloseEnough(double a, double b)