UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,433 lines (1,374 loc) 48.1 kB
/** * @fileoverview * Extension methods to perceive aromatic rings in molecule ctab. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /core/kekule.common.js * requires /core/kekule.structures.js * requires /utils/kekule.utils.js * requires /algorithm/kekule.structure.ringSearches.js */ (function(){ "use strict"; var OU = Kekule.ObjUtils; var AU = Kekule.ArrayUtils; var BT = Kekule.BondType; var BO = Kekule.BondOrder; var CU = Kekule.ChemStructureUtils; /** * Special markers of electron count of p orbit. * @enum * @private */ var PElectronCountMarkers = { SATURATED_CARBON: -1, ESTER_CARBON: -16, SULFONE_OR_SULFOXIDE_SULPHUR: -32 }; /** * Enumeration of aromatic detection types, aromatic(4n+2), antiaromatic(4n) or non aromatic * @enum */ Kekule.AromaticTypes = { /** Not an aromatic ring. */ NONAROMATIC: 0, /** An aromatic ring. */ EXPLICIT_AROMATIC: 1, /** An anti-aromatic ring. */ ANTIAROMATIC: -1, /** * Uncertain, maybe aromatic. * For example, there is a variable atom on ring so that the pi electron number can not be exactly calculated. */ UNCERTAIN: 64 }; ClassEx.extend(Kekule.Atom, /** @lends Kekule.Atom# */ { /** @private */ isSulfoneOrSulfoxideSulphur: function() { if (this.getSymbol() === 'S') { var mbonds = this.getLinkedMultipleBonds(); if (mbonds.length >= 1) { for (var i = 0, l = mbonds; i < l; ++i) { var connObjs = this.getLinkedObjsOnConnector(mbonds[i]); for (var j = 0, k = connObjs.length; j < k; ++j) { var obj = connObjs[j]; if ((obj instanceof Kekule.Atom) && (obj.getSymbol() === 'O')) return true; } } } } return false; }, /** @private */ isEsterCarbon: function() { if (this.getSymbol() === 'C') { var bonds = this.getLinkedBonds(BT.COVALENT); var foundDoubleO, foundSingleO; for (var i = 0, l = bonds.length; i < l; ++i) { var b = bonds[i]; if (b.isBondBetween('C', 'O')) { if (!foundSingleO && (b.getBondOrder() === BO.SINGLE)) foundSingleO = true; else if (!foundDoubleO && (b.getBondOrder() === BO.DOUBLE)) foundDoubleO = true; if (foundSingleO && foundDoubleO) return true; } } } return false; } }); /* * Default options to percept aromatic rings. * @object */ Kekule.globalOptions.add('algorithm.aromaticRingsPerception', { allowUncertainRings: false }); ClassEx.extend(Kekule.StructureConnectionTable, /** @lends Kekule.StructureConnectionTable# */ { /** * Returns Possible PI electron numbers of atom. * @param node * @param {Array} ctabRingNodes All nodes in cycle block. This array is used to help to find * if there is a out-of-ring double bond on node. * @param {Array} ctabRingConnectors All connectors in cycle block. This array is used to help to find * if there is a out-of-ring double bond on node. * @returns {Variant} A number when the pi number is exact or an array with all possible pi electron numbers when the electron count is uncertain. * @private */ _getPossibleRingNodePiElectronCounts: function(node, ctabRingNodes, ctabRingConnectors) { /** @ignore */ var _getRingElementPiElectronCount = function(elemSymbol, node, ctabRingNodes, ctabRingConnectors) { // TODO: charge on hetero atoms should be reconsidered var symbol = elemSymbol; var isotope = Kekule.IsotopeFactory.getIsotope(symbol); var charge = node.getCharge(); //var bonds = node.getLinkedBonds(BT.COVALENT); // now only consider covalent bonds //var implValence = node.getImplicitValence && node.getImplicitValence(); var isSaturated = node.isSaturated && node.isSaturated(); if (!isSaturated) // check if the multiple bond is on ring or outside ring { var baseECount; var multipleBonds = node.getLinkedMultipleBonds(); if (node.isSulfoneOrSulfoxideSulphur && node.isSulfoneOrSulfoxideSulphur()) return PElectronCountMarkers.SULFONE_OR_SULFOXIDE_SULPHUR; else if (node.isEsterCarbon && node.isEsterCarbon()) return PElectronCountMarkers.ESTER_CARBON; var multipleBondsOnRing, multipleBondOnRingCount; if (ctabRingConnectors) { multipleBondsOnRing = AU.intersect(multipleBonds, ctabRingConnectors); multipleBondOnRingCount = multipleBondsOnRing.length; } else // sometimes ctabRingConnectors are not set (e.g., in kekulize method), we suppose the multiple bond is on ring { multipleBondsOnRing = null; // flag multipleBondOnRingCount = 1; } if (multipleBondOnRingCount) // multiple bond on ring { if ((multipleBondOnRingCount === 2) && (multipleBondsOnRing[0].getBondOrder() === Kekule.BondOrder.EXPLICIT_AROMATIC)) { if (isotope.isHetero && isotope.isHetero()) // hetero atom on aromatic bond may provide 1 or 2 e { baseECount = [1, 2]; } else // C baseECount = 1; } else if ((multipleBondOnRingCount === 1) && (multipleBondsOnRing && multipleBondsOnRing[0].getBondOrder() === Kekule.BondOrder.DOUBLE)) baseECount = 1; else if ((multipleBondOnRingCount === 1) && !multipleBondsOnRing) // receive special flag baseECount = 1; else return -1; //0; // C=C=C or triple bond has no aromatic, returns directly } else // multiple bond outside ring { if (symbol === 'C') { var hasHetero = false; for (var i = 0, l = multipleBonds.length; i < l; ++i) { var bond = multipleBonds[i]; var linkedNodes = node.getLinkedObjsOnConnector(bond); if (linkedNodes.length === 1) { var n = linkedNodes[0]; var linkedIsotope = n.getPrimaryIsotope(); if (linkedIsotope.isHetero()) { hasHetero = true; break; } } } if (hasHetero) // C=O/N/S/P, C has no p electron baseECount = 0; else baseECount = 1; } else baseECount = 1; } // adjust with charge if (AU.isArray(baseECount)) { for (var i = 0, l = baseECount.length; i < l; ++i) { baseECount[i] = Math.min(Math.max(baseECount[i] - charge, 0), 2); } return baseECount; } else return Math.min(Math.max(baseECount - charge, 0), 2); // C=C(+), C+ has 0 pi e. } else // saturated { if (node.getRadical && (node.getRadical() === Kekule.RadicalOrder.DOUBLET)) return 1; else if (isotope.isHetero()) // saturated N/S/P/O..., pX2 { //return 2; return Math.min(Math.max(2 - charge, 0), 2); } else if (symbol === 'C') { if (charge > 0) // +1 return 0; else if (charge < 0) // -1 return 2; else // no charge { return PElectronCountMarkers.SATURATED_CARBON; } } } return 0; // default }; if (node instanceof Kekule.VariableAtom) { var isotopeIds = node.getAllowedIsotopeIds(); if (isotopeIds && isotopeIds.length) { var result = []; for (var i = 0, l = isotopeIds.length; i < l; ++i) { var isoId = isotopeIds[i]; var symbol = Kekule.IsotopeFactory.getIsotopeById(isoId).getSymbol(); AU.pushUnique(result, _getRingElementPiElectronCount(symbol, node, ctabRingNodes, ctabRingConnectors)); } /* result._isVar = true; console.log(result); */ return result; } else return [0, 1, 2]; } var isotope = node.getPrimaryIsotope(); if (!isotope) // isotope not certain, may be a subgroup or variable atom. return [0, 1, 2]; // returns all possible numbers else { var symbol = isotope.getSymbol(); return _getRingElementPiElectronCount(symbol, node, ctabRingNodes, ctabRingConnectors); } }, /** * Check if a ring is a aromatic one. * @param {Object} ring * @param {Kekule.MapEx} piECountMap * @param {Array} sssRings in this molecule. * @returns {Hash} A {result, eMap} Hash, where result is a value from {@link Kekule.AromaticTypes}, * while the eMap stores the possible aromatic pi electron count of each node in ring. * @private */ _checkRingAromaticType: function(ring, piECountMap, sssRings) { if (piECountMap) { var isInSSSR = sssRings && Kekule.ChemStructureUtils.isInRings(ring, sssRings); var nodes = ring.nodes; var nodeCount = nodes.length; var isMedRing = (nodeCount > 6 && nodeCount < 16); // med ring, usually has no aromatic var nodeECounts = []; var currIndexes = []; // form a counts array var totalCount = nodes.length; for (var i = 0; i < totalCount; ++i) { var n = nodes[i]; var counts = piECountMap.get(n); if (AU.isArray(counts)) // an array of all possible e counts { nodeECounts.push(counts); } else nodeECounts.push([counts]); currIndexes[i] = 0; } var incIndexesOnPos = function(pos, indexes) { var currValue = indexes[pos]; var newValue = ++currValue; if (newValue >= nodeECounts[pos].length) { if (pos >= indexes.length - 1) // highest pos, can not inc now return false; else { indexes[pos] = 0; return incIndexesOnPos(pos + 1, indexes); } } else { indexes[pos] = newValue; return true; } }; var nextIndexes = function(indexes) { return incIndexesOnPos(0, indexes); }; var finalResult; var lastResult = null; var tryCount = 0; var aromaticEIndexes = null; // loop and calculate all possible pi e sums do { ++tryCount; var eSum = 0; var currResult = null; // Kekule.AromaticTypes.NONAROMATIC; for (var i = 0; i < totalCount; ++i) { var eCount = nodeECounts[i][currIndexes[i]]; if (eCount >= 0) eSum += eCount; else // n < 0, not able to form aromatic ring { currResult = Kekule.AromaticTypes.NONAROMATIC; break; } } if (currResult === null) // not determinated { var times = parseInt(eSum / 4); var mod = eSum % 4; var isMed = isMedRing && isInSSSR; if ((times <= 5) && (times !== 2 || !isMed)) // times = 2, e.g. 10 carbon sssRing, not aromatic { if (mod === 2) { currResult = Kekule.AromaticTypes.EXPLICIT_AROMATIC; aromaticEIndexes = AU.clone(currIndexes); } else if (!mod) currResult = Kekule.AromaticTypes.ANTIAROMATIC; } } if (lastResult !== null) // check previous and curr result value { if (currResult && lastResult !== currResult) { if ((lastResult === Kekule.AromaticTypes.EXPLICIT_AROMATIC) || (currResult === Kekule.AromaticTypes.EXPLICIT_AROMATIC)) { finalResult = Kekule.AromaticTypes.UNCERTAIN; break; } } } else // lastResult not set lastResult = currResult; } while (nextIndexes(currIndexes)); if (!finalResult) finalResult = lastResult; if (aromaticEIndexes) // find aromatic electron set, returns it too { var eMap = new Kekule.MapEx(); for (var i = 0, l = aromaticEIndexes.length; i < l; ++i) { eMap.set(nodes[i], nodeECounts[i][aromaticEIndexes[i]]); } } //return lastResult; return {'result': finalResult, 'eMap': eMap}; /* var eSum = 0; for (var i = 0, l = nodes.length; i < l; ++i) { var n = nodes[i]; var eCounts = piECountMap.get(n); if (AU.isArray(eCounts)) // an array of all possible e counts { } else { var eCount = eCounts; if (eCount >= 0) { eSum += eCount; } else // n < 0, not able to form aromatic ring return Kekule.AromaticTypes.NONAROMATIC; } } var times = parseInt(eSum / 4); var mod = eSum % 4; if ((times <= 5) && (times !== 2)) // times = 2, 10 carbon ring, not aromatic { if (mod === 2) return Kekule.AromaticTypes.EXPLICIT_AROMATIC; else if (!mod) return Kekule.AromaticTypes.ANTIAROMATIC; } return Kekule.AromaticTypes.NONAROMATIC; */ } }, /** @private */ _calcPossibleRingNodesPElectronCounts: function(piECountMap, rings, refRings) { var allNodes = []; var allConnectors = []; var targetNodes = []; if (!refRings) { for (var i = 0, l = rings.length; i < l; ++i) { AU.pushUnique(allNodes, rings[i].nodes); AU.pushUnique(allConnectors, rings[i].connectors); } targetNodes = allNodes; } else { for (var i = 0, l = refRings.length; i < l; ++i) { AU.pushUnique(allNodes, refRings[i].nodes); AU.pushUnique(allConnectors, refRings[i].connectors); } for (var i = 0, l = rings.length; i < l; ++i) { AU.pushUnique(targetNodes, rings[i].nodes); } } for (var i = 0, l = targetNodes.length; i < l; ++i) { var node = targetNodes[i]; var cachedECount = node.getStructureCacheData('piElectronCount'); if (Kekule.ObjUtils.notUnset(cachedECount)) { piECountMap.set(node, [cachedECount]); } else { var eCounts = this._getPossibleRingNodePiElectronCounts(node, allNodes, allConnectors); piECountMap.set(node, eCounts); } //node.setCharge(eCount); // debug } }, _storeAromaticCacheToRingInfo: function(ring, aromaticType, piECountMap) { ring.aromaticType = aromaticType; if (aromaticType === Kekule.AromaticTypes.EXPLICIT_AROMATIC) { // store pi electron map to nodes in ring var nodes = ring.nodes; for (var i = 0, l = nodes.length; i < l; ++i) { var node = nodes[i]; if (Kekule.ObjUtils.isUnset(node.getStructureCacheData('piElectronCount'))) { if (piECountMap) { var eCount = piECountMap.get(node); if (Kekule.ObjUtils.notUnset(eCount)) { node.setStructureCacheData('piElectronCount', eCount); } } } } } }, /** * Perceive and all aromatic rings in ctab. * @param {Bool} allowUncertainRings Whether uncertain rings (e.g., with variable atom) be included in result. * @param {Array} candidateRings Rings in ctab that the detection will be performed. * If this param is not set, all memebers of SSSR of ctab will be checked. * @return {Array} Found aromatic rings. */ perceiveAromaticRings: function(allowUncertainRings, candidateRings) { if (Kekule.ObjUtils.isUnset(allowUncertainRings)) allowUncertainRings = Kekule.globalOptions.algorithm.aromaticRingsPerception.allowUncertainRings; // TODO: need to detect azulene and some other special aromatic rings var sssRings = this.findSSSR(); var rings = candidateRings || sssRings; var result = []; /* // mark all pi electron number of all nodes in rings var allNodes = []; var allConnectors = []; for (var i = 0, l = rings.length; i < l; ++i) { AU.pushUnique(allNodes, rings[i].nodes); AU.pushUnique(allConnectors, rings[i].connectors); } */ var piECountMap = new Kekule.MapEx(); try { /* for (var i = 0, l = allNodes.length; i < l; ++i) { var node = allNodes[i]; var eCount = this._getPossibleRingNodePiElectronCounts(node, allNodes, allConnectors); piECountMap.set(node, eCount); //node.setCharge(eCount); // debug } */ this._calcPossibleRingNodesPElectronCounts(piECountMap, rings, null); // calc pi e count of rings for (var i = 0, l = rings.length; i < l; ++i) { var ring = rings[i]; var aromaticType; if (Kekule.ObjUtils.notUnset(ring.aromaticType)) aromaticType = ring.aromaticType; else { var checkResult = this._checkRingAromaticType(ring, piECountMap, sssRings); aromaticType = checkResult.result; //if (aromaticType === Kekule.AromaticTypes.EXPLICIT_AROMATIC) this._storeAromaticCacheToRingInfo(ring, aromaticType, checkResult.eMap); } if ((aromaticType === Kekule.AromaticTypes.EXPLICIT_AROMATIC) || (allowUncertainRings && (aromaticType === Kekule.AromaticTypes.UNCERTAIN))) { result.push(ring); } } return result; } finally { piECountMap.finalize(); } }, /** * Perceive and all aromatic rings in ctab, same as method perceiveAromaticRings. * @param {Bool} allowUncertainRings Whether uncertain rings (e.g., with variable atom) be included in result. * @param {Array} candidateRings Rings in ctab that the detection will be performed. * If this param is not set, all memebers of SSSR of ctab will be checked. * @return {Array} Found aromatic rings. */ findAromaticRings: function(allowUncertainRings, candidateRings) { return this.perceiveAromaticRings(allowUncertainRings, candidateRings); }, /** * Returns aromatic type of a ring. * @param {Object} ring * @param {Array} refRings Should list all related rings to ring, help to determine the p electron number. * If this value is not set, SSSR of ctab will be used instead. * @return {Int} Value from {@link Kekule.AromaticTypes}. */ getRingAromaticType: function(ring, refRings) { // restore from cache first if (Kekule.ObjUtils.notUnset(ring.aromaticType)) return ring.aromaticType; var sssRings = this.findSSSR(); if (!refRings) refRings = sssRings; var result; var piECountMap = new Kekule.MapEx(); try { this._calcPossibleRingNodesPElectronCounts(piECountMap, [ring], refRings); var checkResult = this._checkRingAromaticType(ring, piECountMap, sssRings); result = checkResult.result; this._storeAromaticCacheToRingInfo(ring, result, checkResult.eMap); } finally { piECountMap.finalize(); } return result; }, /** * Returns the bond order changes that need to be done on aromatic rings when do a hucklization. * @param {Array} targetBonds Optional, the target bonds. If this param is not set, all aromatic rings in connection table will be handled. * @param {Hash} options Option object, can include fields: { * allowUncertainRings: Whether uncertain rings (e.g., with variable atom) be included in result. Default is false. * }. * @returns {Array} Each item is a hash of {bond, (new)bondOrder (always be explicit aromatic)}. */ getHucklizationChanges: function(targetBonds, options) { var allowUncertainRings = options && options.allowUncertainRings; var BO = Kekule.BondOrder; var result = []; var mol = this.getParent(); var aromaticRings = this.findAromaticRings(allowUncertainRings); for (var i = 0, l = aromaticRings.length; i < l; ++i) { var bonds = aromaticRings[i].connectors; for (var j = 0, k = bonds.length; j < k; ++j) { var bond = bonds[j]; if (!targetBonds || targetBonds.indexOf(bond) >= 0) { var currOrder = bond.getBondOrder && bond.getBondOrder(); if (currOrder === BO.SINGLE || currOrder === BO.DOUBLE) // triple bond will not be affected { if (bond.setBondOrder) { result.push({'bond': bond, 'bondOrder': Kekule.BondOrder.EXPLICIT_AROMATIC}); } } } } } return result; }, /** * Set the orders of Kekule form bonds (single/double bonds) in aromatic rings to {@link Kekule.BondOrder.EXPLICIT_AROMATIC}. * @param {Array} targetBonds Optional, the target bonds. If this param is not set, all aromatic rings in connection table will be handled. * @param {Hash} options Option object, can include fields: { * allowUncertainRings: Whether uncertain rings (e.g., with variable atom) be included in result. Default is false. * }. * @return {Array} Hucklized bonds. */ hucklize: function(targetBonds, options) { var allowUncertainRings = options && options.allowUncertainRings; var BO = Kekule.BondOrder; var result = []; var mol = this.getParent(); mol.setAutoClearStructureCache(false); // the structure after hucklization should be the same with current one, so cache need not to be cleared mol.beginUpdate(); try { /* var aromaticRings = this.findAromaticRings(allowUncertainRings); for (var i = 0, l = aromaticRings.length; i < l; ++i) { var bonds = aromaticRings[i].connectors; for (var j = 0, k = bonds.length; j < k; ++j) { var bond = bonds[j]; if (!targetBonds || targetBonds.indexOf(bond) >= 0) { var currOrder = bond.getBondOrder && bond.getBondOrder(); if (currOrder === BO.SINGLE || currOrder === BO.DOUBLE) // triple bond will not be affected { if (bond.setBondOrder) { bond.setBondOrder(Kekule.BondOrder.EXPLICIT_AROMATIC); result.push(bond); } } } } } */ var changes = this.getHucklizationChanges(targetBonds, options); if (changes) { for (var i = 0, l = changes.length; i < l; ++i) { var bond = changes[i].bond; var order = changes[i].bondOrder; if (bond.setBondOrder) { bond.setBondOrder(order); result.push(bond); } } } } finally { mol.endUpdate(); mol.setAutoClearStructureCache(true); } return result; }, /** * Returns the bond order changes that need to be done on explicit aromatic bonds. * @param {Array} targetBonds Target explicit aromatic bonds. If not set, all aromatic bonds in molecule will be handled. * @param {Hash} options Options for kekulization process. It may include the following fields: * { * doAromaticTests: Whether do an aromatic ring perception to determinate the pi electron count before carrying out the calculation. Default is false. * includingSubgroups: Whether do kekulization on sub groups too. Default is true. * expandSubFragments: Whether expand all sub structures before kekulization. * useShadow: Whether use a shadow fragment for calculating the changes to avoid affect current molecule. Default is true. * Note: if not using shadow, the bond modifications will be applied directly to structure, rather than record them. * } * @returns {Array} Each item is a hash of {bond, (new)bondOrder}. */ getKekulizationChanges: function(targetBonds, options) { var ops = Object.extend({ doAromaticTests: false, includingSubFragments: true, expandSubFragments: false, useShadow: true }, options || {}); var result = []; var mol = this.getParent(); if (ops.doAromaticTests) this.perceiveAromaticRings(); var targetFragment; if (ops.useShadow) { var shadow = mol.createShadow(); targetFragment = shadow.getShadowFragment(); } else targetFragment = mol; try { if (ops.expandSubFragments && ops.useShadow) targetFragment.unmarshalAllSubFragments(true); // perform on the flattened shadow // map target bonds to shadow first var actualTargetBonds; if (targetBonds) { actualTargetBonds = []; if (ops.useShadow) { for (var i = 0, l = targetBonds.length; i < l; ++i) { var shadowBond = shadow.getShadowObj(targetBonds[i]); if (shadowBond) actualTargetBonds.push(shadowBond); } } else actualTargetBonds = targetBonds; } else actualTargetBonds = (ops.includingSubFragments && !ops.expandSubFragments)? targetFragment.getAllContainingConnectors(): targetFragment.getConnectors(); // filter out explicit aromatic ones var explicitAromaticBonds = []; for (var i = 0, l = actualTargetBonds.length; i < l; ++i) { var b = actualTargetBonds[i]; if (b.getBondType() === Kekule.BondType.COVALENT && b.getBondOrder() === Kekule.BondOrder.EXPLICIT_AROMATIC && b.getConnectedChemNodeCount() === 2) explicitAromaticBonds.push(b); } targetFragment.setAutoClearStructureCache(false); // the structure after kekulization should be the same with current one, so cache need not to be cleared targetFragment.beginUpdate(); try { // split aromatic bonds to unconnected parts var parts = this._splitConnectorsToContinuousParts(explicitAromaticBonds) || []; // handle each parts var partResult; for (var i = 0, l = parts.length; i < l; ++i) { var part = parts[i]; partResult = this._kekulizeContinousBonds(part, shadow, targetFragment, mol); if (partResult) // some bonds need to be modified { for (var j = 0, k = part.length; j < k; ++j) { var newBondOrder = part[j].getBondOrder(); if (newBondOrder !== BO.EXPLICIT_AROMATIC) { var originalBond = ops.useShadow? shadow.getSourceObj(part[j]): part[j]; result.push({'bond': originalBond, 'bondOrder': newBondOrder}); } } } } } finally { targetFragment.endUpdate(); targetFragment.setAutoClearStructureCache(true); } } finally { if (ops.useShadow) { shadow.finalize(); //targetFragment.finalize(); } } result._useShadow = ops.useShadow; // a special flag field return result; }, /** * Set the orders of Kekule form bonds (single/double bonds) in aromatic rings to {@link Kekule.BondOrder.EXPLICIT_AROMATIC}. * @param {Array} targetBonds Target explicit aromatic bonds. If not set, all aromatic bonds in molecule will be handled. * @param {Hash} options Options for kekulization process. It may include the following fields: * { * doAromaticTests: Whether do an aromatic ring perception to determinate the pi electron count before carrying out the calculation. Default is false. * includingSubgroups: Whether do kekulization on sub groups too. Default is true. * expandSubFragments: Whether expand all sub structures before kekulization. * useShadow: Whether use a shadow fragment for calculating the changes to avoid affect current molecule. Default is true. * Note: if not using shadow, the bond modifications will be applied directly to structure, rather than record them. * } * @return {Array} Changed bonds. */ kekulize: function(targetBonds, options) { var result = []; var changes = this.getKekulizationChanges(targetBonds, options); if (changes && changes._useShadow) { var mol = this.getParent(); try { mol.setAutoClearStructureCache(false); // the structure after kekulization should be the same with current one, so cache need not to be cleared mol.beginUpdate(); for (var i = 0, l = changes.length; i < l; ++i) { var change = changes[i]; change.bond.setBondOrder(change.bondOrder); result.push(change.bond); } } finally { mol.endUpdate(); mol.setAutoClearStructureCache(true); } } return result; }, /** @private */ _kekulizeContinousBonds: function(targetBonds, shadow, shadowMol, originalMol) { // get all related nodes first var targetNodes = []; for (var i = 0, l = targetBonds.length; i < l; ++i) { var connNodes = targetBonds[i].getConnectedChemNodes() || []; AU.pushUnique(targetNodes, connNodes); } var determinatedMap = new Kekule.MapEx(false); // if original nodes has pi e information, stores it for (var i = 0, l = targetNodes.length; i < l; ++i) { var shadowNode = targetNodes[i]; var originalNode = shadow? shadow.getSourceObj(shadowNode): shadowNode; //originalMol.getFlatternedShadowSourceObj(shadowNode); var cachedPiECount = originalNode && originalNode.getStructureCacheData('piElectronCount'); if (OU.notUnset(cachedPiECount)) { determinatedMap.set(shadowNode, {'cachedPiElectronCount': cachedPiECount}); } } var result = this._doKekulizeContinousBondSys(0, targetBonds, targetNodes, determinatedMap); return result; }, /** @private */ _doKekulizeContinousBondSys: function(currBondIndex, undeterminatedBonds, sysNodes, determinatedMap) // determinatedMap should be all filled with null to each node/bond { var self = this; // returns explicit bond order, or bond order calculated in this process var getBondOrder = function(bond) { var bondOrder; var bondInfo = determinatedMap.get(bond); if (bondInfo) bondOrder = bondInfo.bondOrder; else if (undeterminatedBonds.indexOf(bond) < 0) // bond outside this system bondOrder = bond.getBondOrder(); if (OU.notUnset(bondOrder)) return bondOrder; else return null; }; // function to return whether a bond is double or triple order var isMultipleBond = function(bond) { var bondOrder = getBondOrder(bond); if ((bondOrder > BO.SINGLE) && (bondOrder <= BO.QUAD)) return true; else return false; }; // function to check if there is a double/triple bond connected to this connector var hasMultibondNeighborConnector = function(bond) { var neighborBonds = bond.getNeighboringBonds(); for (var i = 0, l = neighborBonds.length; i < l; ++i) { var bond = neighborBonds[i]; if (isMultipleBond(bond)) return true; } return false; }; // check if all bond orders are determinated or outside this calcuation system var allBondsDeterminated = function(bonds) { var bondsInSys = AU.intersect(bonds, undeterminatedBonds); var result = !bondsInSys || !bondsInSys.length; if (!result) { result = true; for (var i = 0, l = bondsInSys.length; i < l; ++i) { var b = bondsInSys[i]; if (!determinatedMap.get(b)) // this bond has not been calculated { result = false; break; } } } return result; }; // function to returns the calculated pi electron count of a node before var getNodeDeterminatedPiECount = function(node) { // check structure cache first var cachedData = determinatedMap.get(node); var result = cachedData && cachedData.cachedPiElectronCount; // if not set, check if node connected with multiple bonds if (OU.isUnset(result)) { var linkedConns = node.getLinkedBonds(Kekule.BondType.COVALENT); var isAllConnectorsDeterminated = allBondsDeterminated(linkedConns); if (isAllConnectorsDeterminated) { var possiblePiECount = AU.toArray(self._getPossibleRingNodePiElectronCounts(node, null, null)); if (!possiblePiECount || !possiblePiECount.length || possiblePiECount[0] < 0) // not a aromatic result return -1; else return possiblePiECount[0]; // any positive value is ok } var multipleBondCount = 0, singleBondCount = 0; for (var i = 0, l = linkedConns.length; i < l; ++i) { var conn = linkedConns[i]; var bondOrder = getBondOrder(conn); if (OU.notUnset(bondOrder)) { if (bondOrder === BO.DOUBLE || bondOrder === BO.TRIPLE || bondOrder === BO.QUAD) ++multipleBondCount; else // if (bondOrder === BO.SINGLE) ++singleBondCount; } else { // can not determinate } } // decide e count, or raise error according to bond situation if (multipleBondCount) { if (multipleBondCount === 1) result = 1; else // more than one multipleBond, error result = -1; } else // no multiple bond, unknown { /* var isotope = node.getIsotope && node.getIsotope(); var isHetero = isotope.isHetero && isotope.isHetero(); */ } } return result; }; var connectedNodeHas2PiElectron = function(bond, nodes) { var pi2Count = 0, pi1Count = 0; for (var i = 0, l = nodes.length; i < l; ++i) { var count = getNodeDeterminatedPiECount(nodes[i]); if (OU.notUnset(count)) { if (count >= 2) ++pi2Count; else // if (count <= 1) ++pi1Count; } } if (pi2Count > 0) return true; else if (pi1Count >= 2) // all with 1 pi count return false; else return null; // undeterminated }; var currBond = undeterminatedBonds[currBondIndex]; var currNodes = currBond.getConnectedChemNodes(); var possibleBondOrders = []; if (!determinatedMap.get(currBond)) // this bond has not be determinated { if (hasMultibondNeighborConnector(currBond)) // has double/triple neighbor bond, this bond should always has single order { possibleBondOrders = [BO.SINGLE]; } else // neighbors are all single, may be double or single order (e.g., C-N in pyrrole) { var has2PiElectron = connectedNodeHas2PiElectron(currBond, currNodes); if (has2PiElectron) possibleBondOrders = [BO.SINGLE]; else if (has2PiElectron !== null) // has2PiElectron === false, 1 pi e count possibleBondOrders = [BO.DOUBLE]; else // has2PiElectron === null, undeterminated { possibleBondOrders = [BO.DOUBLE, BO.SINGLE]; } } //console.log('possible bond order', possibleBondOrders, currNodes[0].getSymbol(), currNodes[1].getSymbol()); var result; // try all possible bond orders for (var i = 0, l = possibleBondOrders.length; i < l; ++i) { result = true; //console.log('try', currBondIndex, i, [possibleBondOrders]); var currBondOrder = possibleBondOrders[i]; determinatedMap.set(currBond, {'bondOrder': currBondOrder}); currBond.setBondOrder(currBondOrder); // check curr node pi e count, to see if this setting has error for (var j = 0, k = currNodes.length; j < k; ++j) { var node = currNodes[j]; if (getNodeDeterminatedPiECount(node) < 0) { result = false; break; } } //console.log('node check pass', i, result, currBondOrder, possibleBondOrders); //alert('node check pass ' + i + ' ' + result); if (result) // node check passed { //console.log('True on', currBondIndex, currBond.getId(), currBondOrder); // continue to determinate the rest bonds if (currBondIndex < undeterminatedBonds.length - 1) { result = this._doKekulizeContinousBondSys(currBondIndex + 1, undeterminatedBonds, sysNodes, determinatedMap); if (result) // kekulize successful,returns break; } else { result = true; break; } } } if (!result) // kekulize failed, maybe there is error in previous bond, rollback { //console.log('failed', currBondIndex); currBond.setBondOrder(BO.EXPLICIT_AROMATIC); // restore the bond order of curr bond determinatedMap.remove(currBond); return false; } else { return true; } } }, /** @private */ _splitConnectorsToContinuousParts: function(connectors) { if (!connectors || !connectors.length) return null; var _getNeighboringConnectorNet = function(startingConnector, remainingConnectors) { /* if (traversedConnectors.indexOf(startingConnector) >= 0) return []; */ var startingIndex = remainingConnectors.indexOf(startingConnector); if (startingIndex < 0) return []; else remainingConnectors.splice(startingIndex, 1); var result = [startingConnector]; //traversedConnectors.push(startingConnector); var neighbors = startingConnector.getNeighboringConnectors(); for (var i = 0, l = neighbors.length; i < l; ++i) { var neighbor = neighbors[i]; if (/*traversedConnectors.indexOf(neighbor) < 0 &&*/ remainingConnectors.indexOf(neighbor) >= 0) { // traversedConnectors.push(neighbor); result = result.concat(_getNeighboringConnectorNet(neighbor, remainingConnectors)); } } return result; }; var result = []; var remainingConnectors = AU.clone(connectors); while (remainingConnectors.length) { var startingConnector = remainingConnectors[0]; var neighborNet = _getNeighboringConnectorNet(startingConnector, remainingConnectors); if (neighborNet && neighborNet.length) { result.push(neighborNet); } } return result; }, }); ClassEx.extend(Kekule.StructureFragment, /** @lends Kekule.StructureFragment# */ { /** * Perceive and mark all aromatic rings in molecule. Found rings will be stored in aromaticRings * property of structure fragment object. * @param {Bool} allowUncertainRings Whether uncertain rings (e.g., with variable atom) be included in result. * @param {Array} candidateRings Rings in molecule that the detection will be performed. * If this param is not set, all memebers of SSSR of molecule will be checked. * @return {Array} Found aromatic rings. */ perceiveAromaticRings: function(allowUncertainRings, candidateRings) { var result = this.hasCtab()? this.getCtab().perceiveAromaticRings(allowUncertainRings, candidateRings): []; this.setAromaticRings(result || []); return result; }, /** * Perceive and all aromatic rings in molecule, same as method perceiveAromaticRings. * @param {Bool} allowUncertainRings Whether uncertain rings (e.g., with variable atom) be included in result. * @param {Array} candidateRings Rings in ctab that the detection will be performed. * If this param is not set, all memebers of SSSR of ctab will be checked. * @return {Array} Found aromatic rings. */ findAromaticRings: function(allowUncertainRings, candidateRings) { return this.perceiveAromaticRings(allowUncertainRings, candidateRings); }, /** * Returns aromatic type of a ring. * @param {Object} ring * @param {Array} refRings Should list all related rings to ring, help to determine the p electron number. * If this value is not set, SSSR of molecule will be used instead. */ getRingAromaticType: function(ring, refRings) { return this.hasCtab()? this.getCtab().getRingAromaticType(ring, refRings): null; }, /** * Returns the bond order changes that need to be done on aromatic rings when do a hucklization. * @param {Array} targetBonds Optional, the target bonds. If this param is not set, all aromatic rings in connection table will be handled. * @param {Hash} options Option object, can include fields: { * allowUncertainRings: Whether uncertain rings (e.g., with variable atom) be included in result. Default is false. * }. * @returns {Array} Each item is a hash of {bond, (new)bondOrder (always be explicit aromatic)}. */ getHucklizationChanges: function(targetBonds, options) { return this.hasCtab()? this.getCtab().getHucklizationChanges(targetBonds, options): []; }, /** * Set the orders of Kekule form bonds (single/double bonds) in aromatic rings to {@link Kekule.BondOrder.EXPLICIT_AROMATIC}. * @param {Array} targetBonds Optional, the target bonds. If this param is not set, all aromatic rings in connection table will be handled. * @param {Hash} options Option object, can include fields: { * allowUncertainRings: Whether uncertain rings (e.g., with variable atom) be included in result. Default is false. * }. * @return {Array} Hucklized bonds. */ hucklize: function(targetBonds, options) { return this.hasCtab()? this.getCtab().hucklize(targetBonds, options): []; }, /** * Returns the bond order changes that need to be done on explicit aromatic bonds. * @param {Array} targetBonds Target explicit aromatic bonds. If not set, all aromatic bonds in molecule will be handled. * @param {Hash} options * @returns {Array} Each item is a hash of {bond, (new)bondOrder}. */ getKekulizationChanges: function(targetBonds, options) { return this.hasCtab()? this.getCtab().getKekulizationChanges(targetBonds, options): []; }, /** * Set the orders of Kekule form bonds (single/double bonds) in aromatic rings to {@link Kekule.BondOrder.EXPLICIT_AROMATIC}. * @param {Array} targetBonds Target explicit aromatic bonds. If not set, all aromatic bonds in molecule will be handled. * @param {Hash} options Options for kekulization process. It may include the following fields: * { * doAromaticTests: Whether do an aromatic ring perception to determinate the pi electron count before carrying out the calculation. Default is false. * includingSubgroups: Whether do kekulization on sub groups too. Default is true. * expandSubFragments: Whether expand all sub structures before kekulization. * useShadow: Whether use a shadow fragment for calculating the changes to avoid affect current molecule. Default is true. * Note: if not using shadow, the bond modifications will be applied directly to structure, rather than record them. * } * @return {Array} Changed bonds. */ kekulize: function(targetBonds, options) { return this.hasCtab()? this.getCtab().kekulize(targetBonds, options): []; } }); ClassEx.extend(Kekule.ChemObject, /** @lends Kekule.ChemObject# */ { /** * Perceive and mark all aromatic rings in chem object. Found rings will be stored in aromaticRings * property of structure fragment object. * @param {Bool} allowUncertainRings Whether uncertain rings (e.g., with variable atom) be included in result. * @param {Array} candidateRings Rings in molecule that the detection will be performed. * If this param is not set, all memebers of SSSR of molecule will be checked. * @return {Array} Found aromatic rings. */ perceiveAromaticRings: function(allowUncertainRings, candidateRings) { var ss = CU.getAllStructFragments(this); var result = []; for (var i = 0, l = ss.length; i < l; ++i) { var rings = ss[i].perceiveAromaticRings(allowUncertainRings, candidateRings); if (rings) result = result.concat(rings); } return result.length? result: null; }, /** * Perceive and all aromatic rings in chem object, same as method perceiveAromaticRings. * @param {Bool} allowUncertainRings Whether uncertain rings (e.g., with variable atom) be included in result. * @param {Array} candidateRings Rings in ctab that the detection will be performed. * If this param is not set, all memebers of SSSR of ctab will be checked. * @return {Array} Found aromatic rings. */ findAromaticRings: function(allowUncertainRings, candidateRings) { return this.perceiveAromaticRings(allowUncertainRings, candidateRings); }, /** @private */ _groupActualTargetBondsOfKekulizationOrHucklization: function(structFragments, targetBonds, options) { var result = []; if (!targetBonds) { for (var i = 0, l = structFragments.length; i < l; ++i) result.push({'structFragment': structFragments[i], 'bonds': null}); } else { for (var i = 0, l = targetBonds.length; i < l; ++i) { var bond = targetBonds[i]; for (var j = 0, k = structFragments.length; j < k; ++j) { var mol = structFragments[j]; if (bond.isChildOf(mol)) { if (!result[j]) result[j] = {'structFragment': mol, 'bonds': [bond]}; else result[j].bonds.push(bond); } } } } return result; }, /** * Returns the bond order changes that need to be done on aromatic rings when do a hucklization. * @param {Array} targetBonds Optional, the target bonds. If this param is not set, all aromatic rings in connection table will be handled. * @param {Hash} options Option object, can include fields: { * allowUncertainRings: Whether uncertain rings (e.g., with variable atom) be included in result. Default is false. * }. * @returns {Array} Each item is a hash of {bond, (new)bondOrder (always be explicit aromatic)}. */ getHucklizationChanges: function(targetBonds, options) { var result = []; var ss = CU.getAllStructFragments(this); var group = this._groupActualTargetBondsOfKekulizationOrHucklization(ss, targetBonds, options); for (var i = 0, l = group.length; i < l; ++i) { var item = group[i]; if (item) result = result.concat(item.structFragment.getHucklizationChanges(item.bonds, options) || []); } /* for (var i = 0, l = ss.length; i < l; ++i) { result = result.concat(ss[i].getHucklizationChanges(targetBonds, options) || []); } */ return result; }, /** * Set the orders of Kekule form bonds (single/double bonds) in aromatic rings to {@link Kekule.BondOrder.EXPLICIT_AROMATIC}. * @param {Array} targetBonds Optional, the target bonds. If this param is not set, all aromatic rings in connection table will be handled. * @param {Hash} options Option object, can include fields: { * allowUncertainRings: Whether uncertain rings (e.g., with variable atom) be included in result. Default is false. * }. * @return {Array} Hucklized bonds. */ hucklize: function(targetBonds, options) { var result = []; var ss = CU.getAllStructFragments(this); /* for (var i = 0, l = ss.length; i < l; ++i) { result = result.concat(ss[i].hucklize(targetBonds, options) || []); } */ var group = this._groupActualTargetBondsOfKekulizationOrHucklization(ss, targetBonds, options); for (var i = 0, l = group.length; i < l; ++i) { var item = group[i]; if (item) result = result.concat(item.structFragment.hucklize(item.bonds, options) || []); } return result; }, /** * Returns the bond order changes that need to be done on explicit aromatic bonds. * @param {Array} targetBonds Target explicit aromatic bonds. If not set, all aromatic bonds in molecule will be handled. * @param {Hash} options Options for kekulization process. It may include the following fields: * { * doAromaticTests: Whether do an aromatic ring perception to determinate the pi electron count before carrying out the calculation. Default is false. * includingSubgroups: Whether do kekulization on sub groups too. Default is true. * expandSubFragments: Whether expand all sub structures before kekulization. * useShadow: Whether use a shadow fragment for calculating the changes to avoid affect current molecule. Default is true. * Note: if not using shadow, the bond modifications will be applied directly to structure, rather than record them. * } * @returns {Array} Each item is a hash of {bond, (new)bondOrder}. */ getKekulizationChanges: function(targetBonds, options) { var result = []; var ss = CU.getAllStructFragments(this); /* for (var i = 0, l = ss.length; i < l; ++i) { result = result.concat(ss[i].getKekulizationChanges(targetBonds, options) || []); } */ var group = this._groupActualTargetBondsOfKekulizationOrHucklization(ss, targetBonds, options); for (var i = 0, l = group.length; i < l; ++i) { var item = group[i]; if (item) result = result.concat(item.structFragment.getKekulizationChanges(item.bonds, options) || []); } return result; }, /** * Set the orders of Kekule form bonds (single/double bonds) in aromatic rings to {@link Kekule.BondOrder.EXPLICIT_AROMATIC}. * @param {Array} targetBonds Target explicit aromatic bonds. If not set, all aromatic bonds in molecule will be handled. * @param {Hash} options * @return {Array} Changed bonds. */ kekulize: function(targetBonds, options) { var result = []; var ss = CU.getAllStructFragments(this); /* for (var i = 0, l = ss.length; i < l; ++i) { result = result.concat(ss[i].kekulize(targetBonds, options) || []); } */ var group = this._groupActualTargetBondsOfKekulizationOrHucklization(ss, targetBonds, options); for (var i = 0, l = group.length; i < l; ++i) { var item = group[i]; if (item) result = result.concat(item.structFragment.kekulize(item.bonds, options) || []); } return result; } }); })();