Revision: 21590 http://sourceforge.net/p/jmol/code/21590 Author: hansonr Date: 2017-05-11 14:53:18 +0000 (Thu, 11 May 2017) Log Message: ----------- CIP update including AY-236.171 (adds M/P and secCis/secTrans in Rule 4b) 1018 lines
Modified Paths: -------------- trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java Modified: trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java =================================================================== --- trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java 2017-05-09 21:41:02 UTC (rev 21589) +++ trunk/Jmol/src/org/jmol/symmetry/CIPChirality.java 2017-05-11 14:53:18 UTC (rev 21590) @@ -92,7 +92,47 @@ * @author Bob Hanson hans...@stolaf.edu */ public class CIPChirality { + +//The rules: +// +// P-92.1.3.1 Sequence Rule 1 has two parts: +// +// (a) Higher atomic number precedes lower; +// (b) A duplicate atom node whose corresponding nonduplicated atom +// node is the root or is closer to the root ranks higher than a +// duplicate atom node whose corresponding nonduplicated node is +// farther from the root. +// +// P-92.1.3.2 Sequence Rule 2 +// +// Higher atomic mass number precedes lower; +// +// P-92.1.3.3 Sequence Rule 3 +// +// When considering double bonds and planar tetraligand atoms, 'seqcis' = 'Z' +// precedes 'seqtrans' = 'E' and this precedes nonstereogenic double bonds. +// +// P-92.1.3.4 Sequence Rule 4 is best considered in three parts: +// +// (a) Chiral stereogenic units precede pseudoasymmetric stereogenic units and +// these precede nonstereogenic units. +// (b) When two ligands have different descriptor pairs, then the one with the +// first chosen like descriptor pairs has priority over the one with a +// corresponding unlike descriptor pair. +// (i) Like descriptor pairs are: 'RR', 'SS', 'MM', 'PP', 'RM', 'SP', +// 'seqCis/seqCis', 'seqTran/sseqTrans', 'RseqCis', +// 'SseqTrans', 'MseqCis', 'PseqTrans' ...; +// (ii) Unlike discriptor pairs are 'RS', 'MP', 'RP', 'SM', +// 'seqCis/seqTrans', 'RseqTrans', 'SseqCis', 'PseqCis', +// 'MseqTrans'.... +// (c) 'r' precedes 's' and 'm' precedes 'p' +// +// P-92.1.3.5 Sequence Rule 5 +// +// An atom or group with descriptor 'R', 'M' and 'seqCis' has priority over its +// enantiomorph 'S', 'P' or 'seqTrans', 'seqCis' or 'seqTrans'. + // "Scoring" a vs. b involves returning 0 (TIE) or +/-n, where n>0 indicates b won, n < 0 // indicates a won, and the |a| indicates in which shell the decision was made. // The basic strategy is to loop through the Sequential Rules 1-5, including all parts @@ -109,7 +149,7 @@ // Rule 4c r precedes s // Rule 5 R precedes S; M precedes P (final determination of pseudoasymmetry descriptors) // - // Some nuances I've learned along the way here: + // Some nuances I've learned along the way here, some of which are still being checked: // // 1. Rule 1a requires a definition of aromaticity -- harder than you might think! // 2. Rule 1b had to be revised to account for Kekule bias (AY-236.215). Note that this @@ -292,7 +332,7 @@ * needed for Jmol's Rule 1b addition * */ - BS bsAromatic; + BS bsKekuleAmbiguous; /** * used to determine whether N is potentially chiral - could do this here, of course.... @@ -316,6 +356,7 @@ ptID = 0; nPriorityMax = 0; lstSmallRings.clear(); + bsKekuleAmbiguous = null; } /** @@ -341,7 +382,7 @@ lstSmallRings = new Lst<BS>(); while (!bs.isEmpty()) getSmallRings(atoms[bs.nextSetBit(0)], bs); - bsAromatic = getAromaticity(atoms); + bsKekuleAmbiguous = getKekule(atoms); bsAzacyclic = getAzacyclic(atoms, bsAtoms); BS bsToDo = BSUtil.copy(bsAtoms); @@ -398,7 +439,7 @@ clearSmallRingEZ(atoms, lstEZ); } - System.out.println("sp2-aromatic = " + bsAromatic); + System.out.println("sp2-aromatic = " + bsKekuleAmbiguous); System.out.println("smallRings = " + PT.toJSON(null,lstSmallRings)); } @@ -408,7 +449,7 @@ for (int i = bsAtoms.nextSetBit(0); i >= 0; i = bsAtoms.nextSetBit(i + 1)) { Node atom = atoms[i]; if (atom.getElementNumber() != 7 || atom.getCovalentBondCount() != 3 - || bsAromatic != null && bsAromatic.get(i)) + || bsKekuleAmbiguous.get(i)) continue; // bridgehead N must be in two rings that have at least three atoms in common. Lst<BS> nRings = new Lst<BS>(); @@ -597,30 +638,52 @@ /** - * Just using a simple aromatic definition involving contiguous sp2 centers. - * No electron counting. + * Just six-membered rings with three internal pi bonds or fused rings + * such as naphthalene or anthracene. Obviously, this is + * not a full-fledged Kekule check, but it will have to do. + * * @param atoms - * @return bsAromatic + * @return bsKekuleAmbiguous */ - private BS getAromaticity(Node[] atoms) { - BS bsAromatic = new BS(); - for (int i = lstSmallRings.size(); --i >= 0;) { + private BS getKekule(Node[] atoms) { + BS bs = new BS(); + int nRings = lstSmallRings.size(); + BS bsDone = new BS(); + for (int i = nRings; --i >= 0;) { + if (bsDone.get(i)) + continue; BS bsRing = lstSmallRings.get(i); - boolean isAromatic = true; + if (bsRing.cardinality() != 6) { + bsDone.set(i); + continue; + } + int nPI = 0; for (int j = bsRing.nextSetBit(0); j >= 0; j = bsRing.nextSetBit(j + 1)) { - switch(atoms[j].getCovalentBondCount()) { - case 2: - case 3: + Node a = atoms[j]; + if (bs.get(a.getIndex())) { + nPI++; continue; - default: - isAromatic = false; - break; + } + if (a.getCovalentBondCount() == 3) { + Edge[] bonds = a.getEdges(); + for (int k = bonds.length; --k >= 0;) { + Edge b = bonds[k]; + if (b.getCovalentOrder() != 2) + continue; + if (bsRing.get(b.getOtherAtomNode(a).getIndex())) { + nPI++; + break; + } + } } } - if (isAromatic) - bsAromatic.or(bsRing); + if (nPI == 6) { + bs.or(bsRing); + bsDone.set(i); + i = nRings; + } } - return (bsAromatic.isEmpty() ? null : bsAromatic); + return bs; } @@ -854,7 +917,7 @@ } else { atom = cipAtom.atom; isAlkene = cipAtom.isAlkene; - } + } root = cipAtom; cipAtom.parent = parent; if (parent != null) @@ -868,7 +931,7 @@ // System.out.println("rule 1b"); if (currentRule == RULE_4) { //cipAtom.resetAuxiliaryChirality();// was resetting E/Z - cipAtom.createAuxiliaryRSCenters(null, null); + cipAtom.createAuxiliaryRule4Paths(null, null); } isChiral = false; @@ -956,23 +1019,11 @@ */ private int getAxialOrEZChirality(Node a, Node pa, Node pb, Node b, boolean isAxial, int ruleMax) { CIPAtom a1 = new CIPAtom().create(a, null, false, true); - CIPAtom b1 = new CIPAtom().create(pa, null, false, true); - a1.canBePseudo = a1.isOddCumulene = isAxial; - int atop = getAtomChiralityLimited(a, a1, b1, ruleMax) - 1; - CIPAtom a2 = new CIPAtom().create(pb, null, false, true); + int atop = getAlkeneEndTopPriority(a1, pa, isAxial, ruleMax); CIPAtom b2 = new CIPAtom().create(b, null, false, true); - b2.canBePseudo = b2.isOddCumulene = isAxial; - int btop = getAtomChiralityLimited(b, b2, a2, ruleMax) - 1; - int c = NO_CHIRALITY; - if (atop >= 0 && btop >= 0) { - if (isAxial) { - c = (isPos(b2.atoms[btop], b2, a1, a1.atoms[atop]) ? STEREO_P : STEREO_M); - if ((a1.ties == null) != (b2.ties == null)) - c |= JC.CIP_CHIRALITY_PSEUDO_FLAG; - } else { - c = (isCis(b2.atoms[btop], b2, a1, a1.atoms[atop]) ? STEREO_Z : STEREO_E); - } - } + int btop = getAlkeneEndTopPriority(b2, pb, isAxial, ruleMax); + int c = (atop >= 0 && btop >= 0 ? + getEneChirality(b2.atoms[btop], b2, a1, a1.atoms[atop], isAxial, true) : NO_CHIRALITY); if (c != NO_CHIRALITY && (isAxial || !isAtropisomeric(a) && !isAtropisomeric(b))) { a.setCIPChirality(c); b.setCIPChirality(c); @@ -982,6 +1033,27 @@ return c; } + int getEneChirality(CIPAtom top1, CIPAtom end1, CIPAtom end2, CIPAtom top2, + boolean isAxial, boolean allowPseudo) { + if (top1 == null || top2 == null || top1.atom == null || top2.atom == null) + return NO_CHIRALITY; + int c; + if (isAxial) { + c = (isPos(top1, end1, end2, top2) ? STEREO_P : STEREO_M); + if (allowPseudo && (end2.ties == null) != (end1.ties == null)) + c |= JC.CIP_CHIRALITY_PSEUDO_FLAG; + } else { + c = (isCis(top1, end1, end2, top2) ? STEREO_Z : STEREO_E); + } + return c; + } + + private int getAlkeneEndTopPriority(CIPAtom a1, Node pa, boolean isAxial, int ruleMax) { + CIPAtom b1 = new CIPAtom().create(pa, null, false, true); + a1.canBePseudo = a1.isOddCumulene = isAxial; + return getAtomChiralityLimited(a1.atom, a1, b1, ruleMax) - 1; + } + private boolean isAtropisomeric(Node a) { return bsAtropisomeric != null && bsAtropisomeric.get(a.getIndex()); } @@ -1215,18 +1287,6 @@ */ private int auxEZ = STEREO_UNDETERMINED; - /** - * the cloned reversed path back to Sphere 1 for alkenes and stereogenic atoms - * - */ - private CIPAtom auxParentReversed; - - /** - * the cloned non-tied set of atoms for determining if an enantiomorphic tie is rs or not - * - */ - private CIPAtom auxPseudo; - boolean canBePseudo = true; /** @@ -1321,7 +1381,7 @@ // those cases, the rootDistance should be the sphere of the parent, not the duplicated atom. // This shows up in AV-360#215. - isAromatic = (bsAromatic != null && bsAromatic.get(atomIndex)); + isAromatic = (bsKekuleAmbiguous != null && bsKekuleAmbiguous.get(atomIndex)); if (parent == null) { // original atom bsPath.set(atomIndex); @@ -1506,7 +1566,7 @@ } /** - * Deep-Sort the substituents of an atom. + * Deep-Sort the substituents of an atom. * */ void sortSubstituents() { @@ -1522,16 +1582,16 @@ } } - + if (Logger.debugging) { Logger.info(root + "---sortSubstituents---" + this); for (int i = 0; i < 4; i++) { // Logger - Logger.info(getRuleName() + ": " + this + "[" + i + "]=" + atoms[i].myPath + " " + Integer.toHexString(prevPriorities[i])); + Logger.info(getRuleName() + ": " + this + "[" + i + "]=" + + atoms[i].myPath + " " + Integer.toHexString(prevPriorities[i])); } Logger.info("---"); } - // if this is Rule 4 or 5, then we do a check of the forward-based stereochemical path boolean checkRule4List = (currentRule > RULE_3 && rule4List != null); for (int i = 0; i < 4; i++) { @@ -1538,15 +1598,17 @@ 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 - : prevPriorities[i] == prevPriorities[j] ? TIED - : prevPriorities[j] < prevPriorities[i] ? B_WINS : A_WINS); + : prevPriorities[i] == prevPriorities[j] ? TIED + : prevPriorities[j] < prevPriorities[i] ? B_WINS : A_WINS); // note that a.compareTo(b) also down-scores duplicated atoms relative to actual atoms. if (score == TIED) 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.info(dots() + "ordering " + this.id + "." + i + "." + j + + " " + this + "-" + a + " vs " + b + " = " + score); switch (score) { case IGNORE: // just increment the index and go on to the next rule -- no breaking of the tie @@ -1554,7 +1616,8 @@ 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 B_WINS: indices[i]++; @@ -1561,7 +1624,7 @@ priorities[i]++; if (Logger_debugHigh) Logger.info(dots() + this + "." + b + " B-beats " + a); - + break; case A_WINS: indices[j]++; @@ -1575,7 +1638,8 @@ case TIED: indices[i]++; if (Logger_debugHigh) - Logger.info(dots() + this + "." + b + " ends up with tie with " + a); + Logger.info(dots() + this + "." + b + " ends up with tie with " + + a); break; case B_WINS: indices[i]++; @@ -1598,7 +1662,7 @@ doCheckPseudo = false; if (ties == null) ties = new Lst<int[]>(); - ties.addLast(new int[]{ i, j }); + ties.addLast(new int[] { i, j }); } } } @@ -1618,12 +1682,7 @@ int pp = prevPriorities[i]; if (pp < 0) pp = 0; -// if (currentRule == RULE_1b) -// pp = atoms[i].getBasePriority(false) & ~PRIORITY_1b_MASK; -// if (currentRule == RULE_2) -// pp = atoms[i].getBasePriority(true) | (pp & PRIORITY_1b_MASK); -// else - pp |= (p << shift); + pp |= (p << shift); newPrevPriorities[pt] = pp; if (a.atom != null) bs.set(priorities[i]); @@ -1653,7 +1712,9 @@ if (Logger.debugging) { 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(prevPriorities[i]) + " new"); + Logger.info(dots() + myPath + "[" + i + "]=" + atoms[i] + " " + + priorities[i] + " " + Integer.toHexString(prevPriorities[i]) + + " new"); } Logger.info(dots() + "-------"); } @@ -1984,14 +2045,10 @@ //System.out.println("checking EZ for " + this); winner2 = getTopAtom(); if (winner2 != null) { - if (auxParentReversed == null) { if (Logger.debugging) Logger.info("reversing path for " + alkeneParent); atom1 = (CIPAtom) alkeneParent.clone(); atom1.addReturnPath(alkeneParent.nextSP2, alkeneParent); - } else { - atom1 = auxParentReversed; - } atom1.sortSubstituents(); winner1 = atom1.getTopAtom(); if (winner1 != null) { @@ -2372,17 +2429,10 @@ * * @return collective string, with setting of rule4List */ - String createAuxiliaryRSCenters(CIPAtom node1, CIPAtom[] ret) { + String createAuxiliaryRule4Paths(CIPAtom node1, CIPAtom[] ret) { - // still deciding when/if this next two bits are necessary. Only for root? + // still deciding when/if this next two bits are necessary. - if (auxParentReversed != null) - auxParentReversed.createAuxiliaryRSCenters(null, null); - if (auxPseudo != null) - auxPseudo.createAuxiliaryRSCenters(null, null); - - // - int rs = -1; String subRS = ""; String s = (node1 == null ? "" : "~"); @@ -2400,8 +2450,8 @@ if (a != null && !a.isDuplicate && !a.isTerminal) { a.priority = priorities[i]; ret1[0] = null; - String ssub = a.createAuxiliaryRSCenters(node1 == null ? a : node1, - ret1); + String ssub = a.createAuxiliaryRule4Paths( + node1 == null ? a : node1, ret1); if (ret1[0] != null) { a.nextChiralBranch = ret1[0]; if (ret != null) @@ -2427,9 +2477,8 @@ case 2: if (node1 != null) { // we want to now if these two are enantiomorphic, identical, or diastereomorphic. - if (root.atomIndex == 21) - System.out.println("testing21"); - adj = (compareRule4aEnantiomers(rule4List[mataList[0]], rule4List[mataList[1]])); + adj = (compareRule4aEnantiomers(rule4List[mataList[0]], + rule4List[mataList[1]])); switch (adj) { case DIASTEREOMERIC: isBranch = true; @@ -2473,12 +2522,53 @@ } if (!isBranch || adj == A_WINS || adj == B_WINS) { - if (isAlkene && alkeneChild != null) { - System.out.println("M/P seqCis Rule 4b not implemented!"); - // we must check for seqCis or M/P + if (isAlkene) { + if (isBranch) { + if (alkeneParent != null) { + // we have a branch that is one end of an alkene or cumulene + // and it has chirality on the two ends + } + } else if (alkeneChild != null) { + // must be alkeneParent. + // 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. + // All odd cumulenes need to be checked. + boolean isAxial = (((alkeneChild.sphere - sphere) % 2) == 0); + if (isAxial || atom.getCIPChiralityCode() == 3 && alkeneChild.bondCount >= 2 + && !isAromatic) { + // we need to line up the two paths + CIPAtom winner2 = getEneEndWinner(alkeneChild, + alkeneChild.parent); + CIPAtom winner1 = (winner2 == null || winner2.atom == null ? null : getEneEndWinner(this, nextSP2)); + rs = getEneChirality(winner1, this, alkeneChild, winner2, + isAxial, false); + switch (rs) { + case STEREO_M: + s = "R"; + rs = STEREO_R; + break; + case STEREO_P: + s = "S"; + rs = STEREO_S; + break; + case STEREO_Z: + s = "R"; + rs = STEREO_R; + break; + case STEREO_E: + s = "S"; + rs = STEREO_S; + break; + } + if (rs != NO_CHIRALITY) + addMataRef(sphere, priority, rs); + } + } } else if (node1 != null && (bondCount == 4 && nPriorities >= 3 - Math.abs(adj) || isTrigonalPyramidal - && nPriorities >= 2 - Math.abs(adj))) { + && nPriorities >= 2 - Math.abs(adj))) { if (isBranch) { // 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 @@ -2485,11 +2575,13 @@ switch (checkPseudoHandedness(mataList, null)) { case STEREO_R: s = (adj == A_WINS ? "r" : "s"); - System.out.println("for " + atoms[mataList[0]] + atoms[mataList[1]] + " adj=" + adj + "R->rs=" + s); +// System.out.println("for " + atoms[mataList[0]] +// + atoms[mataList[1]] + " adj=" + adj + "R->rs=" + s); break; case STEREO_S: s = (adj == A_WINS ? "s" : "r"); - System.out.println("for " + atoms[mataList[0]] + atoms[mataList[1]] + " adj=" + adj + "S->rs=" + s); +// System.out.println("for " + atoms[mataList[0]] +// + atoms[mataList[1]] + " adj=" + adj + "S->rs=" + s); break; } if (isrs) @@ -2502,10 +2594,7 @@ CIPAtom atom1 = (CIPAtom) clone(); if (atom1.set()) { atom1.addReturnPath(null, this); - int thisRule = currentRule; - currentRule = RULE_1a; - atom1.sortSubstituents(); - currentRule = thisRule; + atom1.sortByRule(RULE_1a); rs = atom1.checkHandedness(); s = (rs == STEREO_R ? "R" : rs == STEREO_S ? "S" : "~"); node1.addMataRef(sphere, priority, rs); @@ -2520,7 +2609,28 @@ return s; } + private CIPAtom getEneEndWinner(CIPAtom end, CIPAtom parent) { + CIPAtom atom1 = (CIPAtom) end.clone(); + atom1.addReturnPath(parent, atom1); + boolean rootPseudo = root.canBePseudo; + CIPAtom a = null; + for (int i = RULE_1a; i <= RULE_5; i++) { + atom1.sortByRule(i); + a = atom1.getTopAtom(); + if (a != null && a.atom != null) + break; + } + root.canBePseudo = rootPseudo; + return a; + } + private void sortByRule(int rule) { + int current = currentRule; + currentRule = rule; + sortSubstituents(); + currentRule = current; + } + private boolean isChiralSequence(String ssub) { return ssub.indexOf("R") >= 0 || ssub.indexOf("S") >= 0 || ssub.indexOf("r") >= 0 || ssub.indexOf("s") >= 0 @@ -2573,7 +2683,6 @@ private int checkEnantiomer(String rs1, String rs2, int m, int n, String rs) { int finalScore = TIED; - System.out.println("???"); // "0~~R 0~~S" for (int i = m; i < n; i++) { // a score of 0 means ~ was present for both @@ -2603,19 +2712,15 @@ int ia = (indices == null ? iab[0] : indices[iab[0]]); int ib = (indices == null ? iab[1] : indices[iab[1]]); CIPAtom atom1; - if (auxPseudo == null) { - // critical here that we do NOT include the tied branches - atom1 = (CIPAtom) clone(); - atom1.atoms[ia] = new CIPAtom().create(null, atom1, false, isAlkene); - atom1.atoms[ib] = new CIPAtom().create(null, atom1, false, isAlkene); - atom1.addReturnPath(null, this); - } else { - atom1 = auxPseudo; - } - int thisRule = currentRule; - currentRule = RULE_1a; - atom1.sortSubstituents(); - currentRule = thisRule; + // critical here that we do NOT include the tied branches + atom1 = (CIPAtom) clone(); + atom1.atoms[ia] = new CIPAtom().create(null, atom1, false, isAlkene); + atom1.atoms[ib] = new CIPAtom().create(null, atom1, false, isAlkene); + atom1.addReturnPath(null, this); + // We are guaranteed that only RULE_1a is necessary, because one of our + // paths goes all the way back to the root, without a duplicate atom, and any + // other path reaching that will terminate with a duplicate atom instead. + atom1.sortByRule(RULE_1a); // Now add the tied branches at the end; it doesn't matter where they // go as long as they are together and in order. atom1.atoms[bondCount - 2] = atoms[Math.min(ia, ib)]; @@ -2622,27 +2727,14 @@ atom1.atoms[bondCount - 1] = atoms[Math.max(ia, ib)]; int rs = atom1.checkHandedness(); if (Logger.debugging) { - for (int i = 0; i < 4; i++) // Logger - Logger.info("pseudo " + rs + " " + priorities[i] + " " + atoms[i].myPath); + for (int i = 0; i < 4; i++) + // Logger + Logger.info("pseudo " + rs + " " + priorities[i] + " " + + atoms[i].myPath); } return rs; } - -// /** -// * reset auxEZ chirality to "undetermined" -// */ -// void resetAuxiliaryChirality() { -// auxEZ = STEREO_UNDETERMINED; -// for (int i = 0; i < 4; i++) -// if (atoms[i] != null && atoms[i].atom != null) -// atoms[i].resetAuxiliaryChirality(); -// if (auxParentReversed != null) -// auxParentReversed.resetAuxiliaryChirality(); -// if (auxPseudo != null) -// auxPseudo.resetAuxiliaryChirality(); -// } - /** * Swap a substituent and the parent in preparation for reverse traversal of * this path 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