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