1 package uk.ac.cam.ch.wwmm.opsin;
2 
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Collection;
6 import java.util.Collections;
7 import java.util.Comparator;
8 import java.util.HashMap;
9 import java.util.HashSet;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.Set;
13 import java.util.Map.Entry;
14 
15 /**
16  * Identifies stereocentres and determines the CIP order of connected atoms
17  * @author dl387
18  *
19  */
20 class StereoAnalyser {
21 	/** The atoms/bonds upon which this StereoAnalyser is operating */
22 	private final Collection<Atom> atoms;
23 	private final Collection<Bond> bonds;
24 
25 	/** Maps each atom to its currently assigned colour. Eventually all atoms in non identical environments will have different colours. Higher is higher priority*/
26 	private final Map<Atom, Integer> mappingToColour;
27 
28 	/** Maps each atom to an array of the colours of its neighbours*/
29 	private final Map<Atom, int[]> atomNeighbourColours;
30 
31 	private final AtomNeighbouringColoursComparator atomNeighbouringColoursComparator = new AtomNeighbouringColoursComparator();
32 	private static final AtomicNumberThenAtomicMassComparator atomicNumberThenAtomicMassComparator = new AtomicNumberThenAtomicMassComparator();
33 
34 	/**
35 	 * Holds information about a tetrahedral stereocentre
36 	 * @author dl387
37 	 *
38 	 */
39 	class StereoCentre{
40 		private final Atom stereoAtom;
41 		private final boolean trueStereoCentre;
42 
43 		/**
44 		 * Creates a stereocentre object from a tetrahedral stereocentre atom
45 		 * @param stereoAtom
46 		 */
StereoCentre(Atom stereoAtom, Boolean isTrueStereoCentre)47 		StereoCentre(Atom stereoAtom, Boolean isTrueStereoCentre) {
48 			this.stereoAtom = stereoAtom;
49 			this.trueStereoCentre = isTrueStereoCentre;
50 		}
51 
getStereoAtom()52 		Atom getStereoAtom() {
53 			return stereoAtom;
54 		}
55 
56 		/**
57 		 * Does this atom have 4 constitutionally different groups (or 3 and a lone pair)
58 		 * or is it only a stereo centre due to the presence of other centres in the molecule
59 		 * @return
60 		 */
isTrueStereoCentre()61 		boolean isTrueStereoCentre() {
62 			return trueStereoCentre;
63 		}
64 
getCipOrderedAtoms()65 		List<Atom> getCipOrderedAtoms() throws CipOrderingException {
66 			List<Atom> cipOrderedAtoms = new CipSequenceRules(stereoAtom).getNeighbouringAtomsInCipOrder();
67 			if (cipOrderedAtoms.size()==3){//lone pair is the 4th. This is represented by the atom itself and is always the lowest priority
68 				cipOrderedAtoms.add(0, stereoAtom);
69 			}
70 			return cipOrderedAtoms;
71 		}
72 	}
73 
74 	/***
75 	 * Holds information about a double bond that can possess E/Z stereochemistry
76 	 * @author dl387
77 	 *
78 	 */
79 	class StereoBond{
80 		private final Bond bond;
StereoBond(Bond bond)81 		StereoBond(Bond bond) {
82 			this.bond = bond;
83 		}
getBond()84 		Bond getBond() {
85 			return bond;
86 		}
87 
88 		/**
89 		 * Returns the following atoms:
90 		 * Highest CIP atom on one side
91 		 * atom in bond
92 		 * other atom in bond
93 		 * Highest CIP atom on other side
94 		 * @return
95 		 * @throws CipOrderingException
96 		 */
getOrderedStereoAtoms()97 		List<Atom> getOrderedStereoAtoms() throws CipOrderingException {
98 			Atom a1 = bond.getFromAtom();
99 			Atom a2 = bond.getToAtom();
100 			List<Atom> cipOrdered1 = new CipSequenceRules(a1).getNeighbouringAtomsInCipOrderIgnoringGivenNeighbour(a2);
101 			List<Atom> cipOrdered2 = new CipSequenceRules(a2).getNeighbouringAtomsInCipOrderIgnoringGivenNeighbour(a1);
102 			List<Atom> stereoAtoms = new ArrayList<Atom>();
103 			stereoAtoms.add(cipOrdered1.get(cipOrdered1.size()-1));//highest CIP adjacent to a1
104 			stereoAtoms.add(a1);
105 			stereoAtoms.add(a2);
106 			stereoAtoms.add(cipOrdered2.get(cipOrdered2.size()-1));//highest CIP adjacent to a2
107 			return stereoAtoms;
108 		}
109 	}
110 
111 	/**
112 	 * Sorts atoms by their atomic number, low to high
113 	 * In the case of a tie sorts by atomic mass
114 	 * @author dl387
115 	 *
116 	 */
117 	private static class AtomicNumberThenAtomicMassComparator implements Comparator<Atom> {
compare(Atom a, Atom b)118 	    public int compare(Atom a, Atom b){
119 	    	return compareAtomicNumberThenAtomicMass(a, b);
120 	    }
121 	}
122 
compareAtomicNumberThenAtomicMass(Atom a, Atom b)123 	private static int compareAtomicNumberThenAtomicMass(Atom a, Atom b){
124     	int atomicNumber1 = a.getElement().ATOMIC_NUM;
125     	int atomicNumber2 = b.getElement().ATOMIC_NUM;
126     	if (atomicNumber1 > atomicNumber2){
127     		return 1;
128     	}
129     	else if (atomicNumber1 < atomicNumber2){
130     		return -1;
131     	}
132     	Integer atomicMass1 = a.getIsotope();
133     	Integer atomicMass2 = b.getIsotope();
134     	if (atomicMass1 != null && atomicMass2 == null){
135     		return 1;
136     	}
137     	else if (atomicMass1 == null && atomicMass2 != null){
138     		return -1;
139     	}
140     	else if (atomicMass1 != null && atomicMass2 != null){
141         	if (atomicMass1 > atomicMass2){
142 	    		return 1;
143 	    	}
144 	    	else if (atomicMass1 < atomicMass2){
145 	    		return -1;
146 	    	}
147     	}
148 		return 0;
149 	}
150 
151 	/**
152 	 * Sorts based on the list of colours for neighbouring atoms
153 	 * e.g. [1,2] > [1,1]  [1,1,3] > [2,2,2]  [1,1,3] > [3]
154 	 * @author dl387
155 	 *
156 	 */
157 	private class AtomNeighbouringColoursComparator implements Comparator<Atom> {
compare(Atom a, Atom b)158 	    public int compare(Atom a, Atom b){
159 	    	int[] colours1 = atomNeighbourColours.get(a);
160 	    	int[] colours2 = atomNeighbourColours.get(b);
161 
162 	    	int colours1Size = colours1.length;
163 	    	int colours2Size = colours2.length;
164 
165 	    	int maxCommonColourSize = Math.min(colours1Size, colours2Size);
166 	    	for (int i = 1; i <= maxCommonColourSize; i++) {
167 				int difference = colours1[colours1Size - i] - colours2[colours2Size - i];
168 				if (difference > 0){
169 					return 1;
170 				}
171 				if (difference < 0){
172 					return -1;
173 				}
174 			}
175 	    	int differenceInSize = colours1Size - colours2Size;
176 	    	if (differenceInSize > 0){
177 	    		return 1;
178 	    	}
179 	    	if (differenceInSize < 0){
180 	    		return -1;
181 	    	}
182 			return 0;
183 	    }
184 	}
185 
186 	/**
187 	 * Employs a derivative of the InChI algorithm to label which atoms are equivalent.
188 	 * These labels can then be used by the findStereo(Atoms/Bonds) functions to find features that
189 	 * can possess stereoChemistry
190 	 * @param molecule
191 	 */
StereoAnalyser(Fragment molecule)192 	StereoAnalyser(Fragment molecule) {
193 		this (molecule.getAtomList(), molecule.getBondSet());
194 	}
195 
196 	/**
197 	 * Employs a derivative of the InChI algorithm to label which atoms are equivalent.
198 	 * These labels can then be used by the findStereo(Atoms/Bonds) functions to find features that
199 	 * can possess stereoChemistry
200 	 * NOTE: All bonds of every atom must be in the set of bonds, no atom may have a bond to an atom not in the list
201 	 * @param atoms
202 	 * @param bonds
203 	 */
StereoAnalyser(Collection<Atom> atoms, Collection<Bond> bonds)204 	StereoAnalyser(Collection<Atom> atoms, Collection<Bond> bonds) {
205 		this.atoms = atoms;
206 		this.bonds = bonds;
207 		List<Atom> ghostAtoms = addGhostAtoms();
208 		List<Atom> atomsToSort = new ArrayList<Atom>(atoms);
209 		atomsToSort.addAll(ghostAtoms);
210 		mappingToColour = new HashMap<Atom, Integer>(atomsToSort.size());
211 		atomNeighbourColours = new HashMap<Atom, int[]>(atomsToSort.size());
212 		Collections.sort(atomsToSort, atomicNumberThenAtomicMassComparator);
213 		List<List<Atom>> groupsByColour = populateColoursByAtomicNumberAndMass(atomsToSort);
214 		boolean changeFound = true;
215 		while(changeFound){
216 			for (List<Atom> groupWithAColour : groupsByColour) {
217 				for (Atom atom : groupWithAColour) {
218 					int[] neighbourColours = findColourOfNeighbours(atom);
219 					atomNeighbourColours.put(atom, neighbourColours);
220 				}
221 			}
222 			List<List<Atom>> updatedGroupsByColour = new ArrayList<List<Atom>>();
223 			changeFound = populateColoursAndReportIfColoursWereChanged(groupsByColour, updatedGroupsByColour);
224 			groupsByColour = updatedGroupsByColour;
225 		}
226 		removeGhostAtoms(ghostAtoms);
227 	}
228 
229 	/**
230 	 * Adds "ghost" atoms in the same way as the CIP rules for handling double bonds
231 	 * e.g. C=C --> C(G)=C(G) where ghost is a carbon with no hydrogen bonded to it
232 	 * @return The ghost atoms created
233 	 */
addGhostAtoms()234 	private List<Atom> addGhostAtoms() {
235 		List<Atom> ghostAtoms = new ArrayList<Atom>();
236 		for (Bond bond : bonds) {
237 			int bondOrder = bond.getOrder();
238 			for (int i = bondOrder; i > 1; i--) {
239 				Atom fromAtom = bond.getFromAtom();
240 				Atom toAtom = bond.getToAtom();
241 
242 				Atom ghost1 = new Atom(fromAtom.getElement());
243 				Bond b1 = new Bond(ghost1, toAtom, 1);
244 				toAtom.addBond(b1);
245 				ghost1.addBond(b1);
246 				ghostAtoms.add(ghost1);
247 
248 				Atom ghost2 = new Atom(toAtom.getElement());
249 				Bond b2 = new Bond(ghost2, fromAtom, 1);
250 				fromAtom.addBond(b2);
251 				ghost2.addBond(b2);
252 				ghostAtoms.add(ghost2);
253 			}
254 		}
255 		return ghostAtoms;
256 	}
257 
258 	/**
259 	 * Removes the ghost atoms added by addGhostAtoms
260 	 * @param ghostAtoms
261 	 */
removeGhostAtoms(List<Atom> ghostAtoms)262 	private void removeGhostAtoms(List<Atom> ghostAtoms) {
263 		for (Atom atom : ghostAtoms) {
264 			Bond b = atom.getFirstBond();
265 			b.getOtherAtom(atom).removeBond(b);
266 		}
267 	}
268 
269 
270 	/**
271 	 * Takes a list of atoms sorted by atomic number/mass
272 	 * and populates the mappingToColour map
273 	 * @param atomList
274 	 * @return
275 	 */
populateColoursByAtomicNumberAndMass(List<Atom> atomList)276 	private List<List<Atom>> populateColoursByAtomicNumberAndMass(List<Atom> atomList) {
277 		List<List<Atom>> groupsByColour = new ArrayList<List<Atom>>();
278 		Atom previousAtom = null;
279 		List<Atom> atomsOfThisColour = new ArrayList<Atom>();
280 		int atomsSeen = 0;
281 		for (Atom atom : atomList) {
282 			if (previousAtom != null && compareAtomicNumberThenAtomicMass(previousAtom, atom) != 0){
283 				for (Atom atomOfthisColour : atomsOfThisColour) {
284 					mappingToColour.put(atomOfthisColour, atomsSeen);
285 				}
286 				groupsByColour.add(atomsOfThisColour);
287 				atomsOfThisColour = new ArrayList<Atom>();
288 			}
289 			previousAtom = atom;
290 			atomsOfThisColour.add(atom);
291 			atomsSeen++;
292 		}
293 		if (!atomsOfThisColour.isEmpty()){
294 			for (Atom atomOfThisColour : atomsOfThisColour) {
295 				mappingToColour.put(atomOfThisColour, atomsSeen);
296 			}
297 			groupsByColour.add(atomsOfThisColour);
298 		}
299 		return groupsByColour;
300 	}
301 
302 	/**
303 	 * Takes the lists of atoms pre-grouped by colour and sorts each by its neighbours colours
304 	 * The updatedGroupsByColour is populated with those for which this process caused a change
305 	 * and populates the mappingToColour map
306 	 * Returns whether mappingToColour was changed
307 	 * @param groupsByColour
308 	 * @param updatedGroupsByColour
309 	 * @return boolean Whether mappingToColour was changed
310 	 */
populateColoursAndReportIfColoursWereChanged(List<List<Atom>> groupsByColour, List<List<Atom>> updatedGroupsByColour)311 	private boolean populateColoursAndReportIfColoursWereChanged(List<List<Atom>> groupsByColour, List<List<Atom>> updatedGroupsByColour) {
312 		boolean changeFound = false;
313 		int atomsSeen = 0;
314 		for (List<Atom> groupWithAColour : groupsByColour) {
315 			Collections.sort(groupWithAColour, atomNeighbouringColoursComparator);
316 			Atom previousAtom = null;
317 			List<Atom> atomsOfThisColour = new ArrayList<Atom>();
318 			for (Atom atom : groupWithAColour) {
319 				if (previousAtom != null && atomNeighbouringColoursComparator.compare(previousAtom, atom) != 0){
320 					for (Atom atomOfThisColour : atomsOfThisColour) {
321 						if (!changeFound && atomsSeen != mappingToColour.get(atomOfThisColour)){
322 							changeFound = true;
323 						}
324 						mappingToColour.put(atomOfThisColour, atomsSeen);
325 					}
326 					updatedGroupsByColour.add(atomsOfThisColour);
327 					atomsOfThisColour = new ArrayList<Atom>();
328 				}
329 				previousAtom = atom;
330 				atomsOfThisColour.add(atom);
331 				atomsSeen++;
332 			}
333 			if (!atomsOfThisColour.isEmpty()){
334 				for (Atom atomOfThisColour : atomsOfThisColour) {
335 					if (!changeFound && atomsSeen != mappingToColour.get(atomOfThisColour)){
336 						changeFound = true;
337 					}
338 					mappingToColour.put(atomOfThisColour, atomsSeen);
339 				}
340 				updatedGroupsByColour.add(atomsOfThisColour);
341 			}
342 		}
343 		return changeFound;
344 	}
345 
346 	/**
347 	 * Produces a sorted (low to high) array of the colour of the atoms surrounding a given atom
348 	 * @param atom
349 	 * @return int[] colourOfAdjacentAtoms
350 	 */
findColourOfNeighbours(Atom atom)351 	private int[] findColourOfNeighbours(Atom atom) {
352 		List<Bond> bonds = atom.getBonds();
353 		int bondCount = bonds.size();
354 		int[] colourOfAdjacentAtoms = new int[bondCount];
355 		for (int i = 0; i < bondCount; i++) {
356 			Bond bond = bonds.get(i);
357 			Atom otherAtom = bond.getOtherAtom(atom);
358 			colourOfAdjacentAtoms[i] = mappingToColour.get(otherAtom);
359 		}
360 		Arrays.sort(colourOfAdjacentAtoms);//sort such that this goes from low to high
361 		return colourOfAdjacentAtoms;
362 	}
363 
364 	/**
365 	 * Retrieves a list of any tetrahedral stereoCentres
366 	 * Internally this is done by checking whether the "colour" of all neighbouring atoms of the tetrahedral atom are different
367 	 * @return List<StereoCentre>
368 	 */
findStereoCentres()369 	List<StereoCentre> findStereoCentres(){
370 		List<Atom> potentialStereoAtoms = getPotentialStereoCentres();
371 		List<Atom> trueStereoCentres = new ArrayList<Atom>();
372 		for (Atom potentialStereoAtom : potentialStereoAtoms) {
373 			if (isTrueStereCentre(potentialStereoAtom)){
374 				trueStereoCentres.add(potentialStereoAtom);
375 			}
376 		}
377 		List<StereoCentre> stereoCentres = new ArrayList<StereoCentre>();
378 		for (Atom trueStereoCentreAtom : trueStereoCentres) {
379 			stereoCentres.add(new StereoCentre(trueStereoCentreAtom, true));
380 		}
381 
382 		potentialStereoAtoms.removeAll(trueStereoCentres);
383 		List<Atom> paraStereoCentres = findParaStereoCentres(potentialStereoAtoms, trueStereoCentres);
384 		for (Atom paraStereoCentreAtom : paraStereoCentres) {
385 			stereoCentres.add(new StereoCentre(paraStereoCentreAtom, false));
386 		}
387 		return stereoCentres;
388 	}
389 
390 	/**
391 	 * Retrieves atoms that pass the isPossiblyStereogenic() criteria
392 	 * @return
393 	 */
getPotentialStereoCentres()394 	private List<Atom> getPotentialStereoCentres() {
395 		List<Atom> potentialStereoAtoms = new ArrayList<Atom>();
396 		for (Atom atom : atoms) {
397 			if (isPossiblyStereogenic(atom)){
398 				potentialStereoAtoms.add(atom);
399 			}
400 		}
401 		return potentialStereoAtoms;
402 	}
403 
404 	/**
405 	 * Checks whether the atom has 3 or 4 neighbours all of which are constitutionally different
406 	 * @param potentialStereoAtom
407 	 * @return
408 	 */
isTrueStereCentre(Atom potentialStereoAtom)409 	private boolean isTrueStereCentre(Atom potentialStereoAtom) {
410 		List<Atom> neighbours = potentialStereoAtom.getAtomNeighbours();
411 		if (neighbours.size()!=3 && neighbours.size()!=4){
412 			return false;
413 		}
414 		int[] colours = new int[4];
415 		for (int i = neighbours.size() - 1 ; i >=0; i--) {
416 			colours[i] = mappingToColour.get(neighbours.get(i));
417 		}
418 
419 		boolean foundIdenticalNeighbour =false;
420 		for (int i = 0; i < 4; i++) {
421 			int cl = colours[i];
422 			for (int j = i +1; j < 4; j++) {
423 				if (cl == colours[j]){
424 					foundIdenticalNeighbour =true;
425 					break;
426 				}
427 			}
428 		}
429 		return !foundIdenticalNeighbour;
430 	}
431 
432 	/**
433 	 * Finds a subset of the stereocentres associated with rule 2 from:
434 	 * DOI: 10.1021/ci00016a003
435 	 * @param potentialStereoAtoms
436 	 * @param trueStereoCentres
437 	 */
findParaStereoCentres(List<Atom> potentialStereoAtoms, List<Atom> trueStereoCentres)438 	private List<Atom> findParaStereoCentres(List<Atom> potentialStereoAtoms, List<Atom> trueStereoCentres) {
439 		List<Atom> paraStereoCentres = new ArrayList<Atom>();
440 		for (Atom potentialStereoAtom : potentialStereoAtoms) {
441 			List<Atom> neighbours = potentialStereoAtom.getAtomNeighbours();
442 			if (neighbours.size() == 4){
443 				int[] colours = new int[4];
444 				for (int i = neighbours.size() - 1 ; i >=0; i--) {
445 					colours[i] = mappingToColour.get(neighbours.get(i));
446 				}
447 				//find pairs of constitutionally identical substituents
448 				Map<Integer, Integer> foundPairs = new HashMap<Integer, Integer>();
449 				for (int i = 0; i < 4; i++) {
450 					int cl = colours[i];
451 					for (int j = i +1; j < 4; j++) {
452 						if (cl == colours[j]){
453 							foundPairs.put(i, j);
454 							break;
455 						}
456 					}
457 				}
458 				int pairs = foundPairs.keySet().size();
459 				if (pairs==1 || pairs==2){
460 					boolean hasTrueStereoCentreInAllBranches = true;
461 					for (Entry<Integer, Integer> entry: foundPairs.entrySet()) {
462 						if (!branchesHaveTrueStereocentre(neighbours.get(entry.getKey()), neighbours.get(entry.getValue()), potentialStereoAtom, trueStereoCentres)){
463 							hasTrueStereoCentreInAllBranches = false;
464 							break;
465 						}
466 					}
467 					if (hasTrueStereoCentreInAllBranches){
468 						paraStereoCentres.add(potentialStereoAtom);
469 					}
470 				}
471 			}
472 		}
473 		return paraStereoCentres;
474 	}
475 
476 
branchesHaveTrueStereocentre(Atom branchAtom1, Atom branchAtom2, Atom potentialStereoAtom, List<Atom> trueStereoCentres)477 	private boolean branchesHaveTrueStereocentre(Atom branchAtom1, Atom branchAtom2, Atom potentialStereoAtom, List<Atom> trueStereoCentres) {
478 		List<Atom> atomsToVisit= new ArrayList<Atom>();
479 		Set<Atom> visitedAtoms = new HashSet<Atom>();
480 		visitedAtoms.add(potentialStereoAtom);
481 		atomsToVisit.add(branchAtom1);
482 		atomsToVisit.add(branchAtom2);
483 		while(!atomsToVisit.isEmpty()){
484 			List<Atom> newAtomsToVisit = new ArrayList<Atom>();
485 			while(!atomsToVisit.isEmpty()){
486 				Atom atom = atomsToVisit.remove(0);
487 				if (trueStereoCentres.contains(atom)){
488 					return true;
489 				}
490 				if (atomsToVisit.contains(atom)){//the two branches have converged on this atom, don't investigate neighbours of it
491 					do{
492 						atomsToVisit.remove(atom);
493 					}
494 					while (atomsToVisit.contains(atom));
495 					continue;
496 				}
497 				else{
498 					List<Atom> neighbours = atom.getAtomNeighbours();
499 					for (Atom neighbour : neighbours) {
500 						if (visitedAtoms.contains(neighbour)){
501 							continue;
502 						}
503 						newAtomsToVisit.add(neighbour);
504 					}
505 				}
506 				visitedAtoms.add(atom);
507 			}
508 			atomsToVisit = newAtomsToVisit;
509 		}
510 		return false;
511 	}
512 
513 	/**
514 	 * Checks whether an atom could be a tetrahedral stereocentre by checking that it is both tetrahedral
515 	 * and does not have neighbours that are identical due to resonance/tautomerism
516 	 * @param atom
517 	 * @return
518 	 */
isPossiblyStereogenic(Atom atom)519 	static boolean isPossiblyStereogenic(Atom atom){
520 		return isKnownPotentiallyStereogenic(atom) && !isAchiralDueToResonanceOrTautomerism(atom);
521 	}
522 
523 	/**
524 	 * Roughly corresponds to the list of atoms in table 8 of the InChI manual
525 	 * tetrahedral phosphorus/arsenic are also allowed by InChI but are, perhaps due to an oversight, omitted from this table
526 	 * Essentially does a crude check for whether an atom is known to be able to possess tetrahedral geometry
527 	 * and whether it is currently tetrahedral. Atoms that are tetrahedral but not typically considered chiral
528 	 * like tertiary amines are not recognised
529 	 * @param atom
530 	 * @return
531 	 */
isKnownPotentiallyStereogenic(Atom atom)532 	static boolean isKnownPotentiallyStereogenic(Atom atom) {
533 		List<Atom> neighbours = atom.getAtomNeighbours();
534 		ChemEl chemEl = atom.getElement();
535 		if (neighbours.size() == 4){
536 			if (chemEl == ChemEl.B || chemEl == ChemEl.C || chemEl == ChemEl.Si || chemEl == ChemEl.Ge ||
537 					chemEl == ChemEl.Sn || chemEl == ChemEl.N || chemEl == ChemEl.P || chemEl == ChemEl.As ||
538 						chemEl == ChemEl.S || chemEl == ChemEl.Se){
539 				return true;
540 			}
541 		}
542 		else if (neighbours.size() ==3){
543 			if ((chemEl == ChemEl.S || chemEl == ChemEl.Se) && (atom.getIncomingValency()==4 || (atom.getCharge() ==1 && atom.getIncomingValency()==3))){
544 				//tetrahedral sulfur/selenium - 3 bonds and the lone pair
545 				return true;
546 			}
547 			if (chemEl == ChemEl.N && atom.getCharge() ==0 && atom.getIncomingValency()==3 && atomsContainABondBetweenThemselves(neighbours)){
548 				return true;
549 				//nitrogen where two attached atoms are connected together
550 			}
551 			if ((chemEl == ChemEl.P || chemEl == ChemEl.As) &&atom.getIncomingValency() == 3) {
552 				//tetrahedral phosphorus/arsenic - 3 bonds and the lone pair
553 				return true;
554 			}
555 		}
556 		return false;
557 	}
558 
atomsContainABondBetweenThemselves(List<Atom> atoms)559 	private static boolean atomsContainABondBetweenThemselves(List<Atom> atoms) {
560 		for (Atom atom : atoms) {
561 			for (Atom neighbour : atom.getAtomNeighbours()) {
562 				if (atoms.contains(neighbour)){
563 					return true;
564 				}
565 			}
566 		}
567 		return false;
568 	}
569 
isAchiralDueToResonanceOrTautomerism(Atom atom)570 	static boolean isAchiralDueToResonanceOrTautomerism(Atom atom) {
571 		ChemEl chemEl = atom.getElement();
572 		if(chemEl == ChemEl.N ||
573 				chemEl == ChemEl.P ||
574 				chemEl == ChemEl.As ||
575 				chemEl == ChemEl.S ||
576 				chemEl == ChemEl.Se) {
577 			List<Atom> neighbours = atom.getAtomNeighbours();
578 			Set<String> resonanceAndTautomerismAtomicElementPlusIsotopes = new HashSet<String>();
579 			for (Atom neighbour : neighbours) {
580 				ChemEl neighbourChemEl = neighbour.getElement();
581 				if ((neighbourChemEl.isChalcogen() || neighbourChemEl == ChemEl.N)
582 						&& isOnlyBondedToHydrogensOtherThanGivenAtom(neighbour, atom)){
583 					if (resonanceAndTautomerismAtomicElementPlusIsotopes.contains(neighbourChemEl.toString() + atom.getIsotope())){
584 						return true;
585 					}
586 					resonanceAndTautomerismAtomicElementPlusIsotopes.add(neighbourChemEl.toString() + atom.getIsotope());
587 				}
588 				if (neighbourChemEl == ChemEl.H && neighbour.getBondCount()==1){
589 					//terminal H atom neighbour
590 					return true;
591 				}
592 			}
593 		}
594 		return false;
595 	}
596 
isOnlyBondedToHydrogensOtherThanGivenAtom(Atom atom, Atom attachedNonHydrogen)597 	private static boolean isOnlyBondedToHydrogensOtherThanGivenAtom(Atom atom, Atom attachedNonHydrogen) {
598 		for (Atom neighbour: atom.getAtomNeighbours()) {
599 			if (neighbour.equals(attachedNonHydrogen)){
600 				continue;
601 			}
602 			if (neighbour.getElement() != ChemEl.H){
603 				return false;
604 			}
605 		}
606 		return true;
607 	}
608 
609 	/**
610 	 *  Retrieves a list of any double bonds possessing the potential to have E/Z stereoChemistry
611 	 *  This is done internally by checking the two atoms attached to the ends of the double bond are different
612 	 *  As an exception nitrogen's lone pair is treated like a low priority group and so is allowed to only have 1 atom connected to it
613 	 * @return
614 	 */
findStereoBonds()615 	List<StereoBond> findStereoBonds() {
616 		List<StereoBond> stereoBonds = new ArrayList<StereoBond>();
617 		for (Bond bond : bonds) {
618 			if (bond.getOrder()==2){
619 				Atom a1 = bond.getFromAtom();
620 				List<Atom> neighbours1 =  a1.getAtomNeighbours();
621 				neighbours1.remove(bond.getToAtom());
622 				if (neighbours1.size()==2 || (neighbours1.size()==1 && a1.getElement() == ChemEl.N && a1.getIncomingValency()==3 && a1.getCharge()==0)){
623 					if (neighbours1.size()==2 && mappingToColour.get(neighbours1.get(0)).equals(mappingToColour.get(neighbours1.get(1)))){
624 						continue;
625 					}
626 					Atom a2 = bond.getToAtom();
627 					List<Atom> neighbours2 = a2.getAtomNeighbours();
628 					neighbours2.remove(bond.getFromAtom());
629 					if (neighbours2.size()==2 || (neighbours2.size()==1 && a2.getElement() == ChemEl.N && a2.getIncomingValency()==3 && a2.getCharge()==0)){
630 						if (neighbours2.size()==2 && mappingToColour.get(neighbours2.get(0)).equals(mappingToColour.get(neighbours2.get(1)))){
631 							continue;
632 						}
633 						stereoBonds.add(new StereoBond(bond));
634 					}
635 				}
636 			}
637 		}
638 		return stereoBonds;
639 	}
640 
641 	/**
642 	 * Returns a number describing the environment of an atom. Atoms with the same number are in identical environments
643 	 * Null if atom was not part of this environment analysis
644 	 * @param a
645 	 * @return
646 	 */
getAtomEnvironmentNumber(Atom a)647 	Integer getAtomEnvironmentNumber(Atom a) {
648 		return mappingToColour.get(a);
649 	}
650 }
651