1 package org.jmol.symmetry; 2 3 import javajs.util.BS; 4 import javajs.util.Lst; 5 import javajs.util.Measure; 6 import javajs.util.P3; 7 import javajs.util.P4; 8 import javajs.util.V3; 9 10 import org.jmol.symmetry.CIPChirality.CIPAtom; 11 import org.jmol.util.BSUtil; 12 import org.jmol.util.Logger; 13 import org.jmol.util.SimpleEdge; 14 import org.jmol.util.SimpleNode; 15 import org.jmol.viewer.JC; 16 import org.jmol.viewer.Viewer; 17 18 /** 19 * A helper class to handle application-specific analysis and store 20 * information needed by CIPChirality and CIPDataSmiles. 21 * 22 * Subclassed as CIPDataSmiles to also allow Jmol's 23 * 24 * "...SMILES...".find("SMILES","chirality") 25 * 26 */ 27 public class CIPData { 28 29 /** 30 * measure of planarity in a trigonal system, in Angstroms 31 * 32 */ 33 static final float TRIGONALITY_MIN = 0.2f; 34 35 /** 36 * Subclass identifier 37 * 38 * @return true for CIPDataTracker 39 */ isTracker()40 protected boolean isTracker() { 41 return false; 42 } 43 44 /** 45 * 46 * Subclass identifier 47 * 48 * @return true for CIPDataSmiles 49 */ isSmiles()50 boolean isSmiles() { 51 return false; 52 } 53 54 /** 55 * A flag that, if set, returns rr for bicyclo[2.2.2]octane 56 */ 57 public boolean testRule6Full; 58 59 /** 60 * Jmol's viewer class 61 */ 62 Viewer vwr; 63 64 /** 65 * all application atoms referenced by bit sets 66 */ 67 SimpleNode[] atoms; 68 69 /** 70 * bit set of all atoms to process 71 */ 72 BS bsAtoms; 73 74 /** 75 * atoms in all molecules containing the atoms of interest 76 */ 77 BS bsMolecule; 78 79 /** 80 * Jmol's definition of aromatic 81 * 82 */ 83 BS bsAromatic; 84 85 /** 86 * 87 * excluded aromatics 88 * 89 * [r5d3n+0,r5d2o+0] 90 */ 91 BS bsXAromatic = new BS(); 92 93 /** 94 * [a-] 95 */ 96 BS bsNegativeAromatic = new BS(); 97 98 /** 99 * all N atoms that are sp3-hybridized and at small ring fusions 100 */ 101 BS bsAzacyclic; 102 103 /** 104 * bit set of all biphenyl-like connections 105 * 106 * "[!H](.t1:-20,20)a{a(.t2:-20,20)-a}a[!H]" 107 * 108 */ 109 BS bsAtropisomeric = new BS(); 110 111 /** 112 * aromatic atoms at the end of a negative helical turn 113 * 114 * "A{a}(.t:-10,-40)a(.t:-10,-40)aaa" 115 * 116 */ 117 BS bsHelixM; 118 119 /** 120 * aromatic atoms at the end of a positive helical turn 121 * 122 * "A{a}(.t:10,40)a(.t:10,40)aaa" 123 * 124 */ 125 BS bsHelixP; 126 127 /** 128 * all 3- to 7-membered rings; used to exclude E/Z and N-SP3 descriptors 129 * 130 */ 131 BS[] lstSmallRings; 132 133 /** 134 * atoms that need specially-calculated element number in Rule 1a 135 * 136 */ 137 BS bsKekuleAmbiguous = new BS(); 138 139 /** 140 * all sp2-hybridized atoms that are not Kekule-ambiguous 141 */ 142 BS bsEnes = new BS(); 143 CIPData()144 public CIPData() { 145 // for reflection 146 } 147 148 /** 149 * Actual constructor. 150 * 151 * @param vwr Jmol viewer 152 * @param bsAtoms selected atoms 153 * @return this 154 */ set(Viewer vwr, BS bsAtoms)155 public CIPData set(Viewer vwr, BS bsAtoms) { 156 this.vwr = vwr; 157 this.atoms = vwr.ms.at; 158 this.bsAtoms = bsAtoms; 159 bsMolecule = vwr.ms.getMoleculeBitSet(bsAtoms); 160 init(); 161 return this; 162 } 163 164 /** 165 * initializer -- called also by CIPDataSmiles 166 * 167 */ init()168 protected void init() { 169 try { 170 // four ortho groups required: bsAtropisomer = match("[!H](.t3:-20,20)a1(.t3).[!H](.t1:-20,20)a(.t1)a1(.t1)(.t2:-20,20)(.t3)(.t4:-20,20)-{a}2(.t1)(.t2)(.t3)(.t4)a(.t2)[!H](.t2).a2(.t4)[!H](.t4)", bsAtoms); 171 // three ortho groups required: bsAtropisomer = match("[!H](.t3:-20,20)a1(.t3).[!H](.t1:-20,20)a(.t1){a}1(.t1)(.t2:-20,20)(.t3)-{a}(.t1)(.t2)(.t3)a(.t2)[!H](.t2)", bsAtoms); 172 // one ortho group on each ring required: 173 BS lstRing = match("[r]"); 174 if (lstRing.isEmpty()) { 175 lstSmallRings = new BS[0]; 176 } else { 177 lstSmallRings = getList("*1**1||*1***1||*1****1||*1*****1||*1******1"); 178 } 179 bsAromatic = match("a"); 180 if (!bsAromatic.isEmpty()) { 181 bsAtropisomeric = match("[!H](.t1:-20,20)a{a(.t2:-20,20)-a}a[!H]"); 182 bsHelixM = match("A{a}(.t:-10,-40)a(.t:-10,-40)aaa"); 183 bsHelixP = match("A{a}(.t:10,40)a(.t:10,40)aaa"); 184 bsXAromatic = match("[r5v3n+0,r5v2o+0]"); 185 bsNegativeAromatic = match("[a-]"); 186 187 188 if (!match("[n+1,o+1]").isEmpty() && !bsXAromatic.isEmpty()) { 189 // look for key 5-member ring aromatics. 190 bsKekuleAmbiguous.or(match("a1[n+,o+]a[n,o]a1")); 191 bsKekuleAmbiguous.or(match("a1[n+,o+][n,o]aa1")); 192 } 193 if (!bsNegativeAromatic.isEmpty()) 194 bsKekuleAmbiguous.or(match("a1=a[a-]a=a1")); 195 196 // pick up five-membered rings with one hetero? 197 //bsKekuleAmbiguous.or(match("a1=a[av3,av2]a=a1")); 198 199 // note that Jmol SMILES does the desired check here -- not including caffeine-like OPEN SMILES rings 200 BS[] lstR6a = getList("a1aaaaa1"); 201 for (int i = lstR6a.length; --i >= 0;) { 202 bsKekuleAmbiguous.or(lstR6a[i]); 203 } 204 } 205 getAzacyclic(); 206 } catch (Exception e) { 207 // ignore 208 } 209 } 210 211 /** 212 * Retrieve an array of bit sets that match a given SMARTS 213 * 214 * @param smarts 215 * @return array of matching bit sets 216 * @throws Exception 217 */ getList(String smarts)218 protected BS[] getList(String smarts) throws Exception { 219 int level = Logger.getLogLevel(); 220 Logger.setLogLevel(Math.min(level, Logger.LEVEL_INFO)); 221 BS[] list = vwr.getSubstructureSetArray(smarts, bsMolecule, JC.SMILES_TYPE_SMARTS); 222 Logger.setLogLevel(level); 223 return list; 224 } 225 226 /** 227 * Return a bit set corresponding to a SMARTS 228 * @param smarts 229 * @return bit set matching this SMARTS 230 * @throws Exception 231 */ match(String smarts)232 protected BS match(String smarts) throws Exception { 233 int level = Logger.getLogLevel(); 234 Logger.setLogLevel(Math.min(level, Logger.LEVEL_INFO)); 235 BS bs = vwr.getSmartsMatch(smarts, bsMolecule); 236 Logger.setLogLevel(level); 237 return bs; 238 } 239 240 /** 241 * Look for conjugated loops of any size that have atoms not already in aromatic rings 242 * 243 */ getEneKekule()244 void getEneKekule() { 245 if (bsEnes.cardinality() < 8) 246 return; 247 248 // check for large ENE loops 249 // need at least five alkenes to trigger this. 250 251 BS bsAllEnes = (BS) bsEnes.clone(); 252 BS bsPath = new BS(); 253 bsEnes.andNot(bsKekuleAmbiguous); 254 BS bsEneAtom1 = new BS(); 255 for (int i = bsEnes.nextSetBit(0); i >= 0; i = bsEnes.nextSetBit(i + 1)) { 256 bsPath.clearAll(); 257 bsEneAtom1.clearAll(); 258 checkEne(bsAllEnes, bsPath, -1, i, 2, bsEneAtom1); 259 } 260 } 261 262 /** 263 * Look for a path that contains this ene in a fully conjugated pattern 264 * 265 * @param bsAllEnes all ene carbons 266 * @param bsPath current path to loop into 267 * @param iLast the last atom 268 * @param iAtom this atom 269 * @param order expected next order, alternating 2,1,2,1,... 270 * @param bsEneAtom1 alternating atoms; first of double bond 271 * @return the atom number of the loop or -1 if failed 272 */ checkEne(BS bsAllEnes, BS bsPath, int iLast, int iAtom, int order, BS bsEneAtom1)273 private int checkEne(BS bsAllEnes, BS bsPath, int iLast, int iAtom, int order, BS bsEneAtom1) { 274 if (bsPath.get(iAtom)) 275 return (bsEneAtom1.get(iAtom) == (order == 2) ? iAtom : -1); 276 bsPath.set(iAtom); 277 SimpleNode a = atoms[iAtom]; 278 int isLoop = -1; 279 SimpleEdge[] edges = a.getEdges(); 280 if (order == 2) 281 bsEneAtom1.set(iAtom); 282 for (int ib = a.getBondCount(); --ib >= 0;) { 283 if (getBondOrder(edges[ib]) != order) 284 continue; 285 SimpleNode b = edges[ib].getOtherNode(a); 286 int iNext = b.getIndex(); 287 if (iNext != iLast && bsAllEnes.get(iNext) 288 && (isLoop = checkEne(bsAllEnes, bsPath, iAtom, iNext, 3 - order, bsEneAtom1)) >= 0) { 289 } 290 } 291 if (isLoop >= 0) { 292 bsKekuleAmbiguous.set(iAtom); 293 bsEnes.clear(iAtom); 294 } 295 return isLoop == iAtom ? -1 : isLoop; 296 } 297 298 /** 299 * Identify bridgehead nitrogens, as these may need to be given chirality 300 * designations. See AY-236.203 P-93.5.4.1 301 * 302 * Sets a bit set of bridgehead nitrogens 303 */ getAzacyclic()304 private void getAzacyclic() { 305 out: for (int i = bsAtoms.nextSetBit(0); i >= 0; i = bsAtoms.nextSetBit(i + 1)) { 306 SimpleNode atom = atoms[i]; 307 if (atom.getElementNumber() != 7 || atom.getCovalentBondCount() != 3 308 || bsKekuleAmbiguous.get(i)) 309 continue; 310 // bridgehead N must be in two rings that have at least three atoms in common. 311 // or in a three-membered ring? 312 313 // don't include N with H attached. Barrier is too low. 314 // added 9/29/2018 BH 315 316 SimpleEdge[] edges = atom.getEdges(); 317 for (int k = edges.length; --k >= 0;) 318 if (edges[k].getOtherNode(atom).getElementNumber() == 1) 319 continue out; 320 321 Lst<BS> nRings = new Lst<BS>(); 322 for (int j = lstSmallRings.length; --j >= 0;) { 323 BS bsRing = lstSmallRings[j]; 324 if (!bsRing.get(i)) 325 continue; 326 nRings.addLast(bsRing); 327 if (j == 0) { 328 addAzacyclicN(i); 329 continue out; 330 } 331 } 332 int nr = nRings.size(); 333 if (nr < 2) 334 continue; 335 BS bsSubs = new BS(); 336 SimpleEdge[] bonds = atom.getEdges(); 337 for (int b = bonds.length; --b >= 0;) 338 if (bonds[b].isCovalent()) 339 bsSubs.set(bonds[b].getOtherNode(atom).getIndex()); 340 BS bsBoth = new BS(); 341 BS bsAll = new BS(); 342 for (int j = 0; j < nr - 1; j++) { 343 BS bs1 = nRings.get(j); 344 for (int k = j + 1; k < nr; k++) { 345 BS bs2 = nRings.get(k); 346 BSUtil.copy2(bs1, bsBoth); 347 bsBoth.and(bs2); 348 if (bsBoth.cardinality() > 2) { 349 BSUtil.copy2(bs1, bsAll); 350 bsAll.or(bs2); 351 bsAll.and(bsSubs); 352 if (bsAll.cardinality() == 3) { 353 addAzacyclicN(i); 354 continue out; 355 } 356 } 357 } 358 } 359 } 360 } 361 addAzacyclicN(int i)362 private void addAzacyclicN(int i) { 363 if (bsAzacyclic == null) 364 bsAzacyclic = new BS(); 365 bsAzacyclic.set(i); 366 } 367 368 /** 369 * Determine whether an atom is one we need to consider. 370 * 371 * @param a 372 * @return true for selected atoms and hybridizations 373 * 374 */ couldBeChiralAtom(SimpleNode a)375 boolean couldBeChiralAtom(SimpleNode a) { 376 boolean mustBePlanar = false; 377 switch (a.getCovalentBondCount()) { 378 default: 379 System.out.println("?? too many bonds! " + a); 380 return false; 381 case 0: 382 return false; 383 case 1: 384 return false; 385 case 2: 386 return a.getElementNumber() == 7; // could be diazine or imine 387 case 3: 388 switch (a.getElementNumber()) { 389 case 7: // N 390 if (bsAzacyclic != null && bsAzacyclic.get(a.getIndex())) 391 break; 392 return false; 393 case 6: // C 394 mustBePlanar = true; 395 break; 396 case 15: // P 397 case 16: // S 398 case 33: // As 399 case 34: // Se 400 case 51: // Sb 401 case 52: // Te 402 case 83: // Bi 403 case 84: // Po 404 break; 405 case 4: 406 break; 407 default: 408 return false; 409 } 410 break; 411 case 4: 412 break; 413 } 414 // check that the atom has at most one 1H atom and whether it must be planar and has a double bond 415 SimpleEdge[] edges = a.getEdges(); 416 int nH = 0; 417 boolean haveDouble = false; 418 for (int j = edges.length; --j >= 0;) { 419 if (mustBePlanar && edges[j].getCovalentOrder() == 2) 420 haveDouble = true; 421 if (edges[j].getOtherNode(a).getIsotopeNumber() == 1) 422 nH++; 423 } 424 return (nH < 2 && (haveDouble 425 || isSmiles() || mustBePlanar == Math.abs(getTrigonality(a, 426 vNorm)) < TRIGONALITY_MIN)); 427 } 428 429 /** 430 * Allow double bonds only if trivalent and first-row atom. (IUPAC 431 * 2013.P-93.2.4) Currently: a) first row b) doubly bonded c) doubly bonded 432 * atom is also first row 433 * 434 * @param a 435 * @param edge optional bond 436 * @return STEREO_M, STEREO_Z, or UNDETERMINED 437 */ couldBeChiralAlkene(SimpleNode a, SimpleEdge edge)438 int couldBeChiralAlkene(SimpleNode a, SimpleEdge edge) { 439 SimpleNode b = (edge == null ? null : edge.getOtherNode(a)); 440 switch (a.getCovalentBondCount()) { 441 default: 442 return CIPChirality.UNDETERMINED; 443 case 2: 444 // imines and diazines 445 if (a.getElementNumber() != 7) // nitrogen 446 return CIPChirality.UNDETERMINED; 447 break; 448 case 3: 449 // first-row only (IUPAC 2013.P-93.2.4) 450 if (!CIPChirality.isFirstRow(a)) 451 return CIPChirality.UNDETERMINED; 452 break; 453 } 454 SimpleEdge[] bonds = a.getEdges(); 455 int n = 0; 456 for (int i = bonds.length; --i >= 0;) 457 if (getBondOrder(bonds[i]) == 2) { 458 if (++n > 1) 459 return CIPChirality.STEREO_M; //central allenes 460 SimpleNode other = bonds[i].getOtherNode(a); 461 if (!CIPChirality.isFirstRow(other)) 462 return CIPChirality.UNDETERMINED; 463 if (b != null && (other != b || b.getCovalentBondCount() == 1)) { 464 // could be allene central, but I think this is not necessary 465 return CIPChirality.UNDETERMINED; 466 } 467 } 468 return CIPChirality.STEREO_Z; 469 } 470 471 /** 472 * Determine the trigonality of an atom in order to determine whether it might 473 * have a lone pair. The global vector vNorm is returned as well, pointing 474 * from the atom to the base plane of its first three substituents. 475 * 476 * @param a 477 * @param vNorm 478 * a vector returned with the normal from the atom to the base plane 479 * @return distance from plane of first three covalently bonded nodes to this 480 * node 481 */ getTrigonality(SimpleNode a, V3 vNorm)482 float getTrigonality(SimpleNode a, V3 vNorm) { 483 P3[] pts = new P3[4]; 484 SimpleEdge[] bonds = a.getEdges(); 485 for (int n = bonds.length, i = n, pt = 0; --i >= 0 && pt < 4;) 486 if (bonds[i].isCovalent()) 487 pts[pt++] = bonds[i].getOtherNode(a).getXYZ(); 488 P4 plane = Measure.getPlaneThroughPoints(pts[0], pts[1], pts[2], vNorm, 489 vTemp, new P4()); 490 return Measure.distanceToPlane(plane, 491 (pts[3] == null ? a.getXYZ() : pts[3])); 492 } 493 494 // temporary fields 495 496 protected V3 vNorm = new V3(), vTemp = new V3(); 497 498 499 // public boolean canBeChiralBond(SimpleEdge bond) { 500 // return !htKekuleBonds.containsKey(Integer.valueOf(bond.hashCode())); 501 // } 502 503 /** 504 * Check cis vs. trans nature of a--b==c--d. 505 * 506 * @param a 507 * @param b 508 * @param c 509 * @param d 510 * @return true if this is a cis relationship 511 */ isCis(CIPAtom a, CIPAtom b, CIPAtom c, CIPAtom d)512 int isCis(CIPAtom a, CIPAtom b, CIPAtom c, CIPAtom d) { 513 Measure.getNormalThroughPoints(a.atom.getXYZ(), b.atom.getXYZ(), 514 c.atom.getXYZ(), vNorm, vTemp); 515 V3 vNorm2 = new V3(); 516 Measure.getNormalThroughPoints(b.atom.getXYZ(), c.atom.getXYZ(), 517 d.atom.getXYZ(), vNorm2, vTemp); 518 return (vNorm.dot(vNorm2) > 0 ? CIPChirality.STEREO_Z : CIPChirality.STEREO_E); 519 } 520 521 /** 522 * Checks the torsion angle and returns true if it is positive 523 * 524 * @param a 525 * @param b 526 * @param c 527 * @param d 528 * @return true if torsion angle is 529 */ isPositiveTorsion(CIPAtom a, CIPAtom b, CIPAtom c, CIPAtom d)530 int isPositiveTorsion(CIPAtom a, CIPAtom b, CIPAtom c, CIPAtom d) { 531 float angle = Measure.computeTorsion(a.atom.getXYZ(), b.atom.getXYZ(), 532 c.atom.getXYZ(), d.atom.getXYZ(), true); 533 return (angle > 0 ? CIPChirality.STEREO_P : CIPChirality.STEREO_M); 534 } 535 getBondOrder(SimpleEdge bond)536 int getBondOrder(SimpleEdge bond) { 537 return bond.getCovalentOrder(); 538 } 539 540 /** 541 * set the coordinate -- SMILES only 542 * 543 * @param atom1 544 * @param atoms 545 * @return true if coordinate is able to be set 546 */ setCoord(SimpleNode atom1, CIPAtom[] atoms)547 boolean setCoord(SimpleNode atom1, CIPAtom[] atoms) { 548 // 549 return true; 550 } 551 552 /** 553 * Determine the ordered CIP winding of this atom. For this, we just take 554 * the directed normal through the plane containing the top three 555 * substituent atoms and dot that with the vector from any one of them to 556 * the fourth ligand (or the root atom if trigonal pyramidal). If this is 557 * positive, we have R. 558 * 559 * @param a 560 * 561 * @return 1 for "R", 2 for "S" 562 */ checkHandedness(CIPAtom a)563 int checkHandedness(CIPAtom a) { 564 CIPAtom[] atoms = a.atoms; 565 if (!setCoord(a.atom, atoms)) 566 return CIPChirality.NO_CHIRALITY; 567 P3 p0 = (atoms[3].atom == null ? a.atom : atoms[3].atom).getXYZ(); 568 P3 p1 = atoms[0].atom.getXYZ(), p2 = atoms[1].atom.getXYZ(), p3 = atoms[2].atom 569 .getXYZ(); 570 Measure.getNormalThroughPoints(p1, p2, p3, vNorm, vTemp); 571 vTemp.setT(p0); 572 vTemp.sub(p1); 573 return (vTemp.dot(vNorm) > 0 ? CIPChirality.STEREO_R : CIPChirality.STEREO_S); 574 } 575 576 /** 577 * Track this decision - CIPDataTracker only 578 * 579 * @param cip 580 * @param a 581 * @param b 582 * @param sphere 583 * @param finalScore 584 * @param trackTerminal 585 */ track(CIPChirality cip, CIPAtom a, CIPAtom b, int sphere, int finalScore, boolean trackTerminal)586 void track(CIPChirality cip, CIPAtom a, CIPAtom b, int sphere, 587 int finalScore, boolean trackTerminal) { 588 // CIPDataTracker only 589 } 590 591 /** 592 * CIPDataTracker only 593 * 594 * @param root 595 * @return string expression of decision path 596 */ getRootTrackerResult(CIPAtom root)597 String getRootTrackerResult(CIPAtom root) { 598 // CIPDataTracker only 599 return null; 600 } 601 setRule6Full(boolean rrrr)602 public void setRule6Full(boolean rrrr) { 603 testRule6Full = rrrr; 604 } 605 } 606