Source: algorithm/kekule.structures.aromatics.js

/**
 * @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 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 isSaturated = node.isSaturated && node.isSaturated();

			if (!isSaturated)  // check if the multiple bond is on ring or outside ring
			{
				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 = AU.intersect(multipleBonds, ctabRingConnectors);
				var multipleBondOnRingCount = multipleBondsOnRing.length;
				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
						{
							return [1, 2];
						}
						else  // C
							return 1;
					}
					else if ((multipleBondOnRingCount === 1) && (multipleBondsOnRing[0].getBondOrder() === Kekule.BondOrder.DOUBLE))
						return 1;
					else
						return 0;  // C=C=C or triple bond has no aromatic
				}
				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
							return 0;
						else
							return 1;
					}
					else
						return 1;
				}
			}
			else // saturated
			{
				if (node.getRadical && (node.getRadical() === Kekule.RadicalOrder.DOUBLET))
					return 1;
				else if (isotope.isHetero())  // saturated N/S/P/O..., pX2
					return 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));
				}
				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
	 * @returns {Int} Value from {@link Kekule.AromaticTypes}
	 * @private
	 */
	_checkRingAromaticType: function(ring, piECountMap)
	{
		if (piECountMap)
		{
			var nodes = ring.nodes;
			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;
			// 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;
					if ((times <= 5) && (times !== 2))  // times = 2, 10 carbon ring, not aromatic
					{
						if (mod === 2)
							currResult = Kekule.AromaticTypes.EXPLICIT_AROMATIC;
						else if (!mod)
							currResult = Kekule.AromaticTypes.ANTIAROMATIC;
					}
				}

				if (lastResult !== null)  // check previous and curr result value
				{
					if (lastResult !== currResult)
					{
						if ((lastResult === Kekule.AromaticTypes.EXPLICIT_AROMATIC)
							|| (currResult === Kekule.AromaticTypes.EXPLICIT_AROMATIC))
						{
							return Kekule.AromaticTypes.UNCERTAIN;
							break;
						}
					}
				}
				else  // lastResult not set
					lastResult = currResult;
			}
			while (nextIndexes(currIndexes))

			return lastResult;
			/*
			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 eCounts = this._getPossibleRingNodePiElectronCounts(node, allNodes, allConnectors);
			piECountMap.set(node, eCounts);
			//node.setCharge(eCount);  // debug
		}
	},
	/**
	 * 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 rings = candidateRings || this.findSSSR();
		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 = this._checkRingAromaticType(ring, piECountMap);
				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.
	 */
	getRingAromaticType: function(ring, refRings)
	{
		if (!refRings)
			refRings = this.findSSSR();
		var result;
		var piECountMap = new Kekule.MapEx();
		try
		{
			this._calcPossibleRingNodesPElectronCounts(piECountMap, [ring], refRings);
			result = this._checkRingAromaticType(ring, piECountMap);
		}
		finally
		{
			piECountMap.finalize();
		}
		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;
	}
});

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);
	}
});

})();