Revision: 21646 http://sourceforge.net/p/jmol/code/21646 Author: hansonr Date: 2017-07-03 13:12:40 +0000 (Mon, 03 Jul 2017) Log Message: ----------- Jmol.___JmolVersion="14.20.1"
new feature: BZ2 compressed file reader -- uses org.apache.tools.bzip2.CBZip2InputStream v. 1.9.6 -- Apache license bug fix: SMILES generator can show wrong @/@@ or stereochemical type for some allenes and cumulenes bug fix: CIPChirality additional Rule 4b and 4c fixes Modified Paths: -------------- trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java trunk/Jmol/src/org/jmol/symmetry/Symmetry.java trunk/Jmol/src/org/jmol/viewer/Jmol.properties Modified: trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java =================================================================== --- trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java 2017-06-28 22:45:07 UTC (rev 21645) +++ trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java 2017-07-03 13:12:40 UTC (rev 21646) @@ -115,8 +115,11 @@ * * code history: * - * 6/28/17 Jmol 14.19.2 major rewrite of Rule 4b (962 lines) + * 7/3/17 Jmol 14.20.1 major rewrite to correct and simplify logic; full validation + * for 433 structures (many duplicates) in AY236, BH64, MV64, MV116, JM, and L (974 lines) * + * 6/30/17 Jmol 14.20.1 major rewrite of Rule 4b (999 lines) + * * 6/25/17 Jmol 14.19.1 minor fixes for Rule 4b and 5 for BH64_012-015; better * atropisomer check * @@ -388,7 +391,9 @@ static final int DIASTEREOMERIC = -3; static final int DIASTEREOMERIC_A_WINS = -2; static final int DIASTEREOMERIC_B_WINS = 2; - static final int IGNORE = Integer.MIN_VALUE; + static final int ENANTIOMERIC_A_WINS = -3; + static final int ENANTIOMERIC_B_WINS = 3; +static final int IGNORE = Integer.MIN_VALUE; static final int NOT_RELEVANT = Integer.MIN_VALUE; static final int STEREO_UNDETERMINED = -1; @@ -485,6 +490,7 @@ V3 vNorm = new V3(), vNorm2 = new V3(), vTemp = new V3(); boolean allowRule1bAlkenes = true; + boolean setAuxiliary; public CIPChirality() { // for reflection @@ -519,12 +525,14 @@ * @param bsHelixP * aromatic atoms at the end of a positive helical turn; * smarts("A{a}(.t:10,40)a(.t:10,40)aaa") + * @param setAuxiliary also set auxiliary (single-atom only) */ public void getChiralityForAtoms(SimpleNode[] atoms, BS bsAtoms, - BS bsAtropisomeric, BS bsHelixM, BS bsHelixP) { + BS bsAtropisomeric, BS bsHelixM, BS bsHelixP, boolean setAuxiliary) { if (bsAtoms.isEmpty()) return; init(); + this.setAuxiliary = (setAuxiliary && bsAtoms.cardinality() == 1); this.bsAtropisomeric = (bsAtropisomeric == null ? new BS() : bsAtropisomeric); @@ -547,7 +555,7 @@ SimpleNode a = atoms[i]; a.setCIPChirality(0); ptID = 0; - int c = getAtomChiralityLimited(a, null, null, RULE_5); + int c = getAtomChiralityLimited(a, null, null); a.setCIPChirality(c == 0 ? JC.CIP_CHIRALITY_NONE : c | ((currentRule - 1) << JC.CIP_CHIRALITY_NAME_OFFSET)); } @@ -939,13 +947,13 @@ index1 = atom1.getIndex(); if (!bsAtropisomeric.get(index1)) continue; - c = setBondChirality(atom, atom1, atom, atom1, true, RULE_5); + c = setBondChirality(atom, atom1, atom, atom1, true); } else if (bond.getCovalentOrder() == 2) { atom1 = getLastCumuleneAtom(bond, atom, null, null); index1 = atom1.getIndex(); if (index1 < index) continue; - c = getBondChiralityLimited(bond, atom, RULE_5); + c = getBondChiralityLimited(bond, atom); } else { continue; } @@ -1019,20 +1027,19 @@ * ignored if atom is not null * @param parent * null for tetrahedral, other alkene carbon for E/Z - * @param ruleMax - * allows limiting this to Rule 3 * * @return if parent != null: [0:none, 1:R, 2:S] otherwise [0:none, 1: * atoms[0] is higher, 2: atoms[1] is higher] */ private int getAtomChiralityLimited(SimpleNode atom, CIPAtom cipAtom, - CIPAtom parent, int ruleMax) { + CIPAtom parent) { int rs = NO_CHIRALITY; - boolean isChiral = false, isAlkene = false; + boolean isAlkene = false; try { if (cipAtom == null) { cipAtom = new CIPAtom().create(atom, null, isAlkene, false, false); - int nSubs = atom.getCovalentBondCount(), elemNo = atom.getElementNumber(); + int nSubs = atom.getCovalentBondCount(), elemNo = atom + .getElementNumber(); isAlkene = (nSubs == 3 && elemNo <= 10 && !cipAtom.isTrigonalPyramidal); // (IUPAC 2013.P-93.2.4) if (nSubs != (parent == null ? 4 : 3) - (nSubs == 3 && !isAlkene ? 1 : 0)) @@ -1047,34 +1054,38 @@ cipAtom.htPathPoints = parent.htPathPoints; currentRule = RULE_1a; if (cipAtom.setNode()) { - for (currentRule = RULE_1a; currentRule <= ruleMax; currentRule++) { + for (; currentRule <= RULE_5; currentRule++) { if (Logger.debugging) Logger.info("-Rule " + getRuleName() + " CIPChirality for " + cipAtom + "-----"); // Logger - if (currentRule == RULE_4a) - cipAtom.createRule4AuxiliaryData(null, "", null); - isChiral = cipAtom.sortSubstituents(0); - if (Logger.debugging) { - Logger.info(currentRule + ">>>>" + cipAtom); - for (int i = 0; i < cipAtom.bondCount; i++) { // Logger - if (cipAtom.atoms[i] != null) // Logger - Logger.info(cipAtom.atoms[i] + " " - + Integer.toHexString(cipAtom.priorities[i])); // Logger + if (currentRule == RULE_4a) { + cipAtom.createRule4AuxiliaryData(null, null); + if (cipAtom.rule4Type == 0) { + // we can skip Rules 4a - 5 if there are no chirality centers + break; } } - if (cipAtom.achiral) { - isChiral = false; - break; - } - if (currentRule == RULE_5) - cipAtom.isPseudo = cipAtom.canBePseudo; - if (isChiral) { - rs = (!isAlkene ? cipAtom.checkHandedness() - : cipAtom.atoms[0].isDuplicate ? STEREO_S : STEREO_R); - if (!isAlkene && cipAtom.isPseudo && cipAtom.canBePseudo) { - currentRule = RULE_5; - rs = rs | JC.CIP_CHIRALITY_PSEUDO_FLAG; + + // initial call to sortSubstituents does all, recursively + + if (cipAtom.sortSubstituents(0)) { + + if (Logger.debugging) { + Logger.info(currentRule + ">>>>" + cipAtom); + for (int i = 0; i < cipAtom.bondCount; i++) { // Logger + if (cipAtom.atoms[i] != null) // Logger + Logger.info(cipAtom.atoms[i] + " " + + Integer.toHexString(cipAtom.priorities[i])); // Logger + } } + + if (isAlkene) { + rs = (cipAtom.atoms[0].isDuplicate ? STEREO_S : STEREO_R); + } else { + rs = cipAtom.checkHandedness() + | (currentRule == RULE_5 && cipAtom.canBePseudo ? JC.CIP_CHIRALITY_PSEUDO_FLAG + : 0); + } if (Logger.debugging) Logger.info(atom + " " + JC.getCIPChiralityName(rs) + " by Rule " + getRuleName() + "\n----------------------------------"); // Logger @@ -1102,11 +1113,10 @@ * @param bond * @param a * first atom to consider, or null - * @param ruleMax * @return one of: {NO_CHIRALITY | STEREO_Z | STEREO_E | STEREO_Ra | STEREO_Sa * | STEREO_ra | STEREO_sa} */ - private int getBondChiralityLimited(SimpleEdge bond, SimpleNode a, int ruleMax) { + private int getBondChiralityLimited(SimpleEdge bond, SimpleNode a) { if (Logger.debugging) Logger.info("get Bond Chirality " + bond); if (a == null) @@ -1117,7 +1127,7 @@ SimpleNode[] parents = new SimpleNode[2]; SimpleNode b = getLastCumuleneAtom(bond, a, nSP2, parents); boolean isAxial = nSP2[0] % 2 == 1; - return setBondChirality(a, parents[0], parents[1], b, isAxial, ruleMax); + return setBondChirality(a, parents[0], parents[1], b, isAxial); } /** @@ -1128,17 +1138,16 @@ * @param pb * @param b * @param isAxial - * @param ruleMax * @return one of: {NO_CHIRALITY | STEREO_Z | STEREO_E | STEREO_M | STEREO_P | * STEREO_m | STEREO_p} */ private int setBondChirality(SimpleNode a, SimpleNode pa, SimpleNode pb, SimpleNode b, - boolean isAxial, int ruleMax) { + boolean isAxial) { CIPAtom a1 = new CIPAtom().create(a, null, true, false, false); - int atop = getAlkeneEndTopPriority(a1, pa, isAxial, ruleMax); + int atop = getAlkeneEndTopPriority(a1, pa, isAxial); int ruleA = currentRule; CIPAtom b2 = new CIPAtom().create(b, null, true, false, false); - int btop = getAlkeneEndTopPriority(b2, pb, isAxial, ruleMax); + int btop = getAlkeneEndTopPriority(b2, pb, isAxial); int ruleB = currentRule; int c = (atop >= 0 && btop >= 0 ? getEneChirality(b2.atoms[btop], b2, a1, a1.atoms[atop], isAxial, true) : NO_CHIRALITY); @@ -1145,6 +1154,11 @@ if (c != NO_CHIRALITY && (isAxial || !bsAtropisomeric.get(a.getIndex()) && !bsAtropisomeric.get(b.getIndex()))) { + if (isAxial && ((ruleA == RULE_5) != (ruleB == RULE_5))) { + // only one of the ends may be enantiomeric to make this r or s + // see AY236.70 and AY236.170 + c |= JC.CIP_CHIRALITY_PSEUDO_FLAG; + } a.setCIPChirality(c | ((ruleA - 1) << JC.CIP_CHIRALITY_NAME_OFFSET)); b.setCIPChirality(c | ((ruleB - 1) << JC.CIP_CHIRALITY_NAME_OFFSET)); if (Logger.debugging) @@ -1172,8 +1186,6 @@ return (top1 == null || top2 == null || top1.atom == null || top2.atom == null ? NO_CHIRALITY : isAxial ? (isPos(top1, end1, end2, top2) ? STEREO_P : STEREO_M) - | (allowPseudo && (end2.ties == null) != (end1.ties == null) ? JC.CIP_CHIRALITY_PSEUDO_FLAG - : 0) : (isCis(top1, end1, end2, top2) ? STEREO_Z : STEREO_E)); } @@ -1183,14 +1195,12 @@ * @param a * @param pa * @param isAxial - * @param ruleMax * @return -1, 0, or 1 */ - private int getAlkeneEndTopPriority(CIPAtom a, SimpleNode pa, boolean isAxial, - int ruleMax) { - a.canBePseudo = a.isAxialRoot = isAxial; + private int getAlkeneEndTopPriority(CIPAtom a, SimpleNode pa, boolean isAxial) { + a.canBePseudo = isAxial; return getAtomChiralityLimited(a.atom, a, - new CIPAtom().create(pa, null, true, false, false), ruleMax) - 1; + new CIPAtom().create(pa, null, true, false, false)) - 1; } /** @@ -1436,42 +1446,18 @@ //// AUXILIARY CHIRALITY for Rules 4 and 5 ///// /** - * already-determined auxiliary chirality (E/Z, R/S, etc) for this atom + * already-determined auxiliary chirality E/Z for this atom * node; this value must be cleared after Rule 3 if continuing */ private int auxEZ = STEREO_UNDETERMINED; /** - * temporary check for pseudochirality + * a flag set false in evaluation of Rule 5 to indicate that there was more + * than one R/S decision made, so this center cannot be r/s. */ - boolean doCheckPseudo; - - /** - * permanent check for pseudochirality - */ - boolean isPseudo; - - /** - * Force achiral condition due to two identical ligands after Rule 4 check. - */ - boolean achiral; - - /** - * a flag set false in Mata analysis - */ boolean canBePseudo = true; /** - * used to determine pseudochirality - */ - Lst<int[]> ties; - - /** - * used to defer pseudochirality check - */ - boolean isAxialRoot; - - /** * auxiliary chirality as determined in createAuxiliaryRule4Data; * possibilities include R/S, r/s, M/P, m/p, C/T (but not c/t), or ~ (ASCII * 126, no stereochemistry); for sorting purposes C=M=R < p=r=s < ~ @@ -1505,8 +1491,20 @@ * */ - private Lst<Object[]> rule4Paths; - + private Lst<String[]> rule4Paths; + + /** + * a list of priorities from the root leading this branch + * + */ + private String priorityPath; + + /** + * for the root atom, the number of auxiiary centers; for other atoms, the auxiiary + * chirality type: 0: ~, 1: R, 2: S + */ + int rule4Type; + CIPAtom() { // had a problem in JavaScript that the constructor of an inner function cannot // access this.b$ yet. That assignment is made after construction. @@ -1519,7 +1517,7 @@ * @param parent * @param isAlkene * @param isDuplicate - * @param isParentBond TODO + * @param isParentBond * @return this */ CIPAtom create(SimpleNode atom, CIPAtom parent, boolean isAlkene, @@ -1759,8 +1757,7 @@ boolean sortSubstituents(int sphere) { int[] indices = new int[4], prevPrior = new int[4]; - ties = null; - int nTies = 0; + int nPrioritiesPrev = nPriorities; for (int i = 0; i < 4; i++) { prevPrior[i] = priorities[i]; priorities[i] = 0; @@ -1781,7 +1778,8 @@ CIPAtom a = atoms[i]; for (int j = i + 1; j < 4; j++) { CIPAtom b = atoms[j]; - boolean logger_debugHigh = Logger.debuggingHigh && b.isHeavy() && a.isHeavy(); + boolean logger_debugHigh = Logger.debuggingHigh && b.isHeavy() + && a.isHeavy(); int score = (a.atom == null ? B_WINS : b.atom == null ? A_WINS : prevPrior[i] == prevPrior[j] ? TIED : prevPrior[j] < prevPrior[i] ? B_WINS : A_WINS); @@ -1790,16 +1788,21 @@ score = (checkRule4List ? checkRule4And5(i, j) : a.checkPriority(b)); if (logger_debugHigh) Logger.info(dots() + "ordering " + this.id + "." + i + "." + j - + " " + this + "-" + a + " vs " + b + " = " + score); // Logger + + " " + this + "-" + a + " vs " + b + " = " + score); // Logger switch (score) { case IGNORE: // just increment the index and go on to the next rule -- no breaking of the tie - if (checkRule4List && sphere == 0) - achiral = true; // two ligands for the root atom found to be equivalent in Rule 4 must be achiral indices[i]++; if (logger_debugHigh) - Logger.info(dots() + atom + "." + b + " ends up with tie with " + a); + Logger.info(dots() + atom + "." + b + " ends up with tie with " + + a); break; + case A_WINS: + indices[j]++; + priorities[j]++; + if (logger_debugHigh) + Logger.info(dots() + this + "." + a + " A-beats " + b); + break; case B_WINS: indices[i]++; priorities[i]++; @@ -1806,30 +1809,13 @@ if (logger_debugHigh) Logger.info(dots() + this + "." + b + " B-beats " + a); break; - case A_WINS: - indices[j]++; - priorities[j]++; - if (logger_debugHigh) - Logger.info(dots() + this + "." + a + " A-beats " + b); - break; case TIED: switch (score = sign(a.breakTie(b, sphere + 1))) { - case TIED: - if (parent == null && currentRule >= RULE_4c && ++nTies == 2 - && a.spiroEnd != null && atoms[0].spiroEnd != null - && a.spiroEnd.atom != b.atom - && atoms[0].spiroEnd != a.spiroEnd) { - priorities[1]++; - int pt = (a.spiroEnd.atom == atoms[0].atom ? j : i); - priorities[pt]++; - indices[pt]++; - if (logger_debugHigh) - Logger.info(dots() + this + " spiro Xaa'bb' tie resolved"); - } else { - indices[j]++; - if (logger_debugHigh) - Logger.info(dots() + this + "." + b + " ends up with tie with " + a); - } + case A_WINS: + indices[j]++; + priorities[j]++; + if (logger_debugHigh) + Logger.info(dots() + this + "." + a + " wins in tie with " + b); break; case B_WINS: indices[i]++; @@ -1837,23 +1823,15 @@ if (logger_debugHigh) Logger.info(dots() + this + "." + b + " wins in tie with " + a); break; - case A_WINS: + case TIED: indices[j]++; - priorities[j]++; if (logger_debugHigh) - Logger.info(dots() + this + "." + a + " wins in tie with " + b); + Logger.info(dots() + this + "." + b + " ends up with tie with " + + a); break; } break; } - if (doCheckPseudo) { - // Rule 4 has found enantiomeric ligands. We need to make sure - // there are not two such sets. - doCheckPseudo = false; - if (ties == null) - ties = new Lst<int[]>(); - ties.addLast(new int[] { i, j, score }); - } } } @@ -1878,38 +1856,100 @@ rule4List = newRule4List; nPriorities = bs.cardinality(); - // Check for pseudochirality + if (parent == null) { + + // Check for special root-only cases: + // + // P-92.2.1.1(c) pseudoasymmetric centers must have + // two and only two enantiomorphic ligands + // + // P-33.5.3.1 spiro compounds have increased constraints + // + // We do the spiro test first, as it may inform the pseudoasymmetric test. - if (ties != null) { - switch (ties.size()) { - case 1: - // in the case of an alkene, we are just checking for a winner on one atom - // otherwise we need to check the chirality of this tie - switch (isAlkene ? STEREO_R : checkPseudoHandedness(ties.get(0), indices)) { - case STEREO_R: - case STEREO_S: - isPseudo = canBePseudo; - break; + if (nPriorities == 2 && priorities[3] == 2) { + // + // We have priorities [0 0 2 2], possibly being spiro + // + int end0, end1; + if ((end0 = getSpiroType(0)) >= 2 && (end1 = getSpiroType(1)) >= 2 + && end0 != end1) { + // + // We are done, because the two spiro ends set both of these ties. + // a a'b b' (order is already OK) + // a a'b'b (needs switching) + // + // P-93.2.3.2 + // + // We have priorities [0 0 2 2], possibly being spiro + // + // b b + // /- -\ + // / - - \ + // a--C--a' R a--C--a' S + // - / \ - + // -/ \- + // b' b' + // + // a==a' and b==b'; order of precedence is set to a > a' > b > b' + // + // Note that this is really a case of axial chirality. It is the same as: + // + // b' a' + // - - + // - - + // a--C--b M a--C--b P + // - - + // - - + // a' b' + + nPrioritiesPrev = 2; + nPriorities = 4; + if (end1 == 2) { + CIPAtom a = atoms[2]; + atoms[2] = atoms[3]; + atoms[3] = a; + } } - break; - case 2: - break; - case 4: - if (nPriorities == 2) { - priorities[1]++; - priorities[atoms[0].spiroEnd == atoms[2].spiroEnd ? 3 : 2]++; - nPriorities = 4; - achiral = false; - } - break; } + + if (currentRule == RULE_5 && nPriorities == 4 && nPrioritiesPrev == 2) { + + // Rule 5 has decided the issue, but how many decisions did we make? + // If priorities [0 0 2 2] went to [1 2 3 4] then + // we have two Rule-5 decisions -- R,S,R',S'. + // In that case, Rule 5 resuls in R/S, not r/s. + // + // S + // - + // - + // R---C---R' despite this being Rule 5, the results is R, not r. + // - + // - + // S' + // + // --------- mirror plane + // + // R' + // - + // - + // S---C---S' not superimposible + // - + // - + // R + // + canBePseudo = false; + } + + } - if ((Logger.debuggingHigh) && atoms[2].atom != null && atoms[2].elemNo != 1) { + if ((Logger.debuggingHigh) && atoms[2].atom != null + && atoms[2].elemNo != 1) { Logger.info(dots() + atom + " nPriorities = " + nPriorities); for (int i = 0; i < 4; i++) { // Logger Logger.info(dots() + myPath + "[" + i + "]=" + atoms[i] + " " + priorities[i] + " " + Integer.toHexString(priorities[i]) - + " new"); // Logger + + " new"); // Logger } Logger.info(dots() + "-------"); } @@ -1917,6 +1957,21 @@ } /** + * Find the other end of a loop to root. + * + * @param i0 index of the spiro starting atom + * @return pointer to spiro end of atoms[0] -- either 0 (not spiro), 1, 2, + * or 3 + */ + private int getSpiroType(int i0) { + if (atoms[i0].spiroEnd != null) + for (int i = 0; i < 4; i++) + if (i != i0 && atoms[i0].spiroEnd.atom == atoms[i].atom) + return i; + return -1; + } + + /** * Provide an indent for clarity in debugging messages * * @return a string of dots based on the value of atom.sphere. @@ -2223,7 +2278,8 @@ // the atom after the alkene). if (auxEZ == STEREO_UNDETERMINED && (auxEZ = alkeneParent.auxEZ) == STEREO_UNDETERMINED) { - auxEZ = getEneWinnerChirality(alkeneParent, this, RULE_3, false); + int[] maxRules = new int[] {RULE_3, RULE_3}; + auxEZ = getEneWinnerChirality(alkeneParent, this, false, maxRules); if (auxEZ == NO_CHIRALITY) auxEZ = STEREO_BOTH_EZ; } @@ -2238,16 +2294,22 @@ * * @param end1 * @param end2 - * @param maxRule * @param isAxial + * @param maxRuleRet [max,max] returns [max1, max2] * @return one of: {NO_CHIRALITY | STEREO_Z | STEREO_E | STEREO_M | * STEREO_P} */ - private int getEneWinnerChirality(CIPAtom end1, CIPAtom end2, int maxRule, - boolean isAxial) { - CIPAtom winner1 = getEneEndWinner(end1, end1.nextSP2, maxRule), - winner2 = (winner1 == null || winner1.atom == null ? null - : getEneEndWinner(end2, end2.nextSP2, maxRule)); + private int getEneWinnerChirality(CIPAtom end1, CIPAtom end2, + boolean isAxial, int[] maxRuleRet) { + // [0] returns max rule for determination 1 + CIPAtom winner1 = getEneEndWinner(end1, end1.nextSP2, maxRuleRet); + int max1 = maxRuleRet[0]; + maxRuleRet[0] = maxRuleRet[1]; + CIPAtom winner2 = (winner1 == null || winner1.atom == null ? null + : getEneEndWinner(end2, end2.nextSP2, maxRuleRet)); + maxRuleRet[1] = maxRuleRet[0]; + maxRuleRet[0] = max1; + // returns [max1, max2] return getEneChirality(winner1, end1, end2, winner2, isAxial, false); } @@ -2258,17 +2320,23 @@ * * @param end * @param prevSP2 - * @param ruleMax + * @param ruleMaxRet * @return higher-priority atom, or null if they are equivalent */ - private CIPAtom getEneEndWinner(CIPAtom end, CIPAtom prevSP2, int ruleMax) { + private CIPAtom getEneEndWinner(CIPAtom end, CIPAtom prevSP2, int[] ruleMaxRet) { CIPAtom atom1 = (CIPAtom) end.clone(); - if (atom1.parent != prevSP2) + int ruleMax = ruleMaxRet[0]; + if (atom1.parent != prevSP2) atom1.addReturnPath(prevSP2, end); + else if (ruleMax > RULE_3) + atom1.rule4List = end.rule4List; CIPAtom a = null; + ruleMaxRet[0] = -1; for (int i = RULE_1a; i <= ruleMax; i++) - if ((a = atom1.getTopSorted(i)) != null) + if ((a = atom1.getTopSorted(i)) != null) { + ruleMaxRet[0] = i; break; + } return (a == null || a.atom == null ? null : a); } @@ -2385,113 +2453,140 @@ String aref = (isRule5 ? "R" : a.getRule4ReferenceDescriptor()); String bref = (isRule5 ? "R" : b.getRule4ReferenceDescriptor()); boolean haveRSOptions = (aref == "RS"); - if (aref.length() != bref.length()) - return (haveRSOptions ? B_WINS : A_WINS); - String aStr = (haveRSOptions ? a.flattenRule4Paths('R', isRule5) + "|" - + a.flattenRule4Paths('S', isRule5) : a.flattenRule4Paths( - aref.charAt(0), isRule5)); - String bStr = (haveRSOptions ? b.flattenRule4Paths('R', isRule5) + "|" - + b.flattenRule4Paths('S', isRule5) : b.flattenRule4Paths( - bref.charAt(0), isRule5)); + System.out.println("reference descriptors are " + aref + " and " + bref); - if (Logger.debugging) - Logger.info(dots() + this + " comparing " + a + " " + aStr + " to " + b - + " " + bStr); // Logger - if (isRule5) { - // note that these two strings cannot be different lengths - return sign(aStr.compareTo(bStr)); - } - aStr = cleanRule4Str(aStr); - bStr = cleanRule4Str(bStr); - if (haveRSOptions) { - // Mata(2005) Figure 9 - // We are trying to ascertain that - // R lull R luuu - // S luuu is the same as S lull - // - // Solution is to SUM all winners. If that is 0, then they are the same - String[] aList = PT.split(aStr, "|"), bList = PT.split(bStr, "|"); - int minScore = Integer.MAX_VALUE, sumScore = 0; - for (int i = aList.length; --i >= 0;) { - for (int j = bList.length; --j >= 0;) { - int score = compareRule4PairStr(aList[i], bList[j], true); - sumScore += score; - if (score != TIED && Math.abs(score) <= Math.abs(minScore)) { - minScore = score; + int score = TIED; + String reason = "Rule 4b"; + String aStr = aref, bStr = bref; + + while (true) { + + // check for RS on only one side + if (aref.length() != bref.length()) { + score = (haveRSOptions ? B_WINS : A_WINS); + reason += " RS on only one side"; + break; + } + + aStr = (haveRSOptions ? a.flattenRule4Paths('R', isRule5) + "|" + + a.flattenRule4Paths('S', isRule5) : a.flattenRule4Paths( + aref.charAt(0), isRule5)); + bStr = (haveRSOptions ? b.flattenRule4Paths('R', isRule5) + "|" + + b.flattenRule4Paths('S', isRule5) : b.flattenRule4Paths( + bref.charAt(0), isRule5)); + + if (Logger.debugging) + Logger.info(dots() + this + " comparing " + a + " " + aStr + " to " + + b + " " + bStr); // Logger + + // just return string comparison if Rule 5 + if (isRule5) { + // note that these two strings cannot be different lengths + score = sign(aStr.compareTo(bStr)); + reason = "Rule 5"; + break; + } + + // check for + aStr = cleanRule4Str(aStr); + bStr = cleanRule4Str(bStr); + if (haveRSOptions) { + // Mata(2005) Figure 9 + // We are trying to ascertain that + // R lull R luuu + // S luuu is the same as S lull + // + // Solution is to SUM all winners. If that is 0, then they are the same + String[] aList = PT.split(aStr, "|"), bList = PT.split(bStr, "|"); + int minScore = Integer.MAX_VALUE, sumScore = 0; + for (int i = aList.length; --i >= 0;) { + for (int j = bList.length; --j >= 0;) { + score = compareRule4PairStr(aList[i], bList[j], true); + sumScore += score; + if (score != TIED && Math.abs(score) <= Math.abs(minScore)) { + minScore = score; + } } } + score = (sumScore == TIED ? TIED : minScore < 0 ? A_WINS : B_WINS); + break; } - minScore = (sumScore == TIED ? TIED : minScore < 0 ? A_WINS : B_WINS); - Logger.info(aStr + (minScore == A_WINS ? " > " : minScore == B_WINS ? " < " : " = ") + bStr); - return minScore; + score = compareRule4PairStr(aStr, bStr, false); + break; + } - if (aStr.length() == 1 && "RS".indexOf(aStr) < 0) { - int score = checkEnantiomer(aStr, bStr, 0, aStr.length(), " rs"); - switch (score) { - case A_WINS: - case B_WINS: - canBePseudo = false; - doCheckPseudo = !isAxialRoot; - return score; - } - } - return compareRule4PairStr(aStr, bStr, false); + + Logger.info(a + + (score == A_WINS ? " > " : score == B_WINS ? " < " : " = ") + + b + " by " + reason + "\n"); + + return score; } /** * Combine all subpaths */ - private void generateRule4Paths() { - rule4Paths = new Lst<Object[]>(); - appendRule4Paths(this, null); + void generateRule4Paths() { - if (true || Logger.debugging) { + getRule4Paths(""); + + + rule4Paths = new Lst<String[]>(); + appendRule4Paths(this, new String[3]); + + getRule4Counts(rule4Count = new Object[] { null, zero, zero, Integer.valueOf(10000)}); + + if (Logger.debugging) { Logger.info("Rule 4b paths for " + this + "=\n"); for (int i = 0; i < rule4Paths.size(); i++) { // Logger String s = rule4Paths.get(i)[0].toString(); // Logger - int prefixLen = ((Integer)rule4Paths.get(i)[1]).intValue(); // Logger + int prefixLen = rule4Paths.get(i)[1].length(); // Logger while (prefixString.length() < prefixLen) // Logger prefixString += prefixString; // Logger - Logger.info(prefixString.substring(0, prefixLen) + s.substring(prefixLen)); + Logger.info(prefixString.substring(0, prefixLen) + s.substring(prefixLen) + " " + priorityPath); } Logger.info(""); } } - private void appendRule4Paths(CIPAtom rootsub, SB path) { - String s0 = (path == null ? auxChirality : path.toString()); - if (path == null) - path = rootsub.addRule4Path(s0, true); - int iFirst = 4; - int lastPriority = -1; - boolean addPriority = false; - for (int i = 4; --i >= 0;) + private void getRule4Counts(Object[] counts) { + if (sphere > ((Integer) counts[3]).intValue()) + return; + if (setAuxiliary && auxChirality != "~") { + atom.setCIPChirality(JC.getCIPChiralityCode(auxChirality)); + } + if (rule4Type > 0) + addMataRef(counts); + for (int i = 0; i < 4; i++) + if (rule4List[i] != null) + atoms[i].getRule4Counts(counts); + } + + private void getRule4Paths(String path) { + priorityPath = path + (priority + 1); + for (int i = 0; i < 4; i++) + if (rule4List[i] != null) + atoms[i].getRule4Paths(priorityPath); + } + + private void appendRule4Paths(CIPAtom rootsub, String[] pathInfo) { + String s0 = (pathInfo[0] == null ? auxChirality : pathInfo[0]); + if (pathInfo[2] == null) + rootsub.rule4Paths.addLast(pathInfo = new String[] {s0, "", priorityPath}); + boolean isFirst = true; + for (int i = 0; i < 4; i++) if (rule4List[i] != null) { - iFirst = i; - if (lastPriority == -1) - lastPriority = atoms[i].priority; - else if (lastPriority != atoms[i].priority) - addPriority = true; - } - for (int i = iFirst; i < 4; i++) - if (rule4List[i] != null) { - if (i != iFirst) - path = rootsub.addRule4Path(s0, false); - if (addPriority) - path.appendI(atoms[i].priority + 1); - path.append(rule4List[i]); + if (isFirst) + pathInfo[2] = priorityPath; + else + rootsub.rule4Paths.addLast(pathInfo = new String[] {s0, s0, priorityPath}); + isFirst = false; + pathInfo[0] += rule4List[i]; if (atoms[i].nextChiralBranch != null) - atoms[i].nextChiralBranch.appendRule4Paths(rootsub, path); + atoms[i].nextChiralBranch.appendRule4Paths(rootsub, pathInfo); } } - private SB addRule4Path(String s0, boolean doPrefix) { - SB path = new SB(); - path.append(s0); - rule4Paths.addLast(new Object[] {path, new Integer(doPrefix ? 0 : s0.length())}); - return path; - } - /** * rule4Count holds in [1] and [2] the number of R and S descriptors, respectively, * in the highest ranking sphere with stereochemistry. @@ -2525,17 +2620,20 @@ int nMax = 0; for (int i = 0; i < nPaths; i++) { // remove all enantiomorphic descriptors - String s = PT.replaceAllCharacters(rule4Paths.get(i)[0].toString(), "srctmp", "~"); + String path = rule4Paths.get(i)[0]; + String priorityPath = rule4Paths.get(i)[2]; + String s = PT.replaceAllCharacters(path, "srctmp", "~"); // remove all - s = s.replace(ref, 'A'); + paths[i] = s = priorityPath + s.replace(ref, 'A'); if (s.length() > nMax) nMax = s.length(); - paths[i] = s; } Arrays.sort(paths); - // now remove the - for (int i = 0; i < nPaths; i++) - paths[i] = PT.replaceAllCharacters(paths[i], "1234", ""); + // now remove the priorities + for (int i = 0; i < nPaths; i++) { + paths[i] = PT.replaceAllCharacters(paths[i], "1234", "").replace('A', ref); + System.out.println("Flattened[" + i + "]=" + paths[i]); + } SB sb = new SB(); String s; for (int i = 0; i < nMax; i++) { @@ -2544,7 +2642,7 @@ sb.append(i < s.length() ? s.substring(i, i + 1) : "~"); } } - return sb.toString().replace('A', ref); + return sb.toString(); } /** @@ -2559,54 +2657,6 @@ : aStr); } -// TODO: what about Rule 5?? -// -// * @param lst -// * @param chRef -// * @param isRule5 -// * @return one string, possibly separated by | indicating that the result -// * has both an R and S side to it -// */ -// private String getMataSequence(String[] lst, String chRef, boolean isRule5) { -// int n = lst.length, len = 0; -// String[] lst1 = new String[n]; -// for (int j = n, i = rule4List.length; --i >= 0;) { -// if (rule4List[i] != null) { -// --j; -// lst1[j] = lst[j]; -// if (atoms[i].nextChiralBranch != null) -// lst1[j] += atoms[i].nextChiralBranch.getMataList(chRef, isRule5); -// } -// } -// String[] sorted = (isRule5 ? lst1 : getMataSortedList(lst1, chRef)); -// for (int i = 0; i < n; i++) { -// String rs = sorted[i]; -// if (rs.length() > len) -// len = rs.length(); -// } -// -// // Strip out all non-R/S designations -// String mlist = ""; -// char ch; -// for (int i = 1; i < len; i++) { -// for (int j = 0; j < n; j++) { -// String rs = sorted[j]; -// if (i < rs.length() && (ch = rs.charAt(i)) != '~' && ch != ';') -// mlist += ch; -// } -// if (isRule5) { -// // clear out this sphere and resort -// for (int j = 0; j < n; j++) { -// String rs = sorted[j]; -// if (i < rs.length()) -// sorted[j] = rs.substring(0, i) + "~" + rs.substring(i + 1); -// } -// Arrays.sort(sorted); -// } -// } -// return mlist; -// } - /** * Comparison of two strings such as RSSR and SRSS for Rule 4b. * @@ -2621,7 +2671,6 @@ private int compareRule4PairStr(String aStr, String bStr, boolean isRSTest) { if (Logger.debugging) Logger.info(dots() + this.myPath + " Rule 4b comparing " + aStr + " " + bStr); - doCheckPseudo = false; int n = aStr.length(); if (n == 0 || n != bStr.length()) return TIED; @@ -2631,15 +2680,7 @@ if (alike != (bref == bStr.charAt(c))) return (isRSTest ? c : 1) * (alike ? A_WINS : B_WINS); // COUNT_LINE } - if (isRSTest) - return TIED; - if (aref == bref) - return IGNORE; - // are opposites - if (!canBePseudo) - root.canBePseudo = false; - doCheckPseudo = canBePseudo && (aref == 'R' || aref == 'S'); - return aref < bref ? A_WINS : B_WINS; + return (isRSTest ? TIED : IGNORE); } // /** @@ -2673,20 +2714,19 @@ * * @param node1 * first node; sphere 1 - * @param priorityPath * @param ret * CIPAtom of next stereochemical branching point * * @return collective string, with setting of rule4List */ - String createRule4AuxiliaryData(CIPAtom node1, String priorityPath, CIPAtom[] ret) { + String createRule4AuxiliaryData(CIPAtom node1, CIPAtom[] ret) { int rs = -1; String subRS = "", s = (node1 == null ? "" : "~"); boolean isBranch = false, noPseudo = false; + int nRS = 0; if (atom != null) { rule4List = new String[4]; // full list based on atoms[] int[] mataList = new int[4]; //sequential pointers into rule4List - int nRS = 0; CIPAtom[] ret1 = new CIPAtom[1]; for (int i = 0; i < 4; i++) { CIPAtom a = atoms[i]; @@ -2695,8 +2735,7 @@ if (a != null && !a.isDuplicate && !a.isTerminal) { a.priority = priorities[i]; ret1[0] = null; - String ssub = a.createRule4AuxiliaryData(node1 == null ? a : node1, - priorityPath + a.priority, ret1); + String ssub = a.createRule4AuxiliaryData(node1 == null ? a : node1, ret1); if (ret1[0] != null) { a.nextChiralBranch = ret1[0]; if (ret != null) @@ -2703,7 +2742,9 @@ ret[0] = ret1[0]; } rule4List[i] = ssub; - if (a.nextChiralBranch != null || isChiralSequence(ssub)) { + if (a.nextChiralBranch != null || + ssub.indexOf("R") >= 0 || ssub.indexOf("S") >= 0 + || ssub.indexOf("r") >= 0 || ssub.indexOf("s") >= 0) { mataList[nRS] = i; nRS++; subRS += ssub; @@ -2800,16 +2841,49 @@ if (!isBranch || adj == A_WINS || adj == B_WINS || adj == NOT_RELEVANT) { if (isAlkene) { if (!isBranch && alkeneChild != null) { - // must be alkeneParent + // must be alkeneParent -- first C of an alkene -- this is where C/T is recorded boolean isSeqCT = (ret != null && ret[0] == alkeneChild); // All odd cumulenes need to be checked. - // If it is an alkene or even cumulene, we must do an auxiliary check on this only - // if it is not already a defined stereochemistry, because in that - // case we have a simple E or Z, and there is no need to check AND - // it does not contribute to the Mata sequence (so would mess it up). - if (!isEvenEne || auxEZ == STEREO_BOTH_EZ + // If it is an alkene or even cumulene, we must do an auxiliary check + // only if it is not already a defined stereochemistry, because in that + // case we have a simple E/Z (c/t), and there is no need to check AND + // it does not contribute to the Mata sequence (similar to r/s or m/p). + // + if (!isEvenEne || (auxEZ == STEREO_BOTH_EZ || auxEZ == STEREO_UNDETERMINED) && alkeneChild.bondCount >= 2 && !isKekuleAmbiguous) { - rs = getEneWinnerChirality(this, alkeneChild, RULE_5, !isEvenEne); + int[] maxRules = new int[] { RULE_5, RULE_5 }; + rs = getEneWinnerChirality(this, alkeneChild, !isEvenEne, maxRules); + // + // Now we must check maxRules. If [5,5], then we have + // + // R R' + // \ / + // == + // / \ + // S S' + // + // planar flip is unchanged, and this is c/t (ignored here) + // + // + // R R + // \ / + // == + // / \ + // S S + // + // planar flip is unchanged; also c/t (ignored here) + // + // But note that we can have C/T (rule4Type = R/S) + // + // R x + // \ / + // == + // / \ + // S root + // + // flips sense upon planar inversion; determination was Rule 5. + // + boolean ignoreCT = (isEvenEne && maxRules[0] == RULE_5 && maxRules[1] == RULE_5); switch (rs) { case STEREO_M: rs = STEREO_R; @@ -2821,16 +2895,17 @@ break; case STEREO_Z: rs = STEREO_R; - s = "R"; + s = (ignoreCT ? "r" : "R"); break; case STEREO_E: rs = STEREO_S; - s = "S"; + s = (ignoreCT ? "s" : "S"); break; } if (rs != NO_CHIRALITY) { auxChirality = s; - addMataRef(priorityPath, rs); + if (!ignoreCT) + rule4Type = rs; subRS = ""; if (isSeqCT) { nextChiralBranch = alkeneChild; @@ -2849,7 +2924,7 @@ if (isBranch && adj != NOT_RELEVANT) { // if here, adj is A_WINS (-1), or B_WINS (1) // we check based on A winning, but then reverse it if B actually won - switch (checkPseudoHandedness(mataList, null)) { + switch (checkPseudoHandedness(mataList)) { case STEREO_R: s = (adj == A_WINS ? "r" : "s"); break; @@ -2859,7 +2934,7 @@ } if (noPseudo) { s = s.toUpperCase(); // Rule 4c or diasteriomers // AY-236.148 - parent.addMataRef(priorityPath, s.equals("R") ? STEREO_R: STEREO_S); + rule4Type = (s.equals("R") ? STEREO_R : STEREO_S); } subRS = ""; //if (ret != null) @@ -2869,17 +2944,16 @@ CIPAtom atom1 = (CIPAtom) clone(); if (atom1.setNode()) { atom1.addReturnPath(null, this); + atom1.rule4List = rule4List; int rule = atom1.sortToRule(RULE_5); if (rule != TIED) { rs = atom1.checkHandedness(); s = (rs == STEREO_R ? "R" : rs == STEREO_S ? "S" : "~"); - if (atom1.isPseudo) { + if (rule == RULE_5) { s = s.toLowerCase(); } else { - parent.addMataRef(priorityPath, rs); + rule4Type = rs; } - if (Logger.debugging) - Logger.info("AUX " + s + " for " + atom1.myPath); } } } @@ -2887,9 +2961,11 @@ } } } + if (node1 == null) + rule4Type = nRS; s += subRS; if (Logger.debugging && !s.equals("~")) - Logger.info("creating aux " + myPath + s); + Logger.info("creating aux " + s + " fofr " + this + " = " + myPath); return s; } @@ -2897,7 +2973,7 @@ * Sort by a given rule, preserving root.canBePseudo and currentRule. * * @param rule - * @return true if a decision has bee made + * @return true if a decision has been made */ private boolean sortByRule(int rule) { boolean rootPseudo = root.canBePseudo; @@ -2917,40 +2993,33 @@ */ private int sortToRule(int maxRule) { for (int i = RULE_1a; i <= maxRule; i++) - if (sortByRule(i)) { - return (isPseudo ? RULE_5 : i); - } + if (sortByRule(i)) + return i; return TIED; } - private boolean isChiralSequence(String ssub) { - return ssub.indexOf("R") >= 0 || ssub.indexOf("S") >= 0 - || ssub.indexOf("r") >= 0 || ssub.indexOf("s") >= 0; - } - /** * Accumlate the number of R and S centers of a given cumlative priority * - * @param priorityPath - * @param rs + * @param rule4Counts */ - private void addMataRef(String priorityPath, int rs) { - if (rule4Count == null) - rule4Count = new Object[] { "5", zero, zero }; - switch (sign(priorityPath.compareTo(rule4Count[0].toString()))) { + private void addMataRef(Object[] rule4Counts) { + int val = sign(priorityPath.length() - (rule4Counts[0] == null ? 10000 : ((String) rule4Counts[0]).length())); + if (val == 0) + val = sign(priorityPath.compareTo(rule4Counts[0].toString())); + switch (val) { case -1: - rule4Count[0] = priorityPath; - rule4Count[STEREO_R] = rule4Count[STEREO_S] = zero; + rule4Counts[0] = priorityPath; + rule4Counts[STEREO_R] = rule4Counts[STEREO_S] = zero; + rule4Counts[3] = Integer.valueOf(sphere); //$FALL-THROUGH$ case 0: - rule4Count[rs] = Integer.valueOf(((Integer) rule4Count[rs]).intValue() + 1); + rule4Counts[rule4Type] = Integer.valueOf(((Integer) rule4Counts[rule4Type]).intValue() + 1); break; } if (Logger.debugging) - Logger.info(this + " addMata " + sphere + " " + priority + " " + rs - + " " + PT.toJSON("rule4Count", rule4Count)); // Logger - if (parent != null) - parent.addMataRef(priorityPath, rs); + Logger.info(this + " addMata " + sphere + " " + priority + " " + rule4Type + + " " + PT.toJSON("rule4Count", rule4Counts)); // Logger } /** @@ -2959,8 +3028,7 @@ * @param i1 * @param i2 * @return NOT_RELEVANT if there is no stereochemistry, TIED if they are - * equal, A_WINS for enantiomer Rxxx, B_WINS for Sxxxx, or - * DIASTERIOMERIC_A_WINS or DIASTERIOMERIC_B_WINS + * equal, A_WINS for enantiomer Rxxx, B_WINS for Sxxxx, or IGNORE */ private int compareRule4Isomers(int i1, int i2) { String rs1 = rule4List[i1], rs2 = rule4List[i2]; @@ -2967,8 +3035,8 @@ if (priorities[i1] != priorities[i2] || atoms[i1].nextChiralBranch != null) return NOT_RELEVANT; int n = rs1.length(); -// if (n != rs2.length()) -// return NOT_RELEVANT; // not possible? + if (n != rs2.length()) + return NOT_RELEVANT; // not possible? if (rs1.equals(rs2)) { @@ -3037,13 +3105,12 @@ * Reverse the path to the parent and check r/s chirality * * @param iab - * @param indices * @return STEREO_R or STEREO_S * */ - private int checkPseudoHandedness(int[] iab, int[] indices) { - int ia = (indices == null ? iab[0] : indices[iab[0]]); - int ib = (indices == null ? iab[1] : indices[iab[1]]); + private int checkPseudoHandedness(int[] iab) { + int ia = iab[0]; + int ib = iab[1]; CIPAtom atom1; // critical here that we do NOT include the tied branches atom1 = (CIPAtom) clone(); @@ -3074,12 +3141,12 @@ * @return highest-priority non-duplicated atom */ private CIPAtom getTopSorted(int rule) { - sortByRule(rule); - for (int i = 0; i < 4; i++) { - CIPAtom a = atoms[i]; - if (!a.sp2Duplicate) - return priorities[i] == priorities[i + 1] ? null : atoms[i]; - } + if (sortByRule(rule)) + for (int i = 0; i < 4; i++) { + CIPAtom a = atoms[i]; + if (!a.sp2Duplicate) + return priorities[i] == priorities[i + 1] ? null : atoms[i]; + } return null; } @@ -3141,13 +3208,16 @@ a.atoms = new CIPAtom[4]; a.priorities = new int[4]; a.htPathPoints = htPathPoints; - a.doCheckPseudo = false; a.alkeneParent = null; + a.rule4Count = null; + a.rule4List = null; + a.rule4Paths = null; + a.priority = a.rule4Type = 0; a.auxChirality = "~"; + a.auxEZ = STEREO_UNDETERMINED; for (int i = 0; i < 4; i++) if (atoms[i] != null) a.atoms[i] = atoms[i]; - a.ties = null; return a; } Modified: trunk/Jmol/src/org/jmol/symmetry/Symmetry.java =================================================================== --- trunk/Jmol/src/org/jmol/symmetry/Symmetry.java 2017-06-28 22:45:07 UTC (rev 21645) +++ trunk/Jmol/src/org/jmol/symmetry/Symmetry.java 2017-07-03 13:12:40 UTC (rev 21646) @@ -45,6 +45,7 @@ import org.jmol.java.BS; import org.jmol.modelset.Atom; import org.jmol.modelset.ModelSet; +import org.jmol.script.T; import org.jmol.util.Escape; import org.jmol.util.JmolMolecule; import org.jmol.util.Logger; @@ -767,6 +768,7 @@ public void calculateCIPChiralityForAtoms(Viewer vwr, BS bsAtoms) { CIPChirality cip = getCIPChirality(vwr); BS bsAtropisomer = null, bsHelixM = null, bsHelixP = null; + boolean setAuxiliary = vwr.getBoolean(T.testflag1); try { // four ortho groups required: bsAtropisomer = vwr.getSmartsMatch("[!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); // three ortho groups required: bsAtropisomer = vwr.getSmartsMatch("[!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); @@ -777,7 +779,7 @@ } catch (Exception e) { // ignore } - cip.getChiralityForAtoms(vwr.ms.at, bsAtoms, bsAtropisomer, bsHelixM, bsHelixP); + cip.getChiralityForAtoms(vwr.ms.at, bsAtoms, bsAtropisomer, bsHelixM, bsHelixP, setAuxiliary); } CIPChirality cip; Modified: trunk/Jmol/src/org/jmol/viewer/Jmol.properties =================================================================== --- trunk/Jmol/src/org/jmol/viewer/Jmol.properties 2017-06-28 22:45:07 UTC (rev 21645) +++ trunk/Jmol/src/org/jmol/viewer/Jmol.properties 2017-07-03 13:12:40 UTC (rev 21646) @@ -63,7 +63,7 @@ -- Apache license bug fix: SMILES generator can show wrong @/@@ or stereochemical type for some allenes and cumulenes -bug fix: CIPChirality Rule 4b fixes +bug fix: CIPChirality additional Rule 4b and 4c fixes JmolVersion="14.19.1" // 2017.06.25 This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, Slashdot.org! http://sdm.link/slashdot _______________________________________________ Jmol-commits mailing list Jmol-commits@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/jmol-commits