
com.actelion.research.chem.io.pdb.converter.BondsCalculator Maven / Gradle / Ivy
package com.actelion.research.chem.io.pdb.converter;
/*
* Copyright 2016 Actelion Pharmaceuticals Ltd., Gewerbestrasse 16, CH-4123 Allschwil, Switzerland
*
* This file is part of cif2molecule.
*
* cif2molecule is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* cif2molecule is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with DataWarrior.
* If not, see http://www.gnu.org/licenses/.
*
* @author Antanas Vaitkus
*/
import com.actelion.research.chem.*;
import com.actelion.research.chem.conf.BondLengthSet;
import com.actelion.research.util.IntQueue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static com.actelion.research.chem.conf.VDWRadii.COVALENT_RADIUS;
/**
* BondsCalculator is used to recreate the bonds and / or calculate the bonds orders
* based on the 3D coordinates of the atoms
*/
public class BondsCalculator {
private static final boolean DEBUG = false;
private static final double BUMP_FACTOR = 0.5;
private static final double SPECIAL_POSITION_CUTOFF = 0.01;
/**
* Calculates the bonds of a molecule by checking the distance between
* all atoms. The bond order is not set with this function.
*
* Complexity O(nAtoms)
* Memory O(nAtoms)
*
* @param mol
* The molecule for which the bonds should be calculated.
* @param lenient
* Suppresses exceptions that would normally be raised
* due to chemical discrepancies found in the molecule.
* @throws Exception
* Exception containing information about the exceeded
* maximum valence of an atom.
*/
public static void createBonds(Molecule3D mol, boolean lenient) throws Exception {
if ( mol.getAllAtoms()==0 ) return;
// Processing organic atoms first:
// 1. Create a grid
MoleculeGrid grid = new MoleculeGrid(mol);
int[] neighborCount = new int[mol.getAllAtoms()];
// 2. For each atom, check the neighbours and create
// a connection if the distance is close to the sum
// of covalent radii.
for (int i = 0; i < mol.getAllAtoms(); i++) {
if (!mol.isOrganicAtom(i)) continue;
// Get the neighbours
Set set = grid.getNeighbours(mol, i, 3.2);
for (int j : set) {
if (i > j) continue;
if (!mol.isOrganicAtom(j)) continue;
// Two hydrogen (H) atoms are very unlikely to form bonds in crystal structures
if (mol.getAtomicNo(i) == 1 && mol.getAtomicNo(j) == 1) continue;
double dist = getBondLength(mol, i, j);
double idealDist = COVALENT_RADIUS[mol.getAtomicNo(i)] + COVALENT_RADIUS[mol.getAtomicNo(j)];
if (dist > idealDist + .45) continue;
if (neighborCount[i] >= maxNeighborCount(mol, i) ||
neighborCount[j] >= maxNeighborCount(mol, j)) {
int hypervalentAtom = (neighborCount[i] >= maxNeighborCount(mol, i)) ? i : j;
String error = "maximum valence of " + maxNeighborCount(mol, hypervalentAtom) +
" exceeded for atom '" + mol.getAtomName(hypervalentAtom) + "'";
if (!lenient) {
throw new BondCalculatorException(error);
} else {
System.err.println();
}
continue;
}
try {
mol.addBond(i, j, 1);
neighborCount[i]++;
neighborCount[j]++;
} catch (Exception e) {
if (!lenient) throw e;
}
}
}
mol.ensureHelperArrays(Molecule.cHelperRings);
// Deleting incorrectly assigned bonds
for (int i = 0; i < mol.getRingSet().getSize(); i++) {
int[] ringAtoms = mol.getRingSet().getRingAtoms(i);
if (ringAtoms.length == 3) {
if (markImprobableDiphosphorusBondForDeletionInThreeAtomRing(mol, i)) continue;
markImprobableBondWithHydrogenForDeletionInThreeAtomRing(mol, i);
} else if (ringAtoms.length == 4) {
// Small rings sometimes get assigned incorrect interring bonds.
// For example, a 4-membered ring with atom connectivity a1-a2,
// a2-a3, a3-a4, a4-a1 sometimes get assigned an additional
// a1-a3 (or a2-a4) bond.
// The ring must be more or less flat to avoid deleting bonds
// in molecules that have a closely packed tetrahedral geometry
// (e. g. tetrahedrane).
if (StrictMath.abs(getDihedral(mol, ringAtoms[0], ringAtoms[1],
ringAtoms[2], ringAtoms[3])) > 20 * StrictMath.PI / 180) continue;
for (int ringAtom : ringAtoms) {
int inRingNeighbours = 0;
for (int k = 0; k < mol.getConnAtoms(ringAtom); k++) {
if (isRingAtom(mol, i, mol.getConnAtom(ringAtom, k))) {
inRingNeighbours++;
}
}
if (inRingNeighbours > 2) {
int longestBondIndex = -1;
double longestBondLength = -Double.MAX_VALUE;
for (int k = 0; k < mol.getConnAtoms(ringAtom); k++) {
if (!isRingAtom(mol, i, mol.getConnAtom(ringAtom, k))) continue;
if (longestBondLength < getBondLength(mol, mol.getConnBond(ringAtom, k))) {
longestBondIndex = k;
longestBondLength = getBondLength(mol, mol.getConnBond(ringAtom, k));
}
}
// if (isPlanar(mol, ringAtom, mol.getConnAtom(ringAtom, longestBondIndex))) {
mol.markBondForDeletion(mol.getConnBond(ringAtom, longestBondIndex));
// }
}
}
}
}
mol.deleteMarkedAtomsAndBonds();
// Processing non-organic atoms
grid = new MoleculeGrid(mol);
for( int i = 0; i < mol.getAllAtoms(); i++ ) {
if (mol.isOrganicAtom(i)) continue;
// TODO: since these are not covalent bonds, maybe none of the metals should be excluded?
// Not Lithium or Magnesium
// TODO: also Be in organometallic compounds?
// TODO: allow Alkali metals to coordinate water?
// if ( (isAlkaliMetalAtom(mol, i) && mol.getAtomicNo(i) != 3 ) ||
// (isAlkalineEarthMetalAtom(mol, i) && mol.getAtomicNo(i) != 12 ) ) continue;
Set set = grid.getNeighbours(mol, i, 3.2);
for (int j : set) {
double dist = getBondLength( mol, i, j );
double idealDist = COVALENT_RADIUS[mol.getAtomicNo(i)] + COVALENT_RADIUS[mol.getAtomicNo(j)];
if (dist > idealDist + .45) continue;
mol.addBond(i, j, Molecule3D.cBondTypeMetalLigand);
}
}
int ringNumber = mol.getRingSet().getSize();
Coordinates[] ringCenters = new Coordinates[ringNumber];
for (int i = 0; i < ringNumber; i++) {
ringCenters[i] = getAtomRingCenter(mol, i);
}
ArrayList[] atomsToRings = getAtomToRings(mol);
for ( int i = 0; i < mol.getAllAtoms(); i++ ) {
if ( !mol.isMetalAtom(i) ) continue;
boolean[] ringProcessed = new boolean[ringNumber];
boolean[] ringCoordinated = new boolean[ringNumber];
for ( int j = mol.getAllConnAtoms(i); j < mol.getAllConnAtomsPlusMetalBonds(i); j++ ) {
int ligand = mol.getConnAtom(i, j);
if ( mol.isMetalAtom(ligand) ) continue;
if ( atomsToRings[ligand] == null ) continue;
int closestRing = -1;
double closestDist = 0;
for ( int ringIndex: atomsToRings[ligand] ) {
if ( closestRing < 0 ||
ringCenters[ringIndex].distance(mol.getCoordinates(i)) < closestDist ) {
closestDist = ringCenters[ringIndex].distance(mol.getCoordinates(i));
closestRing = ringIndex;
}
}
if ( ringProcessed[closestRing] ) continue;
int bondCount = 0;
for ( int k = 0; k < mol.getAllConnAtomsPlusMetalBonds(i); k++ ) {
if ( isRingAtom(mol, closestRing, mol.getConnAtom(i, k) ) ) bondCount++;
}
// TODO: check is ring is (potentially) aromatic?
ringProcessed[closestRing] = true;
if ( bondCount > mol.getRingSet().getRingSize(closestRing) / 2 ) {
ringCoordinated[closestRing] = true;
}
}
for ( int j = 0; j < ringNumber; j++ ) {
if ( ringCoordinated[j] ) {
int[] ringAtoms = mol.getRingSet().getRingAtoms(j);
for (int ringAtom : ringAtoms) {
mol.addBond(i, ringAtom, Molecule.cBondTypeMetalLigand);
}
}
}
}
mol.deleteMarkedAtomsAndBonds();
mol.ensureHelperArrays(Molecule.cHelperNeighbours);
// TODO: ask about the applicability of this part
// remove erroneously assigned metal-ligand bonds
ArrayList bondsToDelete = new ArrayList<>();
for (int i = 0; i < mol.getAllBonds(); i++) {
if ( mol.getBondType(i) != Molecule3D.cBondTypeMetalLigand ) continue;
int metal;
int ligand;
if ( mol.isMetalAtom( mol.getBondAtom(0, i) ) ) {
metal = mol.getBondAtom(0, i);
ligand = mol.getBondAtom(1, i);
} else {
metal = mol.getBondAtom(1, i);
ligand = mol.getBondAtom(0, i);
}
for (int j = 0; j < mol.getAllConnAtomsPlusMetalBonds(ligand); j++ ) {
int ligandNeighbour = mol.getConnAtom(ligand, j);
if ( ligandNeighbour == metal ) continue;
double sigma = 0.2;
if ( getBondLength( mol, metal, ligand ) - getBondLength(mol, metal, ligandNeighbour) > sigma) {
bondsToDelete.add(i);
}
}
}
bondsToDelete.forEach(mol::markBondForDeletion);
mol.deleteMarkedAtomsAndBonds();
}
/**
* Evaluates if the given three-membered ring contains an unlikely bond
* between two phosphorus atoms. The bond is considered unlikely if it
* occurs in a three-membered ring and is above the specified bond length
* threshold.
*
* Such incorrect bonds are likely to be placed in molecules where two
* phosphorus (P) atoms are bridged by a third atom forming an acute angle.
* For example, COD entry 4071806 contains the PCP pattern that is incorrectly
* fused into a P1CP1 ring due to the distance between the two P atoms (2.426 A)
* falling into the range of previously observed long P-P bonds. It should be noted,
* however, that not all diphosporus bonds appearing in three-membered rings are
* incorrect as illustrated by COD entry 4062100. As such, additional bond length
* threshold is used to determine is the bond should be marked for deletion.
*
* @param mol
* Molecule that contains the ring.
* @param ringIndex
* Index of the ring in the molecule.
* @return
* Boolean value denoting if an improbable diphosphorus
* bond was identified and marked for deletion.
*/
private static boolean markImprobableDiphosphorusBondForDeletionInThreeAtomRing(Molecule3D mol, int ringIndex) {
if (mol.getRingSet().getRingSize(ringIndex) != 3) return false;
int[] ringBonds = mol.getRingSet().getRingBonds(ringIndex);
int longestDiphosphoroBondIndex = -1;
double longestDiphosphoroBondLength = 0;
for (int ringBond : ringBonds) {
int a1 = mol.getBondAtom(0, ringBond);
if (mol.getAtomicNo(a1) != 15) continue;
int a2 = mol.getBondAtom(1, ringBond);
if (mol.getAtomicNo(a2) != 15) continue;
if (getBondLength(mol, ringBond) > longestDiphosphoroBondLength) {
longestDiphosphoroBondIndex = ringBond;
longestDiphosphoroBondLength = getBondLength(mol, ringBond);
}
}
if (longestDiphosphoroBondIndex == -1) return false;
// TODO: eventually replace the hardcoded value with one based on statistics
// Explanation: the current cutoff values of 2.42 A was selected
// after analysing about a hundred structures that were assigned
// extremely long diphosphorus bonds. The longest correct
// disphophorus bond in a three-membered ring with the bond
// length of 2.409 was observed in COD entry 7107333.
if (longestDiphosphoroBondLength < 2.42) return false;
mol.markBondForDeletion(longestDiphosphoroBondIndex);
return true;
}
/**
* Evaluates if the given three-membered ring contains an unlikely bond
* that involves a hydrogen atom.
*
* Such incorrect bonds are placed in molecules where a hydrogen atom
* is positioned withing a bonding distance of two atoms. For example,
* COD entry 7027071 contains a hydrogen atom ('H4C') that is within
* a 0.8 A distance of the nitrogen atom ('N4') and a 1.46 A distance
* of a carbon atom ('C8'). As a result, an incorrect bond between
* the 'H4C' and 'C8' atoms gets placed forming a three-membered ring
* (C1HN1).
*
* @param mol
* Molecule that contains the ring.
* @param ringIndex
* Index of the ring in the molecule.
* @return
* Boolean value denoting if an improbable bond that involves
* a hydrogen atom was identified and marked for deletion.
*/
private static boolean markImprobableBondWithHydrogenForDeletionInThreeAtomRing(Molecule3D mol, int ringIndex){
if (mol.getRingSet().getRingSize(ringIndex) != 3) return false;
// TODO: allow two boron atoms to share a hydrogen
boolean bondMarkedForDeletion = false;
int[] ringAtoms = mol.getRingSet().getRingAtoms(ringIndex);
for (int j = 0; j < ringAtoms.length; j++) {
if (mol.getAtomicNo(ringAtoms[j]) != 1) continue;
int n1 = ringAtoms[((j - 1) + ringAtoms.length) % ringAtoms.length];
int n2 = ringAtoms[((j + 1) + ringAtoms.length) % ringAtoms.length];
int boundAtom = n1;
int unboundAtom = n2;
if (getBondLength(mol, ringAtoms[j], n1) > getBondLength(mol, ringAtoms[j], n2)) {
boundAtom = n2;
unboundAtom = n1;
}
mol.markBondForDeletion(mol.getBond(ringAtoms[j], unboundAtom));
System.err.printf(
"WARNING",
"a bond between atoms '" + mol.getAtomName(ringAtoms[j]) + "' and '" +
mol.getAtomName(unboundAtom) + "' was initially assigned based on the interatomic " +
"distance (" + "%.3f" + " nm), but was " +
"subsequently removed based on the molecular geometry -- atoms '" +
mol.getAtomName(ringAtoms[j]) + "', '" + mol.getAtomName(boundAtom) + "' and '" +
mol.getAtomName(unboundAtom) + "' are very unlikely to form a three-membered atom ring" + "\n",
getBondLength(mol, ringAtoms[j], unboundAtom)/10
);
bondMarkedForDeletion = true;
}
return bondMarkedForDeletion;
}
/**
* Calculates the geometrical center of the atom ring.
* @param mol
* The molecule that contains the ring.
* @param ringIndex
* The ring index in the molecule.
* @return
* Coordinates of the geometrical center of the atom ring.
*/
private static Coordinates getAtomRingCenter(Molecule3D mol, int ringIndex) {
Coordinates c = new Coordinates();
for ( int i: mol.getRingSet().getRingAtoms(ringIndex) ) {
c.x += mol.getAtomX(i);
c.y += mol.getAtomY(i);
c.z += mol.getAtomZ(i);
}
int ringAtomCount = mol.getRingSet().getRingAtoms(ringIndex).length;
c.x /= ringAtomCount;
c.y /= ringAtomCount;
c.z /= ringAtomCount;
return c;
}
/**
* Calculates for the organic subset of atoms the maximum number of neighbors
* @param mol
* The molecule that contains the atom.
* @param atom
* The index of the atom in the molecule.
* @return
* The maximum neighbour count for the atom.
*/
private static int maxNeighborCount(Molecule3D mol, int atom) {
int atomicNo = mol.getAtomicNo(atom);
return atomicNo == 5 ? 6 // B
: atomicNo <= 7 ? 4 // N, C
: atomicNo == 8 ? 3 // O
: atomicNo == 9 ? 1 // F
: atomicNo == 14 ? 6 // Si
: atomicNo == 15 ? 6 // P
: atomicNo == 16 ? 6 // S
: atomicNo == 17 ? 4 // Cl
: atomicNo == 33 ? 6 // As
: atomicNo == 34 ? 6 // Se
: atomicNo == 35 ? 6 // Br
: atomicNo == 52 ? 6
: atomicNo == 53 ? 6 : 8;
}
/**
* Calculate the bond orders of the molecule (without knowing the hydrogens).
* The calculation is based on the bond distance between each atoms.
*
* http://www.ccp14.ac.uk/ccp/web-mirrors/i_d_brown/valence.txt
* s = exp((Ro - R)/B)
*
* The implementation of this method is also most similar to the algorithm
* described in:
* http://www.daylight.com/meetings/mug01/Sayle/m4xbondage.html
*
* @param mol
* The molecule for which the bond orders should be calculated.
*/
public static void calculateBondOrders(Molecule3D mol) {
mol.ensureHelperArrays(Molecule.cHelperRings);
/*
* Pass 1: assign higher order bonds in unambiguous locations
*/
assignObviousHigherOrderBonds(mol);
// Place disilene bonds
assignDisileneBonds(mol);
//////////////////////////////////
// Functional Group Recognition
// Hybridization State Determination
resolveKnownIons(mol);
int[] spOrder = calculateHybridizationStates(mol);
recogniseFunctionalGroups(mol, spOrder);
// FIXME: this method should eventually be moved to the start of the method
resolveSharedHydrogenAtoms(mol);
// hybridization states need to be recalculated since
// the resolveSharedHydrogenAtoms() method might affect
// the order of atoms in the array
spOrder = calculateHybridizationStates(mol);
boolean[] visited = new boolean[mol.getAllBonds()];
if (DEBUG) {
System.err.println("> DEBUG: entering preliminary pass phase");
}
//Preliminary pass: process obvious bonds outside rings
for (int i = 0; i < mol.getBonds(); i++) {
if (mol.getBondType(i) == Molecule.cBondTypeMetalLigand) continue;
if (mol.isRingBond(i)) continue;
int a1 = mol.getBondAtom(0, i);
int a2 = mol.getBondAtom(1, i);
if (mol.getLowestFreeValence(a1) < 2) continue;
if (mol.getLowestFreeValence(a2) < 2) continue;
// if (!isPlanar(mol, a1, a2)) continue;
if (spOrder[a1] != 1 || spOrder[a2] != 1) continue;
int index = BondLengthSet.getBondIndex(3, false, false, mol.getAtomicNo(a1), mol.getAtomicNo(a2), 2, 2);
if (index == -1) continue;
double bondLength = BondLengthSet.getBondLength(index);
double bondLengthStd = BondLengthSet.getBondStdDev(index);
// Used to be 3 order > TRIPLE_BOND_CUTOFF
if (bondLength + bondLengthStd > getBondLength(mol, i)) {
mol.setBondOrder(i, 3);
visited[i] = true;
if (DEBUG) {
System.err.println(">> DEBUG: placing a triple bond between atoms '" +
mol.getAtomName(mol.getBondAtom(0, i)) + "' and '" +
mol.getAtomName(mol.getBondAtom(1, i)) + "' based on " +
"the geometry of the molecule and bond length statistics.");
}
}
}
if (DEBUG) {
System.err.println("> DEBUG: exiting preliminary pass phase");
}
identifySquarates(mol, spOrder);
if (DEBUG) {
System.err.println("> DEBUG: entering exocyclic double bond detection phase.");
}
// go through all exocyclic atoms and set the most likely double bonds
boolean[] isNonAromatic = assignExocyclicDoubleBonds(mol, spOrder);
// check exocyclic atoms that are part of non-aromatic rings
for (int i = 0; i < mol.getRingSet().getSize(); i++) {
if (isNonAromatic[i]) continue;
int[] ringAtoms = mol.getRingSet().getRingAtoms(i);
for (int ringAtom : ringAtoms) {
if (mol.getLowestFreeValence(ringAtom) < 1) continue;
for (int j = 0; j < mol.getConnAtoms(ringAtom); j++) {
// check if atom is exocyclic and capable of a double bond
int exoAtom = mol.getConnAtom(ringAtom, j);
if (mol.getAtomCharge(exoAtom) != 0) continue;
if (mol.getLowestFreeValence(exoAtom) < 1) continue;
if (spOrder[exoAtom] != 2) continue;
// if ( mol.getAtomicNo(exoAtom) == 8 ) continue;
// Check if the neighbour atom doesn't belong to the same ring
if (isRingAtom(mol, i, exoAtom)) continue;
boolean allowsDoubleBonds = true;
if (mol.isRingAtom(exoAtom)) {
for (int k = 0; k < mol.getRingSet().getSize(); k++) {
if (isRingAtom(mol, k, exoAtom) && !isNonAromatic[k]) {
allowsDoubleBonds = false;
break;
}
}
}
if (!allowsDoubleBonds) continue;
if (!isPlanar(mol, ringAtom, exoAtom)) continue;
int minPiCount = (mol.getConnAtoms(exoAtom) > 1) ? 1 : 0;
double averageSingleBondLength = getIdealisedBondLength(1, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, minPiCount);
double averageDoubleBondLength = getIdealisedBondLength(2, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, 1);
double averageDoubleBondStd = BondLengthSet.getBondStdDev(BondLengthSet.getBondIndex(2, false, false, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, 1));
double calculatedLength = getBondLength(mol, ringAtom, exoAtom);
double doubleBondDiff = StrictMath.abs(averageDoubleBondLength - calculatedLength);
if (averageSingleBondLength < averageDoubleBondLength) {
if (DEBUG) {
System.err.println(">>> DEBUG: single '" + mol.getAtomicNo(ringAtom) + "-" +
mol.getAtomicNo(exoAtom) + "' bond seems to be shorter than a the " +
"double bond of the same type -- skipping the estimation of this bond.");
}
continue;
}
if (doubleBondDiff < 0 || doubleBondDiff <= averageDoubleBondStd * 1.5) {
mol.setBondOrder(mol.getBond(ringAtom, exoAtom), 2);
if (DEBUG) {
System.err.println(">>> DEBUG: placing a double bond between atoms '" +
mol.getAtomName(ringAtom) + "' and '" + mol.getAtomName(exoAtom) + "'.");
}
}
}
}
}
if (DEBUG) {
System.err.println("> DEBUG: exiting exocyclic ring atom double bond detection phase.");
}
/////////////////////////////////////////////////////////
// Aromatic Ring Perception
// This procedure calculates a normal to the ring and check that all
// atoms in the ring and their neighbours are within the plane defined by the normal
if ( DEBUG ) {
System.err.println("> DEBUG: entering aromatic ring perception phase");
}
RingCollection ringSet = mol.getRingSet();
boolean[] aromaticRing = findAromaticRings(mol, spOrder);
for (int i = 0; i < ringSet.getSize(); i++) {
if ( aromaticRing[i] ) {
boolean isResolved = false;
if (resolveTetraatomicAromaticRingIons(mol,i)) {
isResolved = true;
// Cyclothiaselenazenium
} else if ( ringSet.getRingSize(i) == 7 ) {
int NSBondCount = 0;
int SSBondCount = 0;
for (int bond : ringSet.getRingBonds(i)) {
if ( mol.getAtomicNo(mol.getBondAtom(0, bond) ) == 16 &&
mol.getAtomicNo(mol.getBondAtom(1, bond) ) == 16 ) {
SSBondCount++;
} else if (
( mol.getAtomicNo(mol.getBondAtom(0, bond)) == 7 &&
mol.getAtomicNo(mol.getBondAtom(1, bond)) == 16 ) ||
( mol.getAtomicNo(mol.getBondAtom(0, bond)) == 16 &&
mol.getAtomicNo(mol.getBondAtom(1, bond)) == 7 ) ) {
NSBondCount++;
} else {
break;
}
}
if ( NSBondCount == 6 && SSBondCount == 1 ) {
int SSBond = -1;
int ringSize = ringSet.getRingSize(i);
for (int j = 0; j < ringSet.getRingBonds(i).length; j++) {
int bond = ringSet.getRingBonds(i)[j];
if ( mol.getAtomicNo(mol.getBondAtom(0, bond) ) == 16 &&
mol.getAtomicNo(mol.getBondAtom(1, bond) ) == 16 ) {
SSBond = j;
break;
}
}
if (SSBond == -1) {
throw new RuntimeException("the S-S bond could not be found even though " +
"it was previously detected.");
} else {
int doubleBond1 = ringSet.getRingBonds(i)[(SSBond - 2 + ringSize) % ringSize];
int doubleBond2 = ringSet.getRingBonds(i)[(SSBond + 2 + ringSize) % ringSize];
int chargedNBond = ringSet.getRingBonds(i)[(SSBond + 3 + ringSize) % ringSize];
mol.setBondOrder(doubleBond1, 2);
int sulphurAtom = mol.getAtomicNo(mol.getBondAtom(0,doubleBond1)) == 16 ? 0 : 1;
mol.setAtomCharge(mol.getBondAtom(sulphurAtom, doubleBond1), 1);
mol.setBondOrder(doubleBond2, 2);
sulphurAtom = mol.getAtomicNo(mol.getBondAtom(0,doubleBond2)) == 16 ? 0 : 1;
mol.setAtomCharge(mol.getBondAtom(sulphurAtom, doubleBond2), 1);
int nitrogenAtom = mol.getAtomicNo(mol.getBondAtom(0,doubleBond1)) == 7 ? 0 : 1;
mol.setAtomCharge(mol.getBondAtom(nitrogenAtom, chargedNBond), 1);
}
isResolved = true;
}
}
if (!isResolved) {
for (int bond : ringSet.getRingBonds(i)) {
mol.setBondType(bond, Molecule3D.cBondTypeDelocalized);
}
}
}
}
//Aromatizer
AromaticityResolver resolver = new AromaticityResolver(mol);
resolver.locateDelocalizedDoubleBonds(null, true, true);
mol.ensureHelperArrays(Molecule3D.cHelperRings);
if (DEBUG) {
for (int i = 0; i < ringSet.getSize(); i++) {
if ( aromaticRing[i] ) {
System.err.println(">> DEBUG: entering ring number " + i );
for (int bond : ringSet.getRingBonds(i)) {
System.err.println(">>> DEBUG: aromatic bond between atoms " +
mol.getAtomName(mol.getBondAtom(0, bond) )+ "' and '" +
mol.getAtomName(mol.getBondAtom(1, bond) ) + "' was marked as a " +
"bond of order " + mol.getBondOrder(bond));
}
}
}
System.err.println("> DEBUG: exiting aromatic ring perception phase");
}
assignObviousHigherOrderBonds(mol);
// Find pi bonds
if ( DEBUG ) {
System.err.println("> DEBUG: entering pi bond detection phase");
}
boolean[] visitedAtoms = new boolean[mol.getAtoms()];
List piBonds = new LinkedList<>();
for (int i = 0; i < mol.getAtoms(); i++) {
// skip atoms that cannot accommodate an additional double bond
if (mol.getConnAtoms(i) < 2) continue;
if (spOrder[i] != 2) continue;
if (mol.getAtomPi(i) != 0) continue;
if (mol.isAromaticAtom(i)) continue;
if (mol.getAtomCharge(i) != 0) continue;
if (mol.getLowestFreeValence(i) < 1) continue;
visitedAtoms[i] = true;
for (int j = 0; j < mol.getConnAtoms(i); j++) {
int a1 = mol.getConnAtom(i, (j + 1) % mol.getConnAtoms(i));
int a2 = mol.getConnAtom(i, j);
if (visitedAtoms[a2]) continue;
// skip atoms that cannot accommodate an additional double bond
if (spOrder[a2] != 2) continue;
if (mol.getAtomPi(a2) != 0) continue;
if (mol.getAtomCharge(a2) != 0) continue;
if (mol.getLowestFreeValence(a2) < 1) continue;
if (!fitsAromaticLengthConstraints(mol, i, a2)) continue;
Coordinates origin = mol.getCoordinates(i);
Coordinates normal = calculatePlaneNormal(origin, mol.getCoordinates(a1), mol.getCoordinates(a2));
if (normal.distSq() == 0) continue;
boolean isFlat = true;
// TODO: join them in a list?
for (int k = 0; k < mol.getConnAtoms(i); k++) {
if (calculatePointToPlaneDistance(normal, origin, mol.getCoordinates(mol.getConnAtom(i, k))) > 0.4) {
isFlat = false;
break;
}
}
for (int k = 0; k < mol.getConnAtoms(a2); k++) {
if (calculatePointToPlaneDistance(normal, origin, mol.getCoordinates(mol.getConnAtom(a2, k))) > 0.4) {
isFlat = false;
break;
}
}
if (isFlat) {
piBonds.add(mol.getConnBond(i, j));
if (DEBUG) {
System.err.println(">> DEBUG: bond between atoms '" + mol.getAtomName(i) +
"' and '" + mol.getAtomName(mol.getConnAtom(i, j)) + "' has been marked " +
"as delocalized");
}
}
}
}
if ( DEBUG ) {
System.err.println("> DEBUG: exiting pi bond detection phase");
}
for (int piBond : piBonds) {
mol.setBondType(piBond, Molecule3D.cBondTypeDelocalized);
}
mol.ensureHelperArrays(Molecule3D.cHelperRings);
List> fragments = getPiBondFragments(mol);
for (List fragment : fragments) {
if (!(mol.getLowestFreeValence(fragment.get(0)) > 0 &&
mol.getLowestFreeValence(fragment.get(fragment.size() - 1)) > 0)) {
continue;
}
// if the fragment is capable of interchanging single-double bonds
// then the more likely pattern should be selected
double[] singleBonds = new double[fragment.size() - 1];
double[] doubleBonds = new double[fragment.size() - 1];
for (int j = 0; j < fragment.size() - 1; j++) {
singleBonds[j] = estimatePiBondFitness(mol, fragment.get(j), fragment.get(j + 1), 1);
doubleBonds[j] = estimatePiBondFitness(mol, fragment.get(j), fragment.get(j + 1), 2);
}
if (fragmentCanResonate(mol, fragment)) {
double leftToRight = 0;
double rightToLeft = 0;
for (int j = 0; j < fragment.size() - 1; j++) {
// start with a single bond
leftToRight += (j % 2) == 0 ? singleBonds[j] : doubleBonds[j];
// start with a double bond
rightToLeft += (j % 2) == 0 ? doubleBonds[j] : singleBonds[j];
}
if (fragment.size() % 2 == 1) {
for (int j = 0; j < fragment.size() - 1; j++) {
int bondOrder;
if (leftToRight < rightToLeft) {
bondOrder = (j % 2 == 0 ? 1 : 2);
} else {
bondOrder = (j % 2 == 0 ? 2 : 1);
}
mol.setBondOrder(mol.getBond(fragment.get(j), fragment.get(j + 1)), bondOrder);
if (DEBUG) {
System.err.println(">>> DEBUG: a bond of order " + bondOrder + " was placed between " +
"atoms '" + mol.getAtomName(fragment.get(j)) + "' and '" +
mol.getAtomName(fragment.get(j + 1)) + "' outside of the aromatizer");
}
}
}
} else {
for (int j = 0; j < fragment.size() - 1; j++) {
if (doubleBonds[j] < 0.03) {
mol.setBondOrder(mol.getBond(fragment.get(j), fragment.get(j + 1)), 2);
}
}
}
}
mol.ensureHelperArrays(Molecule3D.cHelperRings);
resolver = new AromaticityResolver(mol);
resolver.locateDelocalizedDoubleBonds(null, true, true);
mol.ensureHelperArrays(Molecule3D.cHelperRings);
if ( DEBUG ) {
for (int piBond : piBonds) {
int a1 = mol.getBondAtom(0, piBond);
int a2 = mol.getBondAtom(1, piBond);
System.err.println(">>>> DEBUG: a bond of order " + mol.getBondOrder(piBond) + " was " +
"placed between atoms '" + mol.getAtomName(a1) + "' and '" + mol.getAtomName(a2) +
"'.");
}
}
// Try to recognise the N-C-N pattern and set the charges accordingly
int index = BondLengthSet.getBondIndex(1, true, false, 6, 7, 1 , 1);
double bondLength = BondLengthSet.getBondLength(index);
double bondStd = BondLengthSet.getBondStdDev(index);
for ( int i = 0; i < mol.getAtoms(); i++ ) {
if ( mol.getAtomicNo(i) == 6 && spOrder[i] == 2 && mol.getConnAtoms(i) > 1 ) {
for (int j = 0; j < mol.getConnAtoms(i); j++) {
int a1 = mol.getConnAtom(i, j);
int a2 = mol.getConnAtom(i, (j + 1) % mol.getConnAtoms(i));
if (spOrder[a1] != 2 || spOrder[a2] != 2) continue;
if (mol.getAtomicNo(a1) != 7 || mol.getAtomicNo(a2) != 7) continue;
double a1Distance = getBondLength(mol, i, a1);
double a2Distance = getBondLength(mol, i, a2);
if ( ( a1Distance > bondLength + 3 * bondStd ) ||
mol.getBondOrder(mol.getBond(i, a1)) == 2 ) continue;
if ( ( a2Distance > bondLength + 3 * bondStd ) ||
mol.getBondOrder(mol.getBond(i, a1)) == 2 ) continue;
int shorterBondNitrogen = a1Distance < a2Distance ? a1 : a2;
// TODO: add an angle constraint
if ( mol.getAtomPi(i) == 0 ) {
if ( mol.getAtomCharge(a1) != 0 || mol.getAtomCharge(a2) != 0 ) continue;
if ( mol.getAtomPi(a1) != 0 || mol.getAtomPi(a2) != 0 ) continue;
if ( mol.getBondOrder(mol.getBond(i, a1)) < 2 &&
mol.getBondOrder(mol.getBond(i, a2)) < 2) {
mol.setBondOrder(mol.getBond(i, shorterBondNitrogen), 2);
mol.setAtomCharge(shorterBondNitrogen, +1);
mol.ensureHelperArrays(Molecule.cHelperRings);
}
} else {
// Switch N-C=N to N=C-N in if the latter resonance form seems more likely
// exocyclic aromatic resonance should not be considered
if ( mol.isAromaticAtom(a1) != mol.isAromaticAtom(a2) ) continue;
int longerBondNitrogen = ( shorterBondNitrogen == a1 ) ? a2 : a1;
if ( mol.getBondOrder(mol.getBond(i, longerBondNitrogen)) == 2 &&
mol.getAtomCharge(longerBondNitrogen) == 1 &&
mol.getAtomPi(shorterBondNitrogen) == 0 ) {
mol.setBondOrder(mol.getBond(i, longerBondNitrogen), 1);
mol.setBondOrder(mol.getBond(i, shorterBondNitrogen), 2);
mol.setAtomCharge(longerBondNitrogen, 0);
mol.setAtomCharge(longerBondNitrogen, +1);
mol.ensureHelperArrays(Molecule.cHelperRings);
}
}
}
}
}
mol.ensureHelperArrays(Molecule.cHelperRings);
/*
* 2nd pass: find obvious double bonds on sp and sp2 B, C and N outside aromatic rings
*/
assignObviousDoubleBonds(mol, spOrder);
/*
* 3rd pass: double and triple bonds that correspond to the bond length
* (but might deviate from the expected geometry)
*/
IntQueue queue = new IntQueue();
for (int i = 0; i < mol.getAllBonds(); i++) {
if ( mol.getBondType(i) == Molecule3D.cBondTypeMetalLigand ) continue;
if (!visited[i] && !mol.isAromaticBond(i)) queue.push(i);
while (!queue.isEmpty()) {
int bnd = queue.pop();
if (visited[bnd]) continue;
visited[bnd] = true;
int a1 = mol.getBondAtom(0, bnd);
int a2 = mol.getBondAtom(1, bnd);
if ( a1 >= spOrder.length || a2 >= spOrder.length) continue;
// Push the neighbour bonds into the queue, but
// avoid processing hydrogen and metal atoms
for (int j = 0; j < mol.getConnAtoms(a1); j++) {
queue.push(mol.getConnBond(a1, j));
}
for (int j = 0; j < mol.getConnAtoms(a2); j++) {
queue.push(mol.getConnBond(a2, j));
}
//Compute the free valence and increase the bond order if needed
double realBondLength = getBondLength(mol, a1, a2);
int doubleBondIndex = BondLengthSet.getBondIndex(2, false, false, mol.getAtomicNo(a1), mol.getAtomicNo(a2), 1, 1);
if ( doubleBondIndex == -1 ) continue;
double idealisedDoubleBondLength = BondLengthSet.getBondLength(doubleBondIndex);
double idealisedDoubleBondStdDev = BondLengthSet.getBondStdDev(doubleBondIndex);
if ( idealisedDoubleBondStdDev == BondLengthSet.DEFAULT_BOND_STDDEV ) {
idealisedDoubleBondStdDev = 0.1;
}
if ( realBondLength < idealisedDoubleBondLength + 3 * idealisedDoubleBondStdDev &&
!mol.isAromaticAtom(a1) && !mol.isAromaticAtom(a2) ) {
//Special case CS
if (mol.getAtomicNo(a1) == 16 && mol.getAllConnAtoms(a1) <= 2) continue;
if (mol.getAtomicNo(a2) == 16 && mol.getAllConnAtoms(a2) <= 2) continue;
if ( mol.getAtomCharge(a1) != 0 || mol.getAtomCharge(a2) != 0 ) continue;
int freeValence1 = StrictMath.max( mol.getLowestFreeValence(a1),
StrictMath.max( ( mol.getAtomicNo(a1) == 7 && spOrder[a1] < 3 ) ? 1 : 0,
( mol.getAtomicNo(a1) == 5 && spOrder[a1] < 3 ) ? 1 : 0 ) );
int freeValence2 = StrictMath.max( mol.getLowestFreeValence(a2),
StrictMath.max( ( mol.getAtomicNo(a2) == 7 && spOrder[a2] < 3 ) ? 1 : 0,
( mol.getAtomicNo(a2) == 5 && spOrder[a2] < 2 ) ? 1 : 0 ) );
if ( freeValence1 < 1 || freeValence2 < 1 ) continue;
boolean aligned = ( spOrder[a1] == 1 && spOrder[a2] == 1 );
boolean fitsTripleBondLength;
int tripleBondIndex = BondLengthSet.getBondIndex(3, false, false, mol.getAtomicNo(a1), mol.getAtomicNo(a2), 2, 2);
if ( tripleBondIndex != -1 ) {
double idealisedTripleBondLength = BondLengthSet.getBondLength(tripleBondIndex);
double idealisedTripleBondStdDev = BondLengthSet.getBondStdDev(tripleBondIndex);
if (idealisedTripleBondStdDev == BondLengthSet.DEFAULT_BOND_STDDEV) {
idealisedTripleBondStdDev = 0.1;
}
fitsTripleBondLength = realBondLength > idealisedTripleBondLength + 3 * idealisedTripleBondStdDev;
} else {
fitsTripleBondLength = realBondLength < idealisedDoubleBondLength;
}
if ( aligned && mol.getAtomPi(a1) < 2 && mol.getAtomPi(a2) < 2 && fitsTripleBondLength ) {
mol.setBondOrder(bnd, 3);
} else if ( mol.getAtomPi(a1) < ( spOrder[a1] == 1 ? 2 : 1 ) &&
mol.getAtomPi(a2) < ( spOrder[a2] == 1 ? 2 : 1 ) ) {
mol.setBondOrder(bnd, 2);
}
}
}
}
mol.ensureHelperArrays(Molecule3D.cHelperRings);
/*
* 4th pass: increase bond order if the geometry requires it
* (even though the bond length might seem too great)
*/
int[] localAtomPi = getAtomPiCount(mol);
for (int i = 0; i < mol.getAtoms(); i++) {
if (!canAccommodateDoubleBond(mol, i, localAtomPi[i], spOrder[i])) continue;
// sp3 atoms smaller than Si should not have double bonds
if (spOrder[i] == 3 && mol.getAtomicNo(i) < 14) continue;
for (int j = 0; j < mol.getConnAtoms(i); j++) {
int a1 = mol.getConnAtom(i, j);
if (canAccommodateDoubleBond(mol, a1, localAtomPi[a1], spOrder[a1])) {
mol.setBondOrder(mol.getBond(i, a1), 2);
localAtomPi[i] += 1;
localAtomPi[a1] += 1;
}
if (localAtomPi[i] >= (spOrder[i] == 1 ? 2 : 1)) break;
}
}
// 5th pass: redistributing charges
/*
* v0 is the start atom
* v1 is the 1st order neighbour of atom v0
* v2 is the 2nd order neighbour of atom v0
* v3 is the 3rd order neighbour of atom v0
*/
localAtomPi = getAtomPiCount(mol);
for (int v0 = 0; v0 < mol.getAllAtoms(); v0++) {
if ( mol.getAtomicNo(v0) == 6 ) {
// C[-]-
if ( spOrder[v0] != 2 ) continue;
if ( mol.getLowestFreeValence(v0) != 1 ) continue;
if ( localAtomPi[v0] != 0 ) continue;
if ( hasMetalLigandBonds(mol, v0) ) continue;
// O=C-C(-)-C=O -> O=C-C=C-O(-)
List terminalOxygens = new LinkedList<>();
// C(-)-N -> C->N(+)
List sp2NitrogenNeighbours = new LinkedList<>();
for (int j = 0; j < mol.getConnAtoms(v0); j++) {
int v1 = mol.getConnAtom(v0, j);
if ( mol.getAtomicNo(v1) == 6 ) {
// C[-]-C(R)=
if ( spOrder[v1] != 2 ) continue;
if ( localAtomPi[v1] == 0 ) continue;
for (int k = 0; k < mol.getConnAtoms(v1); k++) {
int v2 = mol.getConnAtom(v1, k);
// select the second order neighbour atom that is double bonded
// to the first order neighbour atom
if ( v2 == v0 ) continue;
if ( mol.getBondOrder(mol.getConnBond(v1, k)) != 2 ) continue;
if ( mol.getAtomicNo(v2) == 8 &&
mol.getConnAtoms(v2) == 1 ) {
terminalOxygens.add(v2);
} else if ( mol.getAtomicNo(v2) == 6 ) {
// C[-]-C(R)=C(R)(R)
for ( int l = 0; l < mol.getConnAtoms(v2); l++ ) {
if ( localAtomPi[v0] > 0 ) break;
int v3 = mol.getConnAtom(v2, l);
if ( v1 == v3 ) continue;
if ( mol.getAtomicNo(v3) != 6 ) continue;
if ( spOrder[v3] != 2 ) continue;
if ( mol.getLowestFreeValence(v3) != 1 ) continue;
if ( hasMetalLigandBonds(mol, v3) ) continue;
mol.setBondOrder(mol.getBond(v0, v1), 2);
mol.setBondOrder(mol.getBond(v1, v2), 1);
mol.setBondOrder(mol.getBond(v2, v3), 2);
localAtomPi[v0]++;
localAtomPi[v3]++;
break;
}
}
}
} else if (mol.getAtomicNo(v1) == 7 && spOrder[v1] == 2 &&
mol.getAtomPi(v1) == 0 && mol.getImplicitHydrogens(v1) == 0) {
sp2NitrogenNeighbours.add(j);
}
}
// FIXME: the valency of associated C atoms might change due to previous loop
// This needs serious refactoring
// moving charges from carbon atoms to oxygen atoms where possible
if ( mol.getLowestFreeValence(v0) > 0 ) {
if (terminalOxygens.size() > 0 ) {
mol.setBondOrder(mol.getConnBond(terminalOxygens.get(0), 0), 1);
mol.setBondOrder(mol.getBond(mol.getConnAtom(terminalOxygens.get(0), 0), v0), 2);
} else if (sp2NitrogenNeighbours.size() > 0) {
int[] sortedAtom = sortAtomsByBondLength(mol, v0, sp2NitrogenNeighbours);
mol.setBondOrder(mol.getConnBond(v0, sortedAtom[0]), 2);
// nitrogen atoms may contain a negative charge that was assigned during previous processing
int nAtom = mol.getConnAtom(v0, sortedAtom[0]);
if (mol.getAtomCharge(nAtom) < 0) {
mol.setAtomCharge(nAtom, mol.getAtomCharge(nAtom) + 1);
}
}
}
} else if ( mol.getAtomicNo(v0) == 16 ) {
// Sp2 sulphur with +1 charge with no metal bonds
if ( spOrder[v0] != 2 ) continue;
if ( mol.getAtomPi(v0) == 0 ) continue;
if ( mol.getAtomCharge(v0) != 1 ) continue;
if ( hasMetalLigandBonds(mol, v0) ) continue;
// (S+)=C-C=(S+) -> S-C=C-S
// (S+)=C-C -> (S+)-C=C
for (int j = 0; j < mol.getConnAtoms(v0); j++) {
// Sp2 carbon that is double bonded to the v0 sulphur
int v1 = mol.getConnAtom(v0, j);
if ( mol.getAtomicNo(v1) != 6 ) continue;
if ( mol.getBondOrder( mol.getBond(v0, v1) ) != 2 ) continue;
for (int k = 0; k < mol.getConnAtoms(v1); k++) {
// Sp2 carbon that is single bonded to the v1 carbon
int v2 = mol.getConnAtom(v1, k);
if ( mol.getAtomicNo(v2) != 6 ) continue;
if ( mol.getBondOrder(mol.getBond(v1,v2)) != 1 ) continue;
if ( mol.getLowestFreeValence(v2) == 1 ) {
mol.setBondOrder(mol.getBond(v1, v2), 2);
mol.setBondOrder(mol.getBond(v0, v1), 1);
mol.setAtomCharge(v0, 0);
} else if (mol.getAtomPi(v2) != 0){
for (int l = 0; l < mol.getConnAtoms(v2); l++) {
int v3 = mol.getConnAtom(v2, l);
if ( mol.getAtomicNo(v3) != 16 ) continue;
if ( mol.getAtomCharge(v3) != 1 ) continue;
if ( mol.getBondOrder(mol.getBond(v2, v3)) != 2 ) continue;
mol.setBondOrder(mol.getBond(v0, v1), 1);
mol.setBondOrder(mol.getBond(v1, v2), 2);
mol.setBondOrder(mol.getBond(v2, v3), 1);
mol.setAtomCharge(v0, 0);
mol.setAtomCharge(v3, 0);
}
}
}
}
}
}
mol.ensureHelperArrays(Molecule.cHelperRings);
// Set charges for atoms other than metals
assignAtomChargesBasedOnValence(mol, spOrder);
/*
* Transform the molecule to acquire a more reasonable resonance structure
*/
// R1(+)=X-R2(-) -> R1-X=R2
for (int i = 0; i < mol.getAtoms(); i++) {
if ( mol.getAtomCharge(i) != 1 ) continue;
if ( mol.getAtomPi(i) == 0 ) continue;
for ( int j = 0; j < mol.getConnAtoms(i); j++ ) {
int a1 = mol.getConnAtom(i, j);
if ( mol.getAtomCharge(a1) == 0 && mol.getConnBondOrder(i, j) == 2 ) {
for (int k = 0; k < mol.getConnAtoms(a1); k++) {
int a2 = mol.getConnAtom(a1, k);
double R1XBondLengthDiff = BondLengthSet.getBondLength(
BondLengthSet.getBondIndex(2, false, false,
mol.getAtomicNo(i),
mol.getAtomicNo(a1), 1, 1) ) - getBondLength(mol, i, a1);
if ( mol.getAtomCharge(i) < 0 && spOrder[a2] == 2 &&
mol.getAtomPi(a2) == 0 &&
( mol.getAtomCharge(i) + mol.getAtomCharge(a2) ) == 0 ) {
double R2XBondLengthDiff = BondLengthSet.getBondLength(
BondLengthSet.getBondIndex(2, false, false,
mol.getAtomicNo(a1),
mol.getAtomicNo(a2), 1, 1)) - getBondLength(mol, a1, a2);
if ( R2XBondLengthDiff < R1XBondLengthDiff ) {
mol.setBondOrder(mol.getBond(a1, a2), 2);
mol.setBondOrder(mol.getBond(i, a1), 1);
mol.setAtomCharge(i, 0);
mol.setAtomCharge(a2, 0);
break;
}
}
}
}
}
}
processKnownIonsInvolvingMetals(mol);
assignMetalCharges(mol);
mol.ensureHelperArrays(Molecule.cHelperNeighbours);
}
/**
* Gets the count of used pi electrons for each atom in the molecule.
* @param mol
* The molecule that contains the bonded atoms.
* @return
* Counts of used pi electron.
*/
private static int[] getAtomPiCount(Molecule3D mol) {
int[] atomPiCount = new int[mol.getAllAtoms()];
for ( int i = 0; i < atomPiCount.length; i++ ) {
if (mol.getAtomicNo(i) == 1) {
atomPiCount[i] = 0;
} else {
atomPiCount[i] = mol.getAtomPi(i);
}
}
return atomPiCount;
}
/**
* Assigns obvious higher order bonds where the geometry explicitly requires it.
*
* @param mol
* Molecule that contains the bonded atoms.
*/
private static void assignObviousHigherOrderBonds(Molecule3D mol) {
// Place obvious triple bonds for sp atoms
boolean changeOccurred = true;
int tripleBondIterationNo = 1;
while (changeOccurred) {
if (DEBUG) {
System.err.println(">> DEBUG: entered obvious triple bond assignment iteration " +
tripleBondIterationNo + ".");
}
changeOccurred = assignObviousTripleBonds(mol);
if (DEBUG) {
System.err.println(">> DEBUG: exited obvious triple bond assignment iteration " +
tripleBondIterationNo + ".");
}
tripleBondIterationNo++;
}
}
/**
* Assigns obvious triple bonds where the geometry explicitly requires it.
*
* @param mol
* Molecule that contains the bonded atoms.
* @return
* Boolean value denoting if any triple bonds were assigned.
*/
private static boolean assignObviousTripleBonds(Molecule3D mol) {
// the local pi count array is used to avoid calling ensureHelperArrays()
// each time a triple bond is assigned
boolean changeOccured = false;
int[] localAtomPi = getAtomPiCount(mol);
for (int i = 0; i < mol.getAtoms(); i++) {
if (mol.isMetalAtom(i)) continue;
if (localAtomPi[i] != 0) continue;
if (calculateHybridizationState(mol, i) != 1) continue;
if (mol.getAllConnAtoms(i) != 2) continue; // skip terminal atoms
boolean allowNeighbourCharge = true;
if (!canAccommodateTripleBond(mol, i, false)) {
if (!canAccommodateTripleBond(mol, i, true)) continue;
allowNeighbourCharge = false;
}
int a1 = mol.getConnAtom(i, 0);
int a2 = mol.getConnAtom(i, 1);
// skip if both atoms are capable of at least a single double bond
boolean a1CanCarryDoubleBond = canAccommodateDoubleBond(mol, a1, localAtomPi[a1], calculateHybridizationState(mol, a1));
boolean a2CanCarryDoubleBond = canAccommodateDoubleBond(mol, a2, localAtomPi[a2], calculateHybridizationState(mol, a2));
boolean a1CanCarryTripleBond = canAccommodateTripleBond(mol, a1, allowNeighbourCharge);
boolean a2CanCarryTripleBond = canAccommodateTripleBond(mol, a2, allowNeighbourCharge);
if ((a1CanCarryDoubleBond || a1CanCarryTripleBond) &&
(a2CanCarryDoubleBond || a2CanCarryTripleBond)) {
continue;
}
if (!a1CanCarryTripleBond && !a2CanCarryTripleBond) continue;
int tripleBondAtom = a1CanCarryTripleBond ? a1 : a2;
mol.setBondOrder(mol.getBond(i, tripleBondAtom), 3);
localAtomPi[i] = 2;
localAtomPi[tripleBondAtom] = 2;
changeOccured = true;
if (DEBUG) {
System.err.println(">>> DEBUG: placed a triple bond between atoms '" +
mol.getAtomName(i) + "' and '" + mol.getAtomName(tripleBondAtom) +
"' without relying on the bond length statistics.");
}
}
mol.ensureHelperArrays(Molecule.cHelperRings);
return changeOccured;
}
/**
* Evaluates is the given atom is capable of carrying a triple bond.
*
* @param mol
* Molecule that contains the atom.
* @param atom
* Index of the atom that should be evaluated.
* @param allowCharge
* Boolean value denoting if the atom is allowed to gain
* a formal charge in order to accommodate the triple bond
* (i.e. N+, B-).
* @return
* Boolean value denoting if the atom is capable of carrying a triple bond.
*/
private static boolean canAccommodateTripleBond(Molecule3D mol, int atom, boolean allowCharge) {
if (mol.getAtomicNo(atom) == 1) return false;
if (mol.isHalogene(atom)) return false;
int requiredValence = 2;
if (allowCharge) {
// boron (B) with the charge of -1 may carry a triple bond
if (mol.getAtomicNo(atom) == 5 ) requiredValence = 1;
// nitrogen (N) with the charge of +1 may carry a triple bond
if (mol.getAtomicNo(atom) == 7 ) requiredValence = 1;
}
if (mol.getLowestFreeValence(atom) < requiredValence) return false;
int spOrder = calculateHybridizationState(mol, atom);
return spOrder == 1;
}
/**
* Assigns obvious double bonds to boron, nitrogen and carbon atoms.
*
* @param mol
* The molecule that contains the bonded atoms.
* @param spOrder
* The sp hybridization type for each atom.
*/
private static void assignObviousDoubleBonds(Molecule3D mol, int[] spOrder) {
// using the local pi count array to avoid calling ensureHelperArrays
// each time a double bond is assigned
int[] atomPi = getAtomPiCount(mol);
for (int i = 0; i < mol.getAtoms(); i++) {
if ( mol.isAromaticAtom(i) ) continue;
if ( mol.getAtomCharge(i) != 0 ) continue;
// B, N, C with valence lower than 4
if ( mol.getAtomicNo(i) > 3 && mol.getAtomicNo(i) < 8 && mol.getOccupiedValence(i) < 4 ) {
if ( spOrder[i] == 1 ) {
for (int j = 0; j < mol.getConnAtoms(i); j++) {
int a1 = mol.getConnAtom(i, j);
if ( mol.getLowestFreeValence(a1) > 0 &&
mol.getAtomCharge(a1) == 0 &&
spOrder[a1] < 3 && atomPi[a1] <
( spOrder[a1] == 1 ? 2 :
spOrder[a1] == 2 ? 1 : 0 ) ) {
mol.setBondOrder(mol.getConnBond(i, j), 2);
atomPi[i]++;
atomPi[a1]++;
}
}
} else if (spOrder[i] == 2) {
if (atomPi[i] != 0) continue;
List potentialDoubleBondNeighbours = new LinkedList<>();
// nitrogen atoms with filled valencies are handled a little differently
boolean isFilledNitrogenAtom = mol.getAtomicNo(i) == 7 && mol.getLowestFreeValence(i) == 0;
for (int j = 0; j < mol.getConnAtoms(i); j++) {
int neighbour = mol.getConnAtom(i, j);
if ( mol.getAtomCharge(neighbour) != 0 ) continue;
int freeValence = StrictMath.max( mol.getLowestFreeValence(neighbour),
mol.getAtomicNo(neighbour) == 5 && spOrder[neighbour] < 3 ? 1 : 0 );
if ( freeValence < 1 ) continue;
if ( isFilledNitrogenAtom && mol.getAtomicNo(neighbour) == 7 && spOrder[neighbour] == 2 ) continue;
if ( atomPi[neighbour] < ( spOrder[neighbour] == 1 ? 2 :
spOrder[neighbour] == 2 ? 1 : 0 ) ||
mol.getAtomicNo(neighbour) == 15 ) {
potentialDoubleBondNeighbours.add(j);
}
}
if ( potentialDoubleBondNeighbours.size() > 1 ) {
double minDiff = -Double.MAX_VALUE;
int minDiffNeighbour = -1;
for (Integer neighbourIndex : potentialDoubleBondNeighbours) {
int neighbour = mol.getConnAtom(i, neighbourIndex);
double singleBondLength =BondLengthSet.getBondLength(
BondLengthSet.getBondIndex(1, false, false, mol.getAtomicNo(i), mol.getAtomicNo(neighbour), 1, 1));
double doubleBondLength = BondLengthSet.getBondLength(
BondLengthSet.getBondIndex(2, false, false, mol.getAtomicNo(i), mol.getAtomicNo(neighbour), 1, 1));
// TODO: think of a more legitimate way of detecting statistical anomalies
// safeguard against statistical anomalies
if ( singleBondLength < doubleBondLength || singleBondLength - doubleBondLength < 0.1 ) {
continue;
}
double bondLengthDifference = doubleBondLength - getBondLength(mol, i, neighbour);
if (bondLengthDifference > minDiff) {
minDiff = bondLengthDifference;
minDiffNeighbour = neighbour;
}
}
if (minDiffNeighbour > -1) {
mol.setBondOrder(mol.getBond(i, minDiffNeighbour), 2);
atomPi[i]++;
atomPi[minDiffNeighbour]++;
}
}
}
}
}
mol.ensureHelperArrays(Molecule.cHelperRings);
}
/**
* Assigns obvious disilene bonds.
*
* @param mol
* The molecule that contains the bonded atoms.
*/
private static void assignDisileneBonds(Molecule3D mol) {
double doubleBondCutoff = 2.29;
// FIXME: properly check if a normal index is returned
int bondIndex = BondLengthSet.getBondIndex(2, false, false, 14, 14, 1, 1 );
double siliconDoubleBondLength = BondLengthSet.getBondLength( bondIndex );
double siliconDoubleBondStdDev = BondLengthSet.getBondStdDev( bondIndex );
if ( siliconDoubleBondLength != -1 && siliconDoubleBondStdDev != -1 ) {
if ( ( siliconDoubleBondLength + 3 * siliconDoubleBondStdDev ) < doubleBondCutoff ) {
doubleBondCutoff = siliconDoubleBondLength + 3 * siliconDoubleBondStdDev;
}
}
int[] atomPi = getAtomPiCount(mol);
for (int i = 0; i < mol.getAtoms(); i++) {
if ( mol.getAtomicNo(i) != 14 ) continue;
if ( atomPi[i] != 0 ) continue;
if ( mol.getOccupiedValence(i) > 3 ) continue;
if ( mol.isAromaticAtom(i) ) continue;
for (int j = 0; j < mol.getConnAtoms(i); j++) {
int a1 = mol.getConnAtom(i, j);
// ring bonds are handled in aromaticity perception stage
if ( mol.isRingBond( mol.getBond( i, a1 ) ) ) continue;
if ( mol.getAtomicNo(a1) != 14 ) continue;
if ( mol.getOccupiedValence(a1) > 3 ) continue;
if ( atomPi[a1] != 0 ) continue;
if ( mol.isAromaticAtom(a1) ) continue;
if ( getBondLength( mol, i, a1 ) < doubleBondCutoff ) {
mol.setBondOrder( mol.getBond(i, a1), 2 );
atomPi[i]++;
atomPi[a1]++;
break;
}
}
}
}
/**
* Detects and assigns exocyclic double bonds that are more likely than
* double bonds inside the rings.
* @param mol
* Molecule that contains the atoms.
* @param spOrder
* The sp hybridization type for each atom.
* @return
* Boolean values denoting is an exocyclic bond has been placed
* for the corresponding atom ring.
*/
private static boolean[] assignExocyclicDoubleBonds(Molecule3D mol, int[] spOrder) {
boolean[] hasExocyclicDoubleBond = new boolean[mol.getRingSet().getSize()];
int[] localAtomPi = getAtomPiCount(mol);
for (int i = 0; i < mol.getRingSet().getSize(); i++) {
int[] ringAtoms = mol.getRingSet().getRingAtoms(i);
for (int ringAtom : ringAtoms) {
// skip the atom if the only exocyclic neighbours are hydrogen atoms
if (mol.getConnAtoms(ringAtom) == 2) continue;
// additional exclusion rules for certain elements
if (mol.getAtomicNo(ringAtom) == 14 || // Silicon (Si)
mol.getAtomicNo(ringAtom) == 15 || // Phosphorus (P)
mol.getAtomicNo(ringAtom) == 32) { // Germanium (Ge)
// skip atoms with full valency
if (mol.getFreeValence(ringAtom) < 1) continue;
// skip atoms that already contain a double bond
if (localAtomPi[ringAtom] > 0) continue;
}
// sp3d2 P and Te
if (spOrder[ringAtom] == 5 &&
(mol.getAtomicNo(ringAtom) == 15 || mol.getAtomicNo(ringAtom) == 52)) continue;
// skip the atom if it is not capable of a double bond
if (mol.getLowestFreeValence(ringAtom) <= 0 && mol.getAtomicNo(ringAtom) < 14) {
continue;
}
// find the best fit for an aromatic double bond inside the ring
double inRingMinDeviation = Double.MAX_VALUE;
for (int j = 0; j < mol.getConnAtoms(ringAtom); j++) {
int ringAtomNeighbour = mol.getConnAtom(ringAtom, j);
if (isRingAtom(mol, i, ringAtomNeighbour)) {
int bondTypeIndex = BondLengthSet.getBondIndex(2, true, false, mol.getAtomicNo(ringAtom),
mol.getAtomicNo(ringAtomNeighbour), 1, 1);
/*
* FIXME: the logic used for exocyclic bonds involving silicon (Si) should be generalised.
* In the future the logic should be generalised for all elements to initially try
* to retrieve the aromatic double bond distance and if it is not available fall back
* to the simple double bond one.
*/
if (mol.getAtomicNo(ringAtom) == 14 || mol.getAtomicNo(ringAtomNeighbour) == 14) {
if (bondTypeIndex == -1) {
bondTypeIndex = BondLengthSet.getBondIndex(2, false, false, mol.getAtomicNo(ringAtom),
mol.getAtomicNo(ringAtomNeighbour), 1, 1);
}
}
double currentDeviation = determineExpectedBondDeviation(mol,
mol.getBond(ringAtom, ringAtomNeighbour),
bondTypeIndex);
if (currentDeviation < inRingMinDeviation) {
inRingMinDeviation = currentDeviation;
}
}
}
for (int k = 0; k < mol.getConnAtoms(ringAtom); k++) {
// check if the ring atom is still capable of
// carrying an additional double bond
if (mol.getAtomicNo(ringAtom) == 15) { // phosphorus (P)
if (localAtomPi[ringAtom] > 0) break;
}
// check if atom is exocyclic and capable of a double bond
int exoAtom = mol.getConnAtom(ringAtom, k);
if (mol.isRingBond(mol.getBond(ringAtom, exoAtom))) continue;
if (mol.getAtomCharge(exoAtom) != 0) continue;
if (mol.getLowestFreeValence(exoAtom) < 1) continue;
// additional exclusion rules for certain elements
if (mol.getAtomicNo(exoAtom) == 14 || // Silicon (Si)
mol.getAtomicNo(exoAtom) == 15 || // Phosphorus (P)
mol.getAtomicNo(exoAtom) == 32) { // Germanium (Ge)
// skip atoms with full valency
if (mol.getFreeValence(exoAtom) < 1) continue;
// skip atoms that already contains a double bond
if (localAtomPi[exoAtom] > 0) continue;
}
// exocyclic double-bonded atoms should normally be planar,
// however, some large atoms may disrupt the planarity
if (!isPlanar(mol, ringAtom, exoAtom) &&
mol.getAtomicNo(ringAtom) < 14 &&
mol.getAtomicNo(exoAtom) < 14) {
continue;
}
// FIXME: pentavalent phosphorus (P) should be considered conjungated,
// but not strictly aromatic as stated in 10.1016/j.ccr.2015.02.010
if ((spOrder[exoAtom] == 2 || getAllConnAtoms(mol, exoAtom, true) == 1)) {
// check exocyclic atom bond length
int minPiCount = (getAllConnAtoms(mol, exoAtom, true) > 1) ? 1 : 0;
double averageSingleBondLength = getIdealisedBondLength(1, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, minPiCount);
double averageSingleBondStd = BondLengthSet.getBondStdDev(BondLengthSet.getBondIndex(1, false, false, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, minPiCount));
double averageDoubleBondLength = getIdealisedBondLength(2, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, 1);
double averageDoubleBondStd = BondLengthSet.getBondStdDev(BondLengthSet.getBondIndex(2, false, false, mol.getAtomicNo(ringAtom), mol.getAtomicNo(exoAtom), 1, 1));
double calculatedLength = getBondLength(mol, ringAtom, exoAtom);
double singleBondDiff = calculatedLength - averageSingleBondLength;
double doubleBondDiff = calculatedLength - averageDoubleBondLength;
if (averageSingleBondLength < averageDoubleBondLength) {
if (DEBUG) {
System.err.println(">>> DEBUG: single '" + mol.getAtomicNo(ringAtom) + "-" +
mol.getAtomicNo(exoAtom) + "' bond seems to be shorter than a the " +
"double bond of the same type -- skipping the estimation of this bond.");
}
continue;
}
// add an exocyclic double bond is the exocyclic atom
// has no alternative way to fill its valency
if (mol.getConnAtoms(exoAtom) > 1 && !hasMetalLigandBonds(mol, exoAtom)) {
int doubleBondNeighbours = 0;
for (int j = 0; j < mol.getConnAtoms(exoAtom); j++) {
if (mol.getLowestFreeValence(mol.getConnAtom(exoAtom, j)) != 0) {
doubleBondNeighbours++;
}
}
if (doubleBondNeighbours == 1 && doubleBondDiff <= 3 * averageDoubleBondStd) {
mol.setBondOrder(mol.getBond(ringAtom, exoAtom), 2);
localAtomPi[ringAtom] += 1;
localAtomPi[exoAtom] += 1;
continue;
}
}
// skip the atom if we are more likely to find a double bond inside the ring
if (doubleBondDiff > inRingMinDeviation) continue;
if (doubleBondDiff < 0 || doubleBondDiff <= 1.5 * averageDoubleBondStd
|| StrictMath.abs(singleBondDiff) > 2 * averageSingleBondStd) {
mol.setBondOrder(mol.getBond(ringAtom, exoAtom), 2);
localAtomPi[ringAtom] += 1;
localAtomPi[exoAtom] += 1;
hasExocyclicDoubleBond[i] = true;
if (DEBUG) {
System.err.println(">>> DEBUG: placing a double bond between atoms '" +
mol.getAtomName(ringAtom) + "' and '" + mol.getAtomName(exoAtom) + "'.");
}
}
}
}
}
}
mol.ensureHelperArrays(Molecule3D.cHelperRings);
return hasExocyclicDoubleBond;
}
/**
* Assigns positive charges to metal atoms in well-known ions.
* @param mol
* The molecule that contains the metal atoms.
*/
private static void processKnownIonsInvolvingMetals(Molecule3D mol) {
for (int i = 0; i < mol.getAllAtoms(); i++) {
if (!mol.isMetalAtom(i)) continue;
if (resolveMetalHexafluoride(mol, i)) continue;
if (resolveGoldInLinearCoordination(mol, i)) continue;
// Aluminium (Al)
if (mol.getAtomicNo(i) == 13) {
// AlCl4(-)
if (mol.getAllConnAtomsPlusMetalBonds(i) == 4) {
boolean isIon = true;
for (int j = 0; j < mol.getAllConnAtomsPlusMetalBonds(i); j++) {
int connAtom = mol.getConnAtom(i, j);
// is terminal Cl atom
if (!(mol.getAtomicNo(connAtom) == 17 && mol.getAllConnAtoms(connAtom) == 0)) {
isIon = false;
break;
}
}
if (isIon) {
mol.setAtomCharge(i, 3);
}
}
}
}
}
/**
* Evaluates if the given atom serves as the central metal atom of a hexafluoride complex
* and assigns appropriate charges if needed. Only hexafluorides that can be resolved
* unambiguously are modified.
*
* @param mol
* Molecule that contains the metal atom.
* @param metalAtom
* Metal atom to be evaluated.
* @return
* Boolean value denoting if the charge of the metal atom was modified.
*/
private static boolean resolveMetalHexafluoride(Molecule3D mol, int metalAtom) {
int charge;
switch (mol.getAtomicNo(metalAtom)) {
case 13: // AlF6(3-)
case 26: // FeF6(3-)
charge = +3;
break;
case 51: // SbF6(-1)
case 79: // AuF6(-1)
charge = +5;
break;
// -1 is treated as a special value
default:
charge = -1;
}
if (charge == -1) return false;
if (!isCentralHexafluoriteIonAtom(mol, metalAtom)) return false;
mol.setAtomCharge(metalAtom, charge);
return true;
}
/**
* Evaluates if the given atom is a gold atom in a linear coordination
* and assigns appropriate charges if needed.
*
* NOTE: gold atoms appearing in linear coordination most likely have
* the oxidation state of +1.
*
* @param mol
* Molecule that contains the metal atom.
* @param metalAtom
* Metal atom to be evaluated.
* @return
* Boolean value denoting if the charge of the metal atom was modified.
*/
private static boolean resolveGoldInLinearCoordination(Molecule3D mol, int metalAtom) {
if (mol.getAtomicNo(metalAtom) != 79) return false; // Gold (Au)
if (mol.getAllConnAtomsPlusMetalBonds(metalAtom) != 2) return false;
int n1 = mol.getConnAtom(metalAtom, 0);
int n2 = mol.getConnAtom(metalAtom, 1);
// 2.80 rad ~ 160 deg
if (getAngle(mol, n1, metalAtom, n2) < 2.80) return false;
mol.setAtomCharge(metalAtom, +1);
return true;
}
/**
* Evaluates if the given atom serves as the central atom of a hexafluoride complex.
* @param mol
* Molecule that contains the atom.
* @param atom
* Atom to be evaluated.
* @return
* Boolean value denoting if given atom serves as the central atom of
* a hexafluoride complex
*/
static boolean isCentralHexafluoriteIonAtom(Molecule3D mol, int atom) {
boolean isIon = true;
if (mol.getAllConnAtomsPlusMetalBonds(atom) != 6) return false;
for (int j = 0; j < mol.getAllConnAtomsPlusMetalBonds(atom); j++) {
int connAtom = mol.getConnAtom(atom, j);
// is terminal F atom
if (!(mol.getAtomicNo(connAtom) == 9 && mol.getAllConnAtoms(connAtom) == 0)) {
isIon = false;
break;
}
}
return isIon;
}
private static void assignAtomChargesBasedOnValence(Molecule3D mol, int[] spOrder) {
// Set charges for non-metal atoms
for (int i = 0; i < mol.getAllAtoms(); i++) {
if (mol.isMetalAtom(i)) continue;
// FIXME: check and properly test if the ( mol.getAtomCharge(i) != 0 )
// can be refactored to the start of this loop
// only apply this set of rules to atoms that do not yet have a charge
if (mol.getAtomCharge(i) == 0) {
if (mol.getAtomicNo(i) == 5) { // boron (B)
if (mol.getOccupiedValence(i) == 4) {
mol.setAtomCharge(i, -1);
}
// } else if ( getAllConnAtoms(mol, i, true) == 6 ) {
// mol.setAtomAbnormalValence(i, 6);
// }
}
// Phoshorus (P)
if (mol.getAtomicNo(i) == 15) {
// Phoshorus (P) atoms with 4 neighbours and no higher order bonds have a +1 formal charge
if (getAllConnAtoms(mol, i, true) == 4 && mol.getAtomPi(i) == 0) {
mol.setAtomCharge(i, +1);
}
// Phosphorus (P) atoms with the valence of 6 have a -1 formal charge
if (mol.getOccupiedValence(i) == 6) {
mol.setAtomCharge(i, -1);
}
}
// Sulphur (S)
if (mol.getAtomicNo(i) == 16) {
// Sulphur (S) sp3 atoms with 3 neighbours have a +1 formal charge
if (spOrder[i] == 3 && getAllConnAtoms(mol, i, true) == 3) {
mol.setAtomCharge(i, +1);
}
}
// Arsernic (As) atoms with 4 neighbours and no double bonds have a +1 formal charge
if (mol.getAtomicNo(i) == 33) {
if (getAllConnAtoms(mol, i, true) == 4 && mol.getAtomPi(i) == 0) {
mol.setAtomCharge(i, +1);
}
}
}
// Hydrogen (H) ions
if (mol.getAtomicNo(i) == 1 && mol.getAllConnAtoms(i) == 0) {
if (mol.getAllConnAtomsPlusMetalBonds(i) > 0 &&
mol.isMetalAtom(mol.getConnAtom(i, 0))) {
// Hydride anion H- (usually bonded to metals)
mol.setAtomCharge(i, -1);
} else {
// Hydron cation H+
mol.setAtomCharge(i, +1);
}
}
// Removing implicit hydrogens by setting a negative charge.
// '_atom_site_attached_hydrogens' data item values are taken
// into consideration
if (mol.getImplicitHydrogens(i) > 0 || mol.getAttachedHydrogenCount(i) > 0) {
// sp2 carbon with three non-hydrogen neighbours should have a positive charge
if (mol.getAtomicNo(i) == 6 && spOrder[i] == 2 &&
mol.getConnAtoms(i) == 3 && mol.getAtomPi(i) == 0
&& mol.getAtomCharge(i) == 0 && !mol.isAromaticAtom(i)) {
mol.setAtomCharge(i, +1);
} else {
mol.setAtomCharge(i, mol.getAtomCharge(i) - mol.getImplicitHydrogens(i)
+ mol.getAttachedHydrogenCount(i));
}
}
// Adding a positive charge to the elements that have a high valency
if (mol.getOccupiedValence(i) > mol.getMaxValence(i) && mol.getAtomCharge(i) == 0) {
mol.setAtomCharge(i, mol.getAtomCharge(i) + mol.getOccupiedValence(i) - mol.getMaxValence(i));
}
}
}
/**
* Assigns positive charges to metal atoms if needed to balance out the overall
* charge of the molecule. It is assumed that the metal atoms were not assigned
* a charge up to this point.
* @param mol
* The molecule that contains the metal atoms.
*/
private static void assignMetalCharges(Molecule3D mol) {
int netCharge = 0;
for (int i = 0; i < mol.getAllAtoms(); i++) {
netCharge += mol.getAtomCharge(i);
}
if ( netCharge >= 0 ) {
return;
}
List metalAtoms = new ArrayList<>();
for (int i = 0; i < mol.getAtoms(); i++) {
if ( !mol.isMetalAtom(i) ) continue;
if ( mol.getAtomCharge(i) != 0 ) continue;
metalAtoms.add(i);
}
if ( metalAtoms.size() == 0 ) {
return;
}
// Put all of the remaining negative charge on the single metal atom
if ( metalAtoms.size() == 1 ) {
mol.setAtomCharge(metalAtoms.get(0), -Math.max(netCharge, -8));
} else {
/*
// Process the metal coordination sphere surroundings first
// All negatively charged ligands are assumed to increase the
// oxidation state by one despite their actual charge
if ( netCharge < 0 ) {
for (Integer metal : metalAtoms) {
if (maximumIonChargeReached(mol, metal)) continue;
if (!hasMetalLigandBonds(mol, metal)) continue;
for (int k = mol.getAllConnAtoms(metal); k < mol.getAllConnAtomsPlusMetalBonds(metal); k++) {
int ligandCharge = mol.getAtomCharge(mol.getConnAtom(metal, k)) < 0 ? 1 : 0;
if (ligandCharge < 0) {
int oldMetalCharge = mol.getAtomCharge(metal);
int newMetalCharge =
StrictMath.min( oldMetalCharge + StrictMath.abs(ligandCharge),
getMaximumIonCharge(mol, metal) );
mol.setAtomCharge(metal, newMetalCharge);
netCharge += newMetalCharge - oldMetalCharge;
}
}
}
}
*/
// TODO: select only atoms that do not have maximum charge
// If all metals are of the same type, valence and charge
// and the net charge is divisible by the number of metals
// atoms, distribute the net charge equally
boolean similarMetals = false;
if ( StrictMath.abs(netCharge) % metalAtoms.size() == 0 ) {
similarMetals = true;
for (Integer metalAtom : metalAtoms) {
similarMetals = similarMetals &&
mol.getAtomicNo(metalAtoms.get(0)) == mol.getAtomicNo(metalAtom) &&
mol.getAtomCharge(metalAtoms.get(0)) == mol.getAtomCharge(metalAtom) &&
mol.getAllConnAtoms(metalAtoms.get(0)) == mol.getAllConnAtoms(metalAtom);
}
}
if (similarMetals) {
int increment = Math.min( StrictMath.abs(netCharge) / metalAtoms.size(), 8 );
for (Integer metalAtom : metalAtoms) {
mol.setAtomCharge(metalAtom, increment);
}
} else {
// Otherwise try to distribute it over all of the metal atoms
boolean changeOccurred = true;
while (netCharge < 0 && changeOccurred) {
changeOccurred = false;
for (Integer metal : metalAtoms) {
int newMetalCharge = mol.getAtomCharge(metal) + 1;
if (newMetalCharge <= getMaximumIonCharge(mol, metal)) {
netCharge += 1;
mol.setAtomCharge(metal, newMetalCharge);
changeOccurred = true;
}
if (netCharge == 0) break;
}
}
}
}
// Set abnormal valence to metals that exhibit uncommon
// charges in order to generate a proper id codes
for (Integer metal : metalAtoms) {
// TODO: extend the logic to other metals as well
// Currently, the logic is only applied to Aluminium (Al).
// The code should work is other metals as well, but
// more extensive testing is needed
if (mol.getAtomicNo(metal) != 13) continue;
if (Molecule.cAtomValence[mol.getAtomicNo(metal)] == null) continue;
if (mol.getMaxValence(metal) > 0) {
mol.setAtomAbnormalValence(metal, mol.getDefaultMaxValenceUncharged(metal) - mol.getMaxValence(metal));
}
}
}
/**
* Creates a list of atom ring indexes that an atom is a
* part of for each atom of the molecule.
*
* @param mol
* The molecule that contains the atoms and rings.
* @return
* Array of atom ring index lists. The array indexes
* match the indexes of the molecule atoms. If an
* atom is not a part of any rings, null value is
* placed in the array instead of an empty list.
*/
private static ArrayList[] getAtomToRings(Molecule3D mol) {
ArrayList[] atomToRings = new ArrayList[mol.getAllAtoms()];
RingCollection ringSet = mol.getRingSet();
for ( int r = 0; r < ringSet.getSize(); r++ ) {
int[] ringAtom = ringSet.getRingAtoms(r);
for (int atom: ringAtom) {
if (atomToRings[atom] == null) {
atomToRings[atom] = new ArrayList<>();
}
atomToRings[atom].add(r);
}
}
return atomToRings;
}
/**
* Determines which atom rings in the molecule are aromatic.
* The aromaticity is determined by checking the ring geometry
* against various constraints like atom chemical type, atom
* planarity, atom bond lengths, atom bond angles.
*
* @param mol
* The molecule that contains the atom rings.
* @param spOrder
* The sp hybridization type for each atom.
* @return
* An array of boolean values marking which atom rings could
* potentially accommodate aromaticity. The indexes of the
* returned array correspond to the atom ring indexes in
* the molecule.
*/
private static boolean[] findAromaticRings(Molecule3D mol, int[] spOrder) {
RingCollection ringSet = mol.getRingSet();
boolean[] looksLikeAromaticRing = new boolean[ringSet.getSize()];
for (int ringNo = 0; ringNo < ringSet.getSize(); ringNo++) {
looksLikeAromaticRing[ringNo] = looksLikeAromaticRing(mol, ringNo, spOrder);
}
return looksLikeAromaticRing;
}
/**
* Determines is the given atom ring could potentially accommodate aromaticity.
* False positive values may be returned.
* @param mol
* Molecule that contains the atom ring.
* @param ringNo
* Index of the ring in the molecule.
* @param spOrder
* The sp hybridization type for each atom.
* @return
* Boolean values denoting is the ring could potentially accommodate aromaticity.
*/
private static boolean looksLikeAromaticRing(Molecule3D mol, int ringNo, int[] spOrder) {
if (mol.getRingSet().getRingSize(ringNo) < 3) return false;
if (mol.getRingSet().getRingSize(ringNo) > 7) return false;
int[] ringAtoms = mol.getRingSet().getRingAtoms(ringNo);
// Rings that are potentially interacting with a metal using
// pi bonds should be allowed to be a little more deformed
boolean isCoordinatedRing = isRingCoordinated(mol, ringNo);
for (int i = 0; i < ringAtoms.length; i++) {
int ringAtom = ringAtoms[i];
if (!looksLikeAromaticAtom(mol, ringAtom, spOrder[ringAtom])) return false;
int a0 = ringAtoms[(i - 1 + ringAtoms.length) % ringAtoms.length];
int a1 = ringAtoms[(i) % ringAtoms.length];
int a2 = ringAtoms[(i + 1) % ringAtoms.length];
int a3 = ringAtoms[(i + 2) % ringAtoms.length];
// Make sure that all boron, carbon, nitrogen and silicon
// atoms in the ring are planar, but allow some deviation
// due to neighbouring sulphur atoms
if ((mol.getAtomicNo(a1) == 5 ||
mol.getAtomicNo(a1) == 6 ||
mol.getAtomicNo(a1) == 7 ||
mol.getAtomicNo(a1) == 14) &&
spOrder[ringAtom] != 2) {
if (!((mol.getAtomicNo(a0) == 16 || mol.getAtomicNo(a2) == 16) &&
(getAllConnAtoms(mol, a1, true)) < 4)) {
return false;
}
}
if (!(fitsAromaticLengthConstraints(mol, a1, a2) ||
hasMetalLigandBonds(mol, a1) ||
hasMetalLigandBonds(mol, a2) ||
isCoordinatedRing)) {
if (DEBUG) {
System.err.println(">>> DEBUG: bond length between atoms '" + mol.getAtomName(a1) + "' and '" +
mol.getAtomName(a2) + "' caused ring " + ringNo + " to be marked as non aromatic.");
}
return false;
}
// check if all of the ring atoms lie in a plane
double dihedral = getDihedral(mol, a0, a1, a2, a3);
if (Math.abs(Math.toDegrees(dihedral)) > 20) {
return false;
}
if (mol.getAtomicNo(a1) == 5 || mol.getAtomicNo(a1) == 6 || mol.getAtomicNo(a1) == 7) {
if (isCoordinatedRing) continue;
for (int j = 0; j < mol.getAllConnAtoms(a1); j++) {
if (isRingAtom(mol, ringNo, mol.getConnAtom(a1, j))) continue;
dihedral = getDihedral(mol, mol.getConnAtom(a1, j), a1, a2, a3);
if (StrictMath.min(Math.abs(Math.toDegrees(dihedral)), 180 - Math.abs(Math.toDegrees(dihedral))) > 25) {
if (DEBUG) {
System.err.println(">>> DEBUG: the torsion angle involving atoms '"
+ mol.getAtomName(mol.getConnAtom(a1, j)) + "', '"
+ mol.getAtomName(a1) + "' , "
+ mol.getAtomName(a2) + "' and '"
+ mol.getAtomName(a3) + "' caused ring " + ringNo +
" to be marked as non aromatic.");
}
return false;
}
}
}
}
// excluding 3-membered rings that are part of a tetrahedron
if (ringAtoms.length == 3) {
int ringAtom = ringAtoms[0];
int exocyclicAtom = -1;
if (mol.getConnAtoms(ringAtom) == 4) {
for (int j = 0; j < mol.getConnAtoms(ringAtom); j++) {
if (!isRingAtom(mol, ringNo, mol.getConnAtom(ringAtom, j))) {
exocyclicAtom = mol.getConnAtom(ringAtom, j);
break;
}
}
}
return (mol.getBond(ringAtoms[1], exocyclicAtom) != -1) &&
(mol.getBond(ringAtoms[2], exocyclicAtom) != -1);
}
return true;
}
/**
* Evaluates if the given atom could potentially participate in aromaticity.
* False positive values may be returned.
*
* @param mol
* Molecule that contains the atom.
* @param atom
* Index of the atom.
* @param spOrder
* The sp hybridization type for each atom.
* @return
* Boolean value denoting if the atom could participate in aromaticity.
*/
private static boolean looksLikeAromaticAtom(Molecule3D mol, int atom, int spOrder) {
// boron (B), carbon (C), nitrogen (N), oxygen (O), silicon (Si)
if (mol.getAtomicNo(atom) < 15) {
if (getAllConnAtoms(mol, atom, true) > 3) return false;
}
// germanium (Ge)
if (mol.getAtomicNo(atom) == 32) {
if (getAllConnAtoms(mol, atom, true) > 3) return false;
if (spOrder != 2) return false;
}
if (getAllConnAtoms(mol, atom, true) > 4) return false;
// Atoms that already have double bonds cannot participate in aromaticity
// TODO: check if it is only true in some cases, sulphur (S) for example
if (mol.getAtomPi(atom) != 0) return false;
return true;
}
/**
* Calculates the hybridization state for each atom in a molecule.
*
* @param mol
* Molecule that contains the atoms.
* @return
* Array of integers with values {1,2,3,4,5} that correspond to
* the the {sp1, sp2, sp3, sp3d1, sp3d2} hybridization states
* accordingly.
*/
private static int[] calculateHybridizationStates(Molecule3D mol) {
int[] spOrder = new int[mol.getAllAtoms()];
for (int i = 0; i < mol.getAllAtoms(); i++) {
spOrder[i] = calculateHybridizationState(mol, i);
if (DEBUG) {
System.err.println("Atom '" + mol.getAtomName(i) + "' was assigned hybridization state " + spOrder[i]);
}
}
return spOrder;
}
/**
* Calculates the hybridization state for the given atom.
*
* @param mol
* Molecule that contains the atom.
* @return
* Integers from the set of {1,2,3,4,5} that correspond to
* the the {sp1, sp2, sp3, sp3d1, sp3d2} hybridization states
* accordingly.
*/
private static int calculateHybridizationState(Molecule3D mol, int i) {
int spOrder = 0; // special value denoting an unrecognised hybridization state
if (getAllConnAtoms(mol, i, true) <= 1) {
// TODO: ask if this can always be assumed
if (mol.getAtomicNo(i) == 8) {
spOrder = 2;
} else {
spOrder = 1;
}
} else if (getAllConnAtoms(mol, i, true) == 2) {
if (getAllConnAtoms(mol, i, false) <= 1) {
spOrder = 2;
} else {
double angle = getAngle(mol, mol.getConnAtom(i, 0), i, mol.getConnAtom(i, 1));
if (Math.abs(angle - Math.PI) < Math.PI / 6) {
spOrder = 1;
} else {
spOrder = 2;
}
}
} else if (getAllConnAtoms(mol, i, true) == 3) {
if (getAllConnAtoms(mol, i, false) < 2) {
spOrder = 0; // TODO: to be changed to 2 or 3
} else if (getAllConnAtoms(mol, i, false) < 3) {
spOrder = 2;
} else {
Coordinates origin = mol.getCoordinates(i);
Coordinates normal = calculatePlaneNormal(origin,
mol.getCoordinates(mol.getConnAtom(i, 0)),
mol.getCoordinates(mol.getConnAtom(i, 1)));
if (normal.distSq() > 0) {
if (calculatePointToPlaneDistance(normal, origin, mol.getCoordinates(mol.getConnAtom(i, 2))) < 0.4) {
spOrder = 2;
} else {
spOrder = 3;
}
} else {
spOrder = 3;
}
}
} else if (getAllConnAtoms(mol, i, true) == 4) {
spOrder = 3;
} else if (getAllConnAtoms(mol, i, true) == 5) {
spOrder = 4;
} else if (getAllConnAtoms(mol, i, true) == 6) {
spOrder = 5;
}
return spOrder;
}
/**
* Assigns bond orders and charges to hydrogen (H) atoms that
* are bonded to two non-metal(lic) atoms.
*
* @param mol
* The molecule that contains the shared hydrogen atoms.
*/
private static void resolveSharedHydrogenAtoms(Molecule3D mol) {
// Shared hydrogen atoms are handled in one of the two ways
// depending on the bond lengths to the neighbouring atoms:
// - in case one of the bonds is significantly shorter,
// the longer bond gets removed;
// - in case neither of the bonds is significantly shorter,
// both bonds are marked as metal-ligand (coordinate) bonds
// and the hydrogen atom is assigned a +1 charge.
for (int i = 0; i < mol.getAllAtoms(); i++) {
// R-H-R bridging hydrogen atom
if (mol.getAtomicNo(i) != 1) continue;
if (mol.getConnAtoms(i) != 2) continue;
// boron compounds must be handled separately
if (mol.getAtomicNo(mol.getConnAtom(i, 0)) == 5) continue;
if (mol.getAtomicNo(mol.getConnAtom(i, 1)) == 5) continue;
int bond1 = mol.getConnBond(i, 0);
int bond2 = mol.getConnBond(i, 1);
double bondLength1 = getBondLength(mol, bond1);
double bondLength2 = getBondLength(mol, bond2);
if (StrictMath.abs(bondLength1 - bondLength2) < 0.1) {
mol.setBondType(bond1, Molecule3D.cBondTypeMetalLigand);
mol.setBondType(bond2, Molecule3D.cBondTypeMetalLigand);
mol.setAtomCharge(i, +1);
} else {
// delete the longer single bond
if (bondLength1 > bondLength2) {
// mol.setBondType(bond1, Molecule3D.cBondTypeMetalLigand);
mol.markBondForDeletion(bond1);
} else {
// mol.setBondType(bond2, Molecule3D.cBondTypeMetalLigand);
mol.markBondForDeletion(bond2);
}
}
}
mol.deleteMarkedAtomsAndBonds();
mol.ensureHelperArrays(Molecule.cHelperNeighbours);
}
/**
* Recognises known ions in a molecule and assigns the appropriate
* bond orders and atom charges if needed.
*
* @param mol
* Molecule that contains the ions.
*/
private static void resolveKnownIons(Molecule3D mol) {
for (int i = 0; i < mol.getAtoms(); i++) {
if (mol.getAtomicNo(i) != 6) continue;
if (calculateHybridizationState(mol, i) != 2) continue;
if (mol.getAllConnAtoms(i) != 3) continue;
if (mol.getAttachedHydrogenCount(i) != 0) continue;
resolveTricyanomethanideIon(mol, i);
}
}
/**
* Evaluates if the given atom acts as the central atom of the tricyanomethanide
* ion ([C-](C#N)(C#N)(C#N)) and assigns appropriate bond orders and atom charges
* if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param cCentralAtom
* Index of the central carbon atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveTricyanomethanideIon(Molecule3D mol, int cCentralAtom) {
if (mol.getAtomicNo(cCentralAtom) != 6) return false;
if (calculateHybridizationState(mol, cCentralAtom) != 2) return false;
if (mol.getAllConnAtoms(cCentralAtom) != 3) return false;
if (mol.getAttachedHydrogenCount(cCentralAtom) != 0) return false;
int[] cAtoms = new int[3];
int[] nAtoms = new int[3];
for (int i = 0; i < mol.getAllConnAtoms(cCentralAtom); i++) {
int cAtom = mol.getConnAtom(cCentralAtom, i);
if (mol.getAllConnAtoms(cAtom) != 2) return false;
for (int j = 0; j < mol.getAllConnAtoms(cAtom); j++) {
int nAtom = mol.getConnAtom(cAtom, j);
if (cCentralAtom == nAtom) continue;
if (!looksLikeCyanoGroup(mol, cAtom, nAtom)) return false;
cAtoms[i] = cAtom;
nAtoms[i] = nAtom;
}
}
// [C-](C#N)(C#N)(C#N)
for (int j = 0; j < 3; j++) {
mol.setBondOrder(mol.getBond(cAtoms[j], nAtoms[j]), 3);
mol.setAtomCharge(cCentralAtom, -1);
}
mol.ensureHelperArrays(Molecule3D.cHelperNeighbours);
return true;
}
/**
* Evaluates if the given atom ring looks like a tetraatomic aromatic ring ion
* and assigns appropriate bond orders and atom charges if needed.
*
* @param mol
* Molecule that contains the atoms ring.
* @param ringNo
* Index of the ring in the molecule.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveTetraatomicAromaticRingIons(Molecule3D mol, int ringNo) {
if (!looksLikeTetraatomicAromaticRingIon(mol, ringNo)) return false;
boolean isChalcogen = true;
int atomicNo = mol.getAtomicNo(mol.getRingSet().getRingAtoms(ringNo)[0]);
switch (atomicNo) {
case 7: // nitrogen (N)
case 15: // phosphorus (P);
case 33: // arsenic (As);
isChalcogen = false;
break;
case 8: // oxygen (O)
case 16: // sulphur (S)
case 34: // selenium (Se); tetraselenium ion ([Se]1[Se+]=[Se+][Se]1)
case 52: // tellurium (Te)
break;
default:
return false;
}
int[] ringBonds = mol.getRingSet().getRingBonds(ringNo);
int shortestBondIndex = 0;
double shortestBondLength = getBondLength(mol, ringBonds[shortestBondIndex]);
for (int i = 0; i < ringBonds.length; i++) {
if (getBondLength(mol, ringBonds[i]) < shortestBondLength) {
shortestBondLength = getBondLength(mol, ringBonds[i]);
shortestBondIndex = i;
}
}
mol.setBondOrder(ringBonds[shortestBondIndex], 2);
int dbAtom1 = mol.getBondAtom(0, ringBonds[shortestBondIndex]);
int dbAtom2 = mol.getBondAtom(1, ringBonds[shortestBondIndex]);
if (isChalcogen) {
// chalcogen ring, i.e. [Se]1[Se][Se+]=[Se+]1
mol.setAtomCharge(dbAtom1, +1);
mol.setAtomCharge(dbAtom2, +1);
} else {
// pnictogen ring, i.e. [P-]1[P-][P]=[P]1
for (int atom : mol.getRingSet().getRingAtoms(ringNo)) {
if (atom != dbAtom1 && atom != dbAtom2) {
mol.setAtomCharge(atom, -1);
}
}
}
return true;
}
/**
* Evaluates if the given atom ring looks like a tetraatomic aromatic ring ion,
* i.e. the tetraselenium ion ([Se]1[Se+]=[Se+][Se]1).
*
* @param mol
* Molecule that contains the atoms ring.
* @param ringNo
* Index of the ring in the molecule.
* @return
* Boolean value denoting if the ring looks like a tetraatomic aromatic
* ring ion.
*/
private static boolean looksLikeTetraatomicAromaticRingIon(Molecule3D mol, int ringNo) {
int[] ringAtoms = mol.getRingSet().getRingAtoms(ringNo);
if (ringAtoms.length != 4) return false;
// rings must be homocyclic
int atomicNo = mol.getAtomicNo(ringAtoms[0]);
for (int atom : ringAtoms) {
if (mol.getAtomicNo(atom) != atomicNo) return false;
if (getAllConnAtoms(mol, atom, true) != 2) return false;
if (mol.getAtomRingCount(atom, 7) != 1) return false;
}
return true;
}
/**
* Recognises functional groups in a molecule and sets the bonds and charges accordingly.
* @param mol
* The molecule that contains the functional groups.
* @param spOrder
* Array that contains the orbital hybridization type for each atom.
*/
private static void recogniseFunctionalGroups(Molecule3D mol, int[] spOrder) {
// TODO: consider the atom type of terminal bonds when sorting
// TODO: ask about the best way to keep track of new double bonds
// calling molecule helpers to ensure pi count after each double
// bond assignment is inefficient so a local array is used instead
int[] localAtomPiCount = getAtomPiCount(mol);
for(int i = 0; i < mol.getAtoms(); i++) {
if ( mol.getAtomCharge(i) != 0 ) continue;
if ( mol.getAtomRadical(i) != Molecule.cAtomRadicalStateNone ) continue;
if ( spOrder[i] == 2 && localAtomPiCount[i] != 0 && mol.getAtomicNo(i) < 7 ) continue;
// Terminal oxygen atoms capable of double bonds
List terminalOxygenAtoms = new LinkedList<>();
// All terminal atoms capable of double bonds (including the oxygen atoms)
List terminalDoubleBondAtoms = new LinkedList<>();
int oxygenCount = 0;
int terminalAtomCount = 0;
for (int j = 0; j < mol.getConnAtoms(i); j++ ) {
int neighbour = mol.getConnAtom(i, j);
boolean isTerminal = ( getAllConnAtoms(mol, neighbour, true) == 1 );
boolean isOxygen = ( mol.getAtomicNo(neighbour) == 8 );
if (isOxygen) oxygenCount++;
if ( isTerminal && isOxygen ) {
terminalOxygenAtoms.add(j);
}
if ( mol.getAtomicNo(neighbour) == 1 ) continue;
if ( isTerminal ) {
terminalAtomCount++;
// for now only consider terminal N, O, P and S for double bonds
if ( localAtomPiCount[neighbour] == 0 &&
( mol.getAtomicNo(neighbour) == 7 ||
mol.getAtomicNo(neighbour) == 8 ||
mol.getAtomicNo(neighbour) == 15 ||
mol.getAtomicNo(neighbour) == 16 ) ) {
terminalDoubleBondAtoms.add(j);
}
}
}
if ( getAllConnAtoms(mol, i, true) == 1 ) {
if (mol.getConnAtoms(i) == 0) continue;
int a1 = mol.getConnAtom(i, 0);
if (resolveDiatomicPnictogenIons(mol, i)) continue;
if (resolveDiatomicChalcogenIons(mol, i)) continue;
if ( mol.getAtomicNo(i) == 6 ) {
// R-N(+)=-=C(-)
if (mol.getAtomicNo(a1) == 7 && spOrder[a1] == 1) {
mol.setBondOrder(mol.getBond(i, a1), 3);
mol.setAtomCharge(i, -1);
mol.setAtomCharge(a1, +1);
localAtomPiCount[i] += 2;
localAtomPiCount[a1] += 2;
}
} else if ( mol.getAtomicNo(i) == 7 ) {
if (mol.getAtomicNo(a1) == 16 ) {
if ( getAllConnAtoms(mol, a1, true) == 1 ) {
if ( hasMetalLigandBonds(mol, i) ) {
mol.setBondOrder(mol.getBond(i, a1), 2);
localAtomPiCount[i]++;
localAtomPiCount[a1]++;
mol.setAtomCharge(i, -1);
} else {
mol.setBondOrder(mol.getBond(i, a1), 3);
localAtomPiCount[i] = localAtomPiCount[i] + 2;
localAtomPiCount[a1] = localAtomPiCount[a1] + 2;
mol.setAtomCharge(a1, 1);
}
} else if (spOrder[a1] == 1) {
mol.setBondOrder(mol.getBond(i, a1), 2);
localAtomPiCount[i]++;
localAtomPiCount[a1]++;
} else if (mol.getAtomicNo(a1) == 16 && spOrder[a1] == 3) {
// R-S#N
// FIXME: eventually replace the hardcoded S#N bond length value once enough statistics
// are gathered
if (getBondLength(mol, mol.getBond(i, a1)) < 1.47) {
mol.setBondOrder(mol.getBond(i, a1), 3);
localAtomPiCount[i] = localAtomPiCount[i] + 2;
localAtomPiCount[a1] = localAtomPiCount[a1] + 2;
}
}
}
} else if ( mol.getAtomicNo(i) == 8 ) {
// TODO: change O-C distances with proper values
if ( mol.getAtomicNo(a1) == 6 ) {
// C-=-O
if ( getAllConnAtoms(mol, a1, true) == 1 ) {
mol.setBondOrder(mol.getBond(i, a1), 3);
mol.setAtomCharge(a1, -1);
mol.setAtomCharge(i, +1);
localAtomPiCount[i] += 2;
localAtomPiCount[a1] += 2;
}
} else if ( mol.getAtomicNo(a1) == 7 ) {
// TODO: add the correct processing of the nitrosonium ion (N-=-O+)
// N=O ion
if ( getAllConnAtoms(mol, a1, true) == 1 ) {
mol.setBondOrder(mol.getBond(i, a1), 2);
mol.setAtomRadical(a1, Molecule3D.cAtomRadicalStateD);
localAtomPiCount[i]++;
localAtomPiCount[a1]++;
}
} else if ( mol.getAtomicNo(a1) == 8 ) {
// O-O ions
double sigma = 0.05;
if ( getAllConnAtoms(mol, a1, true) == 1 ) {
if (getBondLength(mol, i, a1) < 1.21 + sigma) {
mol.setBondOrder(mol.getBond(i, a1), 2);
localAtomPiCount[i]++;
localAtomPiCount[a1]++;
} else if (getBondLength(mol, i, a1) < 1.28 + sigma) {
mol.setBondOrder(mol.getConnBond(i, 0), 1);
mol.setAtomCharge(i, -1);
mol.setAtomRadical(a1, Molecule3D.cAtomRadicalStateD);
} else {
mol.setBondOrder(mol.getConnBond(i, 0), 1);
mol.setAtomCharge(i, -1);
mol.setAtomCharge(a1, -1);
}
}
}
}
} else if ( mol.getAllConnAtoms(i) == 2 ) {
if ( localAtomPiCount[i] == 2 ) continue;
// assigning neighbour atoms ordered by chemical number
int a1 = mol.getAtomicNo(mol.getConnAtom(i, 0)) <=
mol.getAtomicNo(mol.getConnAtom(i, 1)) ?
mol.getConnAtom(i, 0) : mol.getConnAtom(i, 1);
int a2 = a1 != mol.getConnAtom(i, 0) ?
mol.getConnAtom(i, 0) : mol.getConnAtom(i, 1);
if ( mol.getAtomicNo(i) == 5 ) {
if ( spOrder[i] == 1 ) {
if ( terminalDoubleBondAtoms.size() == 2 ) {
mol.setBondOrder(mol.getBond(i, a1), 2);
mol.setBondOrder(mol.getBond(i, a2), 2);
localAtomPiCount[i] += 2;
localAtomPiCount[a1] += 1;
localAtomPiCount[a2] += 1;
mol.setAtomCharge(i, -1);
}
}
} else if ( mol.getAtomicNo(i) == 6 ) {
if ( spOrder[i] == 1 ) {
if (terminalDoubleBondAtoms.size() == 1) {
int terminalAtom = mol.getConnAtom(i, terminalDoubleBondAtoms.get(0));
int nonTerminalAtom = (terminalAtom != a1) ? a1 : a2;
// cyano group (R-C#N)
if (resolveCyanoGroup(mol, i, terminalAtom)) {
localAtomPiCount[i] += 2;
localAtomPiCount[terminalAtom] += 2;
continue;
}
// isocyanate group (R-N=C=O)
if (resolveIsocyanateGroup(mol, i, nonTerminalAtom, terminalAtom)) {
localAtomPiCount[i] += 2;
localAtomPiCount[terminalAtom] += 1;
localAtomPiCount[nonTerminalAtom] += 1;
continue;
}
// isothiocyanate group (R-N=C=S)
if (resolveIsothiocyanateGroup(mol, i, nonTerminalAtom, terminalAtom)) {
localAtomPiCount[i] += 2;
localAtomPiCount[terminalAtom] += 1;
localAtomPiCount[nonTerminalAtom] += 1;
continue;
}
} else if (terminalDoubleBondAtoms.size() == 2) {
// thiocyanate ion (S=C=N[-])
if (resolverThiocyanateIon(mol, i, a2, a1)) {
localAtomPiCount[i] += 2;
localAtomPiCount[a1] += 1;
localAtomPiCount[a2] += 1;
continue;
}
// atoms are of the same species
if (mol.getAtomicNo(a1) == mol.getAtomicNo(a2)) {
mol.setBondOrder(mol.getBond(i, a1), 2);
mol.setBondOrder(mol.getBond(i, a2), 2);
localAtomPiCount[i] += 2;
localAtomPiCount[a1] += 1;
localAtomPiCount[a2] += 1;
}
}
}
} else if ( mol.getAtomicNo(i) == 7 ) {
if ( spOrder[i] == 1 ) {
if (terminalDoubleBondAtoms.size() > 0) {
// azide anion ([N-]=[N+]=[N-])
if (resolveAzideIon(mol, i)) {
localAtomPiCount[i] += 2;
localAtomPiCount[a1] += 1;
localAtomPiCount[a2] += 1;
continue;
}
// azide group (R[N]=[N+]=[N-])
if (resolveAzideGroup(mol, i)) {
localAtomPiCount[i] = mol.getAtomPi(i);
localAtomPiCount[a1] = mol.getAtomPi(a1);
localAtomPiCount[a2] = mol.getAtomPi(a2);
continue;
}
if ((mol.getAtomicNo(a1) == 6 && mol.getAtomicNo(a2) == 7) ||
(mol.getAtomicNo(a1) == 7 && mol.getAtomicNo(a2) == 6)) {
int carbonAtom = (mol.getAtomicNo(a1) == 6) ? a1 : a2;
int nitrogenAtom = (mol.getAtomicNo(a1) == 7) ? a1 : a2;
if ( getAllConnAtoms(mol, nitrogenAtom, true) == 1 &&
spOrder[carbonAtom] == 2) {
double doubleNNBond = getIdealisedBondLength(2, 7, 7, 1, 2);
double tripleNNBond = getIdealisedBondLength(3, 7, 7, 2, 2);
double singleCNBond = getIdealisedBondLength(1, 6, 7, 1, 2);
double doubleCNBond = getIdealisedBondLength(2, 6, 7, 1, 2);
double NNBondLength = getBondLength(mol, i, nitrogenAtom);
double CNBondLength = getBondLength(mol, i, carbonAtom);
double C2N2N = StrictMath.max( doubleNNBond - NNBondLength, 0 ) +
StrictMath.max( doubleCNBond - CNBondLength, 0 );
double C1N3N = StrictMath.max( tripleNNBond - NNBondLength, 0 ) +
StrictMath.max( singleCNBond - CNBondLength, 0 );
// RC-[N+]#N
if ( C1N3N < C2N2N ) {
mol.setBondOrder(mol.getBond(i, nitrogenAtom), 3);
mol.setAtomCharge(i, +1);
localAtomPiCount[i] += 2;
localAtomPiCount[nitrogenAtom] += 1;
} else {
// RC=[N+]=[N-]
mol.setBondOrder(mol.getBond(i, nitrogenAtom), 2);
mol.setBondOrder(mol.getBond(i, carbonAtom), 2);
mol.setAtomCharge(i, +1);
mol.setAtomCharge(nitrogenAtom, -1);
localAtomPiCount[i] += 2;
localAtomPiCount[nitrogenAtom] += 1;
localAtomPiCount[carbonAtom] += 1;
}
}
// O=N=O, S=N=S
} else if ( terminalOxygenAtoms.size() == 2 ||
mol.getAtomicNo(a1) == 16 && mol.getAtomicNo(a2) == 16 ) {
mol.setAtomCharge(i, +1);
for (Integer terminalDoubleBondAtom : terminalDoubleBondAtoms) {
mol.setBondOrder(mol.getConnBond(i, terminalDoubleBondAtom), 2);
localAtomPiCount[i] += 1;
localAtomPiCount[mol.getConnAtom(i, terminalDoubleBondAtom)] += 1;
}
}
} else {
// R-P=[N+]=P-R
if ( mol.getAtomicNo(a1) == 15 && spOrder[a1] == 3 &&
mol.getAtomicNo(a2) == 15 && spOrder[a2] == 3 ) {
// cyclophosphaneses
if ( mol.isRingAtom(i) ) {
double NPBondLength1 = getBondLength(mol, i, mol.getConnAtom(i, 0));
double NPBondLength2 = getBondLength(mol, i, mol.getConnAtom(i, 1));
if ( NPBondLength1 < NPBondLength2 && localAtomPiCount[mol.getConnAtom(i, 0)] == 0) {
mol.setBondOrder(mol.getConnBond(i, 0), 2);
localAtomPiCount[i] += 1;
localAtomPiCount[mol.getConnAtom(i, 0)] += 1;
} else if ( localAtomPiCount[mol.getConnAtom(i, 1)] == 0 ) {
mol.setBondOrder(mol.getConnBond(i, 1), 2);
localAtomPiCount[i] += 1;
localAtomPiCount[mol.getConnAtom(i, 1)] += 1;
}
// R-P=N(+)=P-R (bridging)
} else {
mol.setBondOrder(mol.getConnBond(i, 0), 2);
mol.setBondOrder(mol.getConnBond(i, 1), 2);
mol.setAtomCharge(i, +1);
localAtomPiCount[i] += 2;
localAtomPiCount[mol.getConnAtom(i, 0)] += 1;
localAtomPiCount[mol.getConnAtom(i, 1)] += 1;
}
}
}
} else if ( spOrder[i] == 2 ) {
if (terminalOxygenAtoms.size() == 1) {
if (resolveNitrosoGroup(mol, i)) {
localAtomPiCount[i] += 1;
localAtomPiCount[mol.getConnAtom(i, terminalOxygenAtoms.get(0))] += 1;
}
} else {
// R-P=N(+)-P-R
if (mol.getAtomicNo(a1) != 15) continue;
if (spOrder[a1] != 3) continue;
if (mol.getAtomicNo(a2) != 15) continue;
if (spOrder[a2] != 3) continue;
// skip if both phosphorus atoms already have a double bond
if (localAtomPiCount[a1] != 0 && localAtomPiCount[a2] != 0) continue;
// select the shortest bond out of the available ones
int doubleBondAtom = a1;
if (localAtomPiCount[a1] != 0) {
doubleBondAtom = a2;
} else if (localAtomPiCount[a2] == 0) {
if (getBondLength(mol, mol.getBond(i, a1)) > getBondLength(mol, mol.getBond(i, a2))) {
doubleBondAtom = a2;
}
}
mol.setBondOrder(mol.getBond(i, doubleBondAtom), 2);
localAtomPiCount[i] += 1;
localAtomPiCount[doubleBondAtom] += 1;
}
}
// S
} else if ( mol.getAtomicNo(i) == 16 ) {
if (spOrder[i] == 2) {
// SO2
if (terminalOxygenAtoms.size() > 0) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
mol.setAtomCharge(i, +1);
}
}
// F, Cl, Br, I
} else if ( mol.getAtomicNo(i) == 7 ||
mol.getAtomicNo(i) == 17 ||
mol.getAtomicNo(i) == 35 ||
mol.getAtomicNo(i) == 53 ) {
if (terminalDoubleBondAtoms.size() > 0) {
// F is not capable of a double bond
if (mol.getAtomicNo(i) == 7) {
mol.setAtomCharge(i, +1);
} else {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
}
} else {
// R-X-R (linear)
if (spOrder[i] == 1) {
mol.setAtomCharge(i, -1);
// R-X-R (bent)
} else if (spOrder[i] == 2) {
mol.setAtomCharge(i, +1);
}
}
}
} else if ( mol.getAllConnAtoms(i) == 3 ) {
int a, b;
int a1 = mol.getConnAtom(i, 0);
int a2 = mol.getConnAtom(i, 1);
int a3 = mol.getConnAtom(i, 2);
if (mol.getAtomicNo(i) == 6 && spOrder[i] == 2) {
if (mol.getAtomRingSize(i) > 0) {
if (terminalDoubleBondAtoms.isEmpty()) continue;
// thioketone in TTF: C(R)(R)(=S)
if (mol.getAtomRingSize(i) != 5) continue;
if (mol.getAtomicNo(a1) != 16) continue;
if (mol.getAtomicNo(a2) != 16) continue;
if (mol.getAtomicNo(a3) != 16) continue;
mol.setBondOrder(mol.getConnBond(i, terminalDoubleBondAtoms.get(0)), 2);
} else {
//C(N)(N)(=O)
//C(R)(O)(=O)
if (terminalOxygenAtoms.size() >= 2) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
continue;
}
//C(R)(OR)(=O)
a = connectedAtom(mol, i, 8, 2, 0, 0);
b = connectedBond(mol, i, 8, 1);
if (a >= 0 && b >= 0) {
mol.setBondOrder(b, 2);
continue;
}
//C(R)(SR)(=O)
a = connectedAtom(mol, i, 16, 2, 0, 0);
b = connectedBond(mol, i, 8, 1);
if (a >= 0 && b >= 0) {
mol.setBondOrder(b, 2);
continue;
}
//C(R)(NR)(=O)
a = connectedAtom(mol, i, 7, 3, 0, 0);
b = connectedBond(mol, i, 8, 1);
if (a >= 0 && b >= 0) {
mol.setBondOrder(b, 2);
continue;
}
//C(R)(SR)(=S)
a = connectedAtom(mol, i, 16, 2, 0, 0);
b = connectedBond(mol, i, 16, 1);
if (a >= 0 && b >= 0) {
mol.setBondOrder(b, 2);
continue;
}
//C(R)(NR)(=S)
a = connectedAtom(mol, i, 7, 3, 0, 0);
b = connectedBond(mol, i, 16, 1);
if (a >= 0 && b >= 0) {
mol.setBondOrder(b, 2);
continue;
}
//C(CR)(N)(=N)
if (terminalDoubleBondAtoms.size() == 2 &&
mol.getAtomicNo(terminalDoubleBondAtoms.get(0)) == 7 &&
mol.getAtomicNo(terminalDoubleBondAtoms.get(1)) == 7 &&
(mol.getAtomicNo(a1) == 6 ||
mol.getAtomicNo(a2) == 6 ||
mol.getAtomicNo(a3) == 6)) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
}
}
} else if( mol.getAtomicNo(i) == 7 ) {
// TODO: evaluate the probability of a certain SP order from the geometry
// slightly disordered nitrate ion
if ( spOrder[i] == 2 ) {
//N(R)(R)C=O -> Amide
a = connectedAtom(mol, i, 6, 2, 8, 1);
b = connectedBond(mol, a, 8, 1);
if (a >= 0 && b >= 0) {
mol.setBondOrder(b, 2);
localAtomPiCount[i] += 1;
localAtomPiCount[b] += 1;
} else
// N(O)(=O) -> Nitro
// nitrate ion
// NF2O(1+)
if ( terminalAtomCount > 1 && terminalOxygenAtoms.size() > 0) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
mol.setAtomCharge(i, +1);
}
}
} else if (mol.getAtomicNo(i) == 16) {
if (spOrder[i] == 2) {
// sulphur trioxide
if (terminalOxygenAtoms.size() > 0) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 3);
}
} else if (spOrder[i] == 3) {
// sulphite ion
if (terminalOxygenAtoms.size() > 0) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
}
}
// Cl, Br, I
} else if ( mol.getAtomicNo(i) == 17 ||
mol.getAtomicNo(i) == 35 ||
mol.getAtomicNo(i) == 53 ) {
if ( spOrder[i] == 3 && terminalDoubleBondAtoms.size() > 0 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 2);
}
} else if ( mol.getAtomicNo(i) == 34 ) {
if (oxygenCount == 3) {
if (spOrder[i] == 2) {
// selenium trioxide
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 2);
} else if (spOrder[i] == 3) {
// selenite ion
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
}
} else {
mol.setAtomCharge(i, +1);
}
// Te
} else if( mol.getAtomicNo(i) == 52 ) {
mol.setAtomCharge(i, +1);
}
} else if( mol.getAllConnAtoms(i) == 4 ) {
// phosphorus (P) and arsenic (As)
if( mol.getAtomicNo(i) == 15 || mol.getAtomicNo(i) == 33 ) {
if (terminalDoubleBondAtoms.isEmpty()) continue;
// skip is the atom already has a double bond
if (localAtomPiCount[i] != 0) continue;
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
localAtomPiCount[i] += 1;
localAtomPiCount[mol.getConnAtom(i, sortedAtoms[0])] += 1;
} else if( mol.getAtomicNo(i)==16 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 2);
// Cl, Br, I
} else if( mol.getAtomicNo(i) == 17 ||
mol.getAtomicNo(i) == 35 ||
mol.getAtomicNo(i) == 53 ) {
if ( terminalDoubleBondAtoms.size() > 0 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 3);
} else {
// Set a negative charge to hypervalent atom
mol.setAtomCharge(i, -1);
}
} else if( mol.getAtomicNo(i) == 34 ) {
if ( oxygenCount == 4 && terminalOxygenAtoms.size() > 1 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 2);
}
}
} else if( mol.getAllConnAtoms(i) == 5 ) {
if ( mol.getAtomicNo(i) == 14 ) { // Silicon (Si)
mol.setAtomCharge(i, -1);
} else if ( mol.getAtomicNo(i) == 53 ) { // Iodine (I)
if ( terminalOxygenAtoms.size() > 0 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalOxygenAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 2);
}
}
} else if ( mol.getAllConnAtoms(i) == 6) {
if (mol.getAtomicNo(i) == 14) { // Silicon (Si)
mol.setAtomCharge(i, -2);
} else if (mol.getAtomicNo(i) == 32) { // Germanium (Ge)
mol.setAtomCharge(i, -2);
} else if (mol.getAtomicNo(i) == 33) { // Arsenic (As)
mol.setAtomCharge(i, -1);
} else if (mol.getAtomicNo(i) == 34) { // Selenium (Se)
if ( terminalDoubleBondAtoms.size() > 0 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
} else {
mol.setAtomCharge(i, -2);
}
} else if( mol.getAtomicNo(i) == 52 ) { // Tellurium (Te)
// telluric acid has a net charge of 0
// even though it has 6 neighbours
if ( terminalDoubleBondAtoms.size() == 0 && oxygenCount == 0 ) {
mol.setAtomCharge(i, -2);
}
} else if( mol.getAtomicNo(i) == 53 ) { // Iodine (I)
if ( terminalDoubleBondAtoms.size() > 0 ) {
int[] sortedAtoms = sortAtomsByBondLength(mol, i, terminalDoubleBondAtoms);
setTerminalDoubleBonds(mol, i, sortedAtoms, 1);
} else {
mol.setAtomCharge(i, +1);
}
}
}
}
mol.ensureHelperArrays(Molecule3D.cHelperRings);
}
/**
* Evaluates if the given nitrogen atom serves as a part of a nitroso (R-N=O)
* group and assigns appropriate bond orders and atom charges if needed.
*
* Current implementation uses the following assumptions that are not explicitly checked:
* - the provided nitrogen atom has only two neighbour atoms;
* - one of the neighbour atoms is an oxygen atom capable of a double bond;
*
* @param mol
* Molecule that contains the atoms.
* @param nAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveNitrosoGroup(Molecule3D mol, int nAtom) {
int oAtom;
int rAtom;
if (mol.getAtomicNo(mol.getConnAtom(nAtom, 0)) == 8) {
oAtom = mol.getConnAtom(nAtom, 0);
rAtom = mol.getConnAtom(nAtom, 1);
} else {
oAtom = mol.getConnAtom(nAtom, 1);
rAtom = mol.getConnAtom(nAtom, 0);
}
// c-nitroso group (RC-N=O)
if (mol.getAtomicNo(rAtom) == 6) {
mol.setBondOrder(mol.getBond(nAtom, oAtom), 2);
return true;
}
return false;
}
/**
* Evaluates if the given atoms could potentially comprise a cyano group (R-C#N).
*
* @param mol
* Molecule that contains the atoms.
* @param cAtom
* Index of the carbon atom to be evaluated.
* @param nAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if the provided atoms could potentially
* comprise the group.
*/
private static boolean looksLikeCyanoGroup(Molecule3D mol, int cAtom, int nAtom) {
if (mol.getAtomicNo(cAtom) != 6) return false;
if (calculateHybridizationState(mol, cAtom) != 1) return false;
if (mol.getAtomicNo(nAtom) != 7) return false;
if (mol.getAllConnAtoms(nAtom) != 1) return false;
if (mol.getAttachedHydrogenCount(nAtom) != 0) return false;
return true;
}
/**
* Evaluates if the given atoms comprise a cyano group (R-C#N)
* and assigns appropriate bond orders and atom charges if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param cAtom
* Index of the carbon atom to be evaluated.
* @param nAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveCyanoGroup(Molecule3D mol, int cAtom, int nAtom) {
if (!looksLikeCyanoGroup(mol, cAtom, nAtom)) return false;
// cyano group (R-C#N)
mol.setBondOrder(mol.getBond(cAtom, nAtom), 3);
return true;
}
/**
* Evaluates if the given atoms comprise an isocyanate group (R-N=C=O)
* and assigns appropriate bond orders and atom charges if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param cAtom
* Index of the carbon atom to be evaluated.
* @param nAtom
* Index of the nitrogen atom to be evaluated.
* @param oAtom
* Index of the oxygen atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveIsocyanateGroup(Molecule3D mol, int cAtom, int nAtom, int oAtom) {
if (mol.getAtomicNo(nAtom) != 7) return false;
if (mol.getAtomicNo(cAtom) != 6) return false;
if (calculateHybridizationState(mol, cAtom) != 1) return false;
if (mol.getAtomicNo(oAtom) != 8) return false;
if (mol.getAllConnAtoms(oAtom) != 1) return false;
if (mol.getAttachedHydrogenCount(oAtom) != 0) return false;
// isocyanate group (N=C=O)
mol.setBondOrder(mol.getBond(nAtom, cAtom), 2);
mol.setBondOrder(mol.getBond(cAtom, oAtom), 2);
return true;
}
/**
* Evaluates if the given atoms comprise an isothiocyanate group (R-N=C=S)
* and assigns appropriate bond orders and atom charges if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param cAtom
* Index of the carbon atom to be evaluated.
* @param nAtom
* Index of the nitrogen atom to be evaluated.
* @param sAtom
* Index of the sulphur atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveIsothiocyanateGroup(Molecule3D mol, int cAtom, int nAtom, int sAtom) {
if (mol.getAtomicNo(nAtom) != 7) return false;
if (mol.getAtomicNo(cAtom) != 6) return false;
if (calculateHybridizationState(mol, cAtom) != 1) return false;
if (mol.getAtomicNo(sAtom) != 16) return false;
if (mol.getAllConnAtoms(sAtom) != 1) return false;
if (mol.getAttachedHydrogenCount(sAtom) != 0) return false;
// isothiocyanate group (N=C=S)
mol.setBondOrder(mol.getBond(nAtom, cAtom), 2);
mol.setBondOrder(mol.getBond(cAtom, sAtom), 2);
return true;
}
/**
* Evaluates if the given atoms comprise a thiocyanate ion (S=C=[N-])
* and assigns appropriate bond orders and atom charges if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param cAtom
* Index of the carbon atom to be evaluated.
* @param sAtom
* Index of the sulphur atom to be evaluated.
* @param nAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolverThiocyanateIon(Molecule3D mol, int cAtom, int sAtom, int nAtom) {
if (mol.getAtomicNo(cAtom) != 6) return false;
if (calculateHybridizationState(mol, cAtom) != 1) return false;
if (mol.getAtomicNo(nAtom) != 7) return false;
if (mol.getAtomicNo(sAtom) != 16) return false;
// TODO: consider the resonance structure of SCN ([S-]-C=N <=> S=C=N[-])?
// thiocyanate ion (S=C=N[-])
mol.setBondOrder(mol.getBond(sAtom, cAtom), 2);
mol.setBondOrder(mol.getBond(cAtom, nAtom), 2);
mol.setAtomCharge(nAtom, -1);
return true;
}
/**
* Evaluate if the given atom could potentially act as the central
* atom of a azide group (R[N]=[N+]=[N-]).
*
* @param mol
* Molecule that contains the atoms.
* @param nCentralAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if the given atom could potentially
* act as the central atom of an azide group.
*/
private static boolean looksLikeUnresolvedAzideGroup(Molecule3D mol, int nCentralAtom) {
if (mol.getAtomicNo(nCentralAtom) != 7) return false;
if (calculateHybridizationState(mol, nCentralAtom) != 1) return false;
if (mol.getConnAtoms(nCentralAtom) != 2) return false;
if (getAllConnAtoms(mol, nCentralAtom, true) != 2) return false;
int nAtom1 = mol.getConnAtom(nCentralAtom, 0);
int nAtom2 = mol.getConnAtom(nCentralAtom, 1);
if (mol.getAtomicNo(nAtom1) != 7) return false;
if (getAllConnAtoms(mol, nAtom1, true) > 2) return false;
if (mol.getAtomicNo(nAtom2) != 7) return false;
if (getAllConnAtoms(mol, nAtom2, true) > 2) return false;
// check if there is at least one terminal atom
if (getAllConnAtoms(mol, nAtom1, true) != 1 &&
getAllConnAtoms(mol, nAtom2, true) != 1) {
return false;
}
return true;
}
/**
* Evaluate if the given atom could potentially act as the central
* atom of an azide group (R[N]=[N+]=[N-]) and assigns appropriate bond
* orders and atom charges for the entire group if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param nCentralAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveAzideGroup(Molecule3D mol, int nCentralAtom) {
if (!looksLikeUnresolvedAzideGroup(mol, nCentralAtom)) return false;
int nAtom1 = mol.getConnAtom(nCentralAtom, 0);
int nAtom2 = mol.getConnAtom(nCentralAtom, 1);
int shorterBondAtom;
int longerBondAtom;
if (getBondLength(mol, nCentralAtom, nAtom1) < getBondLength(mol, nCentralAtom, nAtom2)) {
shorterBondAtom = nAtom1;
longerBondAtom = nAtom2;
} else {
shorterBondAtom = nAtom2;
longerBondAtom = nAtom1;
}
double shorterBondLength = getBondLength(mol, nCentralAtom, shorterBondAtom);
double longerBondLength = getBondLength(mol, nCentralAtom, longerBondAtom);
boolean accommodatesTripleBond = calculateHybridizationState(mol, shorterBondAtom) == 1 &&
StrictMath.abs(shorterBondAtom - longerBondAtom) > 0.05;
if (accommodatesTripleBond) {
double NNSingleBondLength = getIdealisedBondLength(2, 7, 7, 1, 2);
double NNDoubleBondLength = getIdealisedBondLength(2, 7, 7, 1, 2);
double NNTripleBondLength = getIdealisedBondLength(3, 7, 7, 2, 2);
double N2N2N = StrictMath.abs(NNDoubleBondLength - shorterBondLength) +
StrictMath.abs(NNDoubleBondLength - longerBondLength);
double N1N3N = StrictMath.abs(NNTripleBondLength - shorterBondLength) +
StrictMath.abs(NNSingleBondLength - longerBondLength);
accommodatesTripleBond = N1N3N < N2N2N;
}
// choose between the resonance structures
// RN=[N+]=[N-] and R[N-]-[N+]#N
if (accommodatesTripleBond) {
mol.setBondOrder(mol.getBond(nCentralAtom, shorterBondAtom), 3);
mol.setBondOrder(mol.getBond(nCentralAtom, longerBondAtom), 1);
mol.setAtomCharge(nCentralAtom, +1);
// in case of NH2-[N+]#N
// TODO: check if this conditional is needed
if (mol.getFreeValence(longerBondAtom) > 0) {
mol.setAtomCharge(longerBondAtom, -1);
}
} else {
mol.setBondOrder(mol.getBond(nCentralAtom, shorterBondAtom), 2);
mol.setBondOrder(mol.getBond(nCentralAtom, longerBondAtom), 2);
mol.setAtomCharge(nCentralAtom, +1);
if (mol.getFreeValence(shorterBondAtom) > 0 || calculateHybridizationState(mol, shorterBondAtom) == 1) {
mol.setAtomCharge(shorterBondAtom, -1);
}
if (mol.getFreeValence(longerBondAtom) > 0 || calculateHybridizationState(mol, longerBondAtom) == 1) {
mol.setAtomCharge(longerBondAtom, -1);
}
}
mol.ensureHelperArrays(Molecule.cHelperRings);
return true;
}
/**
* Evaluate if the given atom could potentially act as the central
* atom of a azide ion ([N-]=[N+]=[N-]).
*
* @param mol
* Molecule that contains the atoms.
* @param nCentralAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if the given atom could potentially
* act as the central atom of an azide ion.
*/
private static boolean looksLikeUnresolvedAzideIon(Molecule3D mol, int nCentralAtom) {
if (!looksLikeUnresolvedAzideGroup(mol, nCentralAtom)) return false;
int nAtom1 = mol.getConnAtom(nCentralAtom, 0);
int nAtom2 = mol.getConnAtom(nCentralAtom, 1);
if (getAllConnAtoms(mol, nAtom1, true) != 1) return false;
if (getAllConnAtoms(mol, nAtom2, true) != 1) return false;
return true;
}
/**
* Evaluate if the given atom could potentially act as the central
* atom of an azide ion ([N-]=[N+]=[N-]) and assigns appropriate bond
* orders and atom charges for the entire ion if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param nCentralAtom
* Index of the nitrogen atom to be evaluated.
* @return
* Boolean value denoting if any bond order or atom charge
* modifications were applied.
*/
private static boolean resolveAzideIon(Molecule3D mol, int nCentralAtom) {
if (!looksLikeUnresolvedAzideIon(mol, nCentralAtom)) return false;
int a1 = mol.getConnAtom(nCentralAtom, 0);
int a2 = mol.getConnAtom(nCentralAtom, 1);
mol.setBondOrder(mol.getBond(nCentralAtom, a1), 2);
mol.setBondOrder(mol.getBond(nCentralAtom, a2), 2);
mol.setAtomCharge(nCentralAtom, +1);
mol.setAtomCharge(a1, -1);
mol.setAtomCharge(a2, -1);
mol.ensureHelperArrays(Molecule.cHelperRings);
return true;
}
/**
* Evaluates if the given atom is part of a diatomic pnictogen ion of
* the forms [E-2][E-2], [E-1]=[E-1] where E = P, As (i.e. [P-2][P-2],
* [P-1]=[P-1]) and assigns appropriate bond orders and atom charges
* for the entire ion if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param atom
* Index of the atom to be evaluated.
* @return
* Boolean value denoting if any atom charge modifications
* were applied.
*/
private static boolean resolveDiatomicPnictogenIons(Molecule3D mol, int atom) {
if (!mol.isNitrogenFamily(atom)) return false;
// skip nitrogen (N) for now
if (mol.getAtomicNo(atom) == 7) return false;
if (getAllConnAtoms(mol, atom, true) != 1) return false;
if (mol.getConnAtoms(atom) != 1) return false;
int nAtom = mol.getConnAtom(atom, 0);
if (!mol.isNitrogenFamily(nAtom)) return false;
// skip nitrogen (N) for now
if (mol.getAtomicNo(nAtom) == 7) return false;
if (mol.getConnAtoms(nAtom) != 1) return false;
// double bonded form ([E-]=[E-])
if (mol.getAtomicNo(atom) == 15 && mol.getAtomicNo(nAtom) == 15) {
// TODO: eventually replace the hardcoded value with one based on statistics
// Explanation: the provided cutoff value was chosen based
// on bond distances from 6 crystal structures that were
// show to contain the P=P ligand. The hardcoded value may
// eventually be removed if sufficiently granular and accurate
// statistics become available.
double phosphorusDoubleBondCuttoff = 2.05;
if (phosphorusDoubleBondCuttoff > getBondLength(mol, atom, nAtom)) {
mol.setBondOrder(mol.getBond(atom, nAtom), 2);
mol.setAtomCharge(atom, -1);
mol.setAtomCharge(nAtom, -1);
return true;
}
}
// TODO: investigate if a cutoff value for As is also needed
// single bonded form ([E-2][E-2])
mol.setAtomCharge(atom, -2);
mol.setAtomCharge(nAtom, -2);
return true;
}
/**
* Evaluates if the given atom is part of a diatomic chalcogen ion of
* the form [E-][E-] where E = S, Se, Te (i.e. [S-][S-]) and assigns
* appropriate atom charges for the entire ion if needed.
*
* @param mol
* Molecule that contains the atoms.
* @param atom
* Index of the atom to be evaluated.
* @return
* Boolean value denoting if any atom charge modifications
* were applied.
*/
private static boolean resolveDiatomicChalcogenIons(Molecule3D mol, int atom) {
if (!mol.isChalcogene(atom)) return false;
// skip oxygen (O) for now
if (mol.getAtomicNo(atom) == 8) return false;
if (getAllConnAtoms(mol, atom, true) != 1) return false;
if (mol.getConnAtoms(atom) != 1) return false;
int nAtom = mol.getConnAtom(atom, 0);
if (!mol.isChalcogene(nAtom)) return false;
// skip oxygen (O) for now
if (mol.getAtomicNo(nAtom) == 8) return false;
if (mol.getConnAtoms(nAtom) != 1) return false;
mol.setAtomCharge(atom, -1);
mol.setAtomCharge(nAtom, -1);
return true;
}
static void coordinationBondsToCovalent(Molecule3D mol) {
for (int i = 0; i < mol.getRingSet().getSize(); i++ ) {
if ( isRingCoordinated(mol, i) ) {
int ringAtom = mol.getRingSet().getRingAtoms(i)[0];
int metalAtom = -1;
for ( int j = mol.getAllConnAtoms(ringAtom); j < mol.getAllConnAtomsPlusMetalBonds(ringAtom); j++) {
if ( mol.isMetalAtom( mol.getConnAtom(ringAtom, j) ) ) {
metalAtom = mol.getConnAtom(ringAtom, j);
break;
}
}
for (int j = 0; j < mol.getRingSet().getRingSize(i); j++) {
mol.markBondForDeletion(mol.getBond(mol.getRingSet().getRingAtoms(i)[j], metalAtom));
}
}
}
mol.deleteMarkedAtomsAndBonds();
mol.ensureHelperArrays(Molecule3D.cHelperNeighbours);
for (int i = 0; i < mol.getAllBonds(); i++ ) {
if ( mol.getBondType(i) == Molecule3D.cBondTypeMetalLigand ) {
mol.setBondOrder(i, 1);
int a1 = mol.getBondAtom(0, i);
int a2 = mol.getBondAtom(1, i);
if ( mol.isMetalAtom(a1) && mol.isMetalAtom(a2) ) continue;
if ( mol.isMetalAtom(a1) || mol.getAtomicNo(a1) == 1 ) {
mol.setAtomCharge( a1, mol.getAtomCharge(a1) - 1 );
mol.setAtomCharge( a2, mol.getAtomCharge(a2) + 1 );
} else if ( mol.isMetalAtom(a2) || mol.getAtomicNo(a2) == 1 ) {
mol.setAtomCharge( a1, mol.getAtomCharge(a1) + 1 );
mol.setAtomCharge( a2, mol.getAtomCharge(a2) - 1 );
} else {
throw new RuntimeException( "a metal-ligand bond was assigned to a non-metal atoms " +
"'" + mol.getAtomName(a1) + "' and '" + mol.getAtomName(a2) + "'" );
}
}
}
}
private static boolean isPlanar(Molecule3D mol, int a1, int a2) {
Coordinates ci = mol.getCoordinates(a1);
Coordinates u = null, v = null;
for (int i = 0; v == null && i < mol.getAllConnAtoms(a1); i++) {
if ( u == null ) {
u = mol.getCoordinates(mol.getConnAtom(a1, i)).subC(ci);
} else {
v = mol.getCoordinates(mol.getConnAtom(a1, i)).subC(ci);
}
}
for (int i = 0; v == null && i < mol.getAllConnAtoms(a2); i++) {
if ( u == null ) {
u = mol.getCoordinates(mol.getConnAtom(a2, i)).subC(ci);
} else {
v = mol.getCoordinates(mol.getConnAtom(a2, i)).subC(ci);
}
}
if ( u == null ) return false;
Coordinates normal = u.cross(v);
if ( normal.distSq() == 0 ) return false; //what to do?
normal = normal.unitC();
Coordinates cj = mol.getCoordinates(a2);
for(int k = 0; k < mol.getAllConnAtoms(a2); k++) {
Coordinates ck = mol.getCoordinates(mol.getConnAtom(a2, k));
if(Math.abs(ck.subC(cj).dot(normal))>0.25) return false;
}
for(int k=0; k0.25) return false;
}
return true;
}
/**
* Util function for substructure searches. It searches for the substructure
* of form a-b-c where a, b, c are consecutively connected atoms and a != c.
* If a negative value is provided as the atomic number or valency of atom c
* this atom will be ignored and only the a-b substructure search will be
* carried out.
*
* @param mol
* The molecule that contains the atoms.
* @param aIndex
* The index of the atom 'a' in the molecule. The atom 'a' is used
* as the starting position of the substructure search.
* @param bAtomicNo
* The atomic number of the substructure atom 'b' that should be
* connected directly to the atom 'a'.
* @param bValence
* The valence of the substructure atom 'b' that should be connected
* directly to the atom 'a'.
* @param cAtomicNo
* The atomic number of the substructure atom 'c' that should be
* connected directly to the atom 'b'.
* @param cValence
* The valence of the substructure atom 'c' that should be connected
* directly to the atom 'b'.
* @return
* Index of the atom 'b' in the molecule is the search is successful
* and -1 otherwise.
*/
private static int connectedAtom( Molecule3D mol, int aIndex, int bAtomicNo,
int bValence, int cAtomicNo, int cValence ) {
loop: for(int i=0; i 0 && mol.getAtomicNo(atm) != bAtomicNo ) continue;
if ( bValence > 0 && mol.getAllConnAtoms(atm) != bValence ) continue;
// Checking if the atom 'c' that is connected to atom 'b'
// is of the the requested chemical type and valence
if ( cAtomicNo > 0 || cValence > 0 ) {
for ( int j = 0; j < mol.getAllConnAtoms(atm); j++ ) {
int otherAtm = mol.getConnAtom(atm, j);
if(otherAtm==aIndex) continue loop;
if ( cAtomicNo > 0 && mol.getAtomicNo(otherAtm) != cAtomicNo ) continue loop;
if ( cValence > 0 && mol.getAllConnAtoms(otherAtm) != cValence ) continue loop;
}
}
return atm;
}
return -1;
}
private static double getBondLength(Molecule3D mol, int a1, int a2) {
return mol.getCoordinates(a1).distance(mol.getCoordinates(a2));
}
private static double getBondLength(Molecule3D mol, int bond) {
return getBondLength(mol, mol.getBondAtom(0, bond), mol.getBondAtom(1, bond));
}
private static int connectedBond(Molecule3D mol, int a, int atomicNo, int valence) {
if(a<0) return -1;
for(int i=0; i< mol.getAllConnAtoms(a); i++) {
int atm = mol.getConnAtom(a, i);
if(atomicNo>0 && mol.getAtomicNo(atm)!=atomicNo) continue;
if(valence>0 && mol.getAllConnAtoms(atm)!=valence) continue;
return mol.getConnBond(a, i);
}
return -1;
}
/**
* Returns the average bond length value based on bond order
* and the type and pi electron count of the bonded atoms.
* The values are taken from the statistical model
* constructed from previously observed bond parameter
* values.
* @param bondOrder
* Bond order expressed as a positive integer.
* @param atomicNo1
* Atomic number of the first atom.
* @param atomicNo2
* Atomic number of the second atom.
* @param atomPi1
* Pi electron count of the first atom.
* @param atomPi2
* Pi electron count of the second atom.
* @return
* The average bond length.
*/
private static double getIdealisedBondLength(int bondOrder, int atomicNo1, int atomicNo2, int atomPi1, int atomPi2) {
return BondLengthSet.getBondLength(BondLengthSet.getBondIndex(bondOrder, false, false, atomicNo1, atomicNo2, atomPi1, atomPi2));
}
/**
* Produce a logical estimate of the bond being aromatic based
* strictly on length constraints. The bond length is compared
* to the average single bond.
* @param mol
* The molecule that contains the bond.
* @param a1
* Index of the first atom that forms a bond.
* @param a2
* Index of the second atom that forms a bond.
* @return
* Estimate deciding if the bond could be aromatic
* based strictly on the bond length.
*/
private static boolean fitsAromaticLengthConstraints(Molecule3D mol, int a1, int a2) {
int atomType1 = Math.min(mol.getAtomicNo(a1), mol.getAtomicNo(a2));
int atomType2 = Math.max(mol.getAtomicNo(a1), mol.getAtomicNo(a2));
int pi1 = BondLengthSet.isPiConsidered(atomType1) ? 1 : -1;
int pi2 = BondLengthSet.isPiConsidered(atomType2) ? 1 : -1;
// Try to get statistics for the aromatic single bond
boolean aromaticConstraintsUsed = true;
int index = BondLengthSet.getBondIndex(1, true, false, atomType1, atomType2, pi1, pi2);
// If no such bond exist, try to get statistics for the
// regular single bond
if ( index == -1 ) {
aromaticConstraintsUsed = false;
index = BondLengthSet.getBondIndex(1, false, false, atomType1, atomType2, pi1, pi2);
if (index == -1) return false;
}
double bondLength = BondLengthSet.getBondLength(index);
double sigma = BondLengthSet.getBondStdDev(index);
if ( sigma < 0 ) sigma = 0.1;
double distance = getBondLength(mol, a1, a2);
// allow 3 standard deviations for aromatic bonds and 1 for non-aromatic
return ( distance < bondLength + ( aromaticConstraintsUsed ? 3 : 1 ) * sigma);
}
/**
* Produce a numerical estimate of how well the bond length fits the statistical model
* constructed from previously observed bond parameter values. The estimate is provided
* as a deviation value from the idealised value (smaller values mean better fitness).
* Currently it only compares the bond length with the average bond length of the same
* type, but a more intricate model might be implemented in the future.
* @param mol
* The molecule that contains the bond.
* @param a1
* Index of the first atom that forms a bond.
* @param a2
* Index of the second atom that forms a bond.
* @param bondOrder
* Order of the bond. Currently only values '1' (single bond)
* and '2' (double bond) are supported.
* @return
* Bond length deviation from the previously observed bond parameter value.
*/
private static double estimatePiBondFitness(Molecule3D mol, int a1, int a2, int bondOrder) {
int atomType1 = Math.min(mol.getAtomicNo(a1), mol.getAtomicNo(a2));
int atomType2 = Math.max(mol.getAtomicNo(a1), mol.getAtomicNo(a2));
double singleBond = getIdealisedBondLength(1, atomType1, atomType2, 1, 1);
double doubleBond = getIdealisedBondLength(2, atomType1, atomType2, 1, 1);
boolean statisticsAvailable = singleBond != -1 || doubleBond != -1;
if (!statisticsAvailable) {
throw new RuntimeException("bond length statistics for bond '" + atomType1
+ "-" + atomType2 + "' is not available");
}
double idealLength = ( bondOrder == 1 ) ? singleBond : doubleBond;
if ( bondOrder == 2 && getBondLength(mol, a1, a2) < idealLength ) {
return 0;
}
return StrictMath.abs( getBondLength(mol, a1, a2) - idealLength );
}
private static boolean isAlkaliMetalAtom (Molecule3D mol, int atom) {
return ( mol.getAtomicNo(atom) == 3 ) ||
( mol.getAtomicNo(atom) == 11 ) ||
( mol.getAtomicNo(atom) == 19 ) ||
( mol.getAtomicNo(atom) == 37 ) ||
( mol.getAtomicNo(atom) == 55 ) ||
( mol.getAtomicNo(atom) == 87 );
}
private static boolean isAlkalineEarthMetalAtom(Molecule3D mol, int atom) {
return ( mol.getAtomicNo(atom) == 4 ) ||
( mol.getAtomicNo(atom) == 12 ) ||
( mol.getAtomicNo(atom) == 20 ) ||
( mol.getAtomicNo(atom) == 38 ) ||
( mol.getAtomicNo(atom) == 56 ) ||
( mol.getAtomicNo(atom) == 88 );
}
List checkMoleculeForBumps(Molecule3D mol) {
List bumps = new LinkedList<>();
double distance;
for (int i = 0; i < mol.getAllAtoms(); i++) {
for (int j = i+1; j < mol.getAllAtoms(); j++) {
if ( i < j ) {
distance = mol.getCoordinates(i).distance(mol.getCoordinates(j));
if ( distance < SPECIAL_POSITION_CUTOFF ||
distance < BUMP_FACTOR * ( COVALENT_RADIUS[mol.getAtomicNo(i)] +
COVALENT_RADIUS[mol.getAtomicNo(j)] ) ) {
bumps.add( "atoms '" + mol.getAtomName(i) + "' and '" + mol.getAtomName(j) + "' " +
"are too close (distance = " + String.format("%6.4f", distance) + ") and " +
"are considered a bump" );
}
}
}
}
return bumps;
}
private static boolean hasMetalLigandBonds(Molecule3D mol, int atom) {
return (mol.getMetalBondedConnAtoms(atom) > 0);
}
private static boolean isCommonIonCharge(int atomicNo, int charge) {
for ( int i = 0; i < Molecule.cCommonOxidationState[atomicNo].length; i++ ) {
if ( Molecule.cCommonOxidationState[atomicNo][i] == charge ) {
return true;
} else if ( Molecule.cCommonOxidationState[atomicNo][i] > charge ) {
return false;
}
}
return false;
}
private static boolean isCommonIonCharge(Molecule3D mol, int atomicNo, int charge) {
return isCommonIonCharge(mol.getAtomicNo(atomicNo), charge);
}
public static int getMaximumIonCharge(Molecule3D mol, int atom) {
if ( Molecule.cCommonOxidationState[mol.getAtomicNo(atom)] == null ) {
return 0;
} else {
return Molecule.cCommonOxidationState[mol.getAtomicNo(atom)]
[Molecule.cCommonOxidationState[mol.getAtomicNo(atom)].length - 1];
}
}
private static boolean maximumIonChargeReached(Molecule3D mol, int atom) {
return mol.getAtomCharge(atom) >= getMaximumIonCharge(mol, atom);
}
private static int getDelocalizedBondCount(Molecule3D mol, int atom) {
return getDelocalizedBonds(mol, atom).size();
}
private static List getDelocalizedBonds(Molecule3D mol, int atom) {
List delocalizedBonds = new LinkedList<>();
for( int i = 0; i < mol.getAllConnAtoms(atom); i++) {
if ( mol.getBondType(mol.getConnBond(atom, i)) == Molecule3D.cBondTypeDelocalized ) {
delocalizedBonds.add(mol.getConnBond(atom, i));
}
}
return delocalizedBonds;
}
private static List> getPiBondFragments (Molecule3D mol) {
List> fragments = new LinkedList<>();
mol.ensureHelperArrays(Molecule3D.cHelperRings);
for (int i = 0; i < mol.getAllAtoms(); i++) {
if (getDelocalizedBondCount(mol, i) == 1) {
// Check if the selected atom is not already at the end of analyzed fragments
boolean used = false;
for (List fragment1 : fragments) {
if (fragment1.get(fragment1.size() - 1) == i) {
used = true;
break;
}
}
if ( used ) { continue; }
List fragment = new LinkedList<>();
fragment.add(i);
int prevBond = getDelocalizedBonds(mol, i).get(0);
int atom;
if ( mol.getBondAtom( 0, prevBond ) != i ) {
atom = mol.getBondAtom( 0, prevBond );
} else {
atom = mol.getBondAtom( 1, prevBond );
}
boolean finished = false;
while (!finished) {
// Currently only process linear pi-bond systems
// TODO: extend the logic to process branched systems
if (getDelocalizedBondCount(mol, atom) > 2) {
break;
}
fragment.add(atom);
if (getDelocalizedBondCount(mol, atom) == 1) {
fragments.add(fragment);
finished = true;
} else {
int currentBond = ( getDelocalizedBonds(mol, atom).get(0) != prevBond ) ?
getDelocalizedBonds(mol, atom).get(0) :
getDelocalizedBonds(mol, atom).get(1);
if ( mol.getBondAtom( 0, currentBond ) != atom ) {
atom = mol.getBondAtom( 0, currentBond );
} else {
atom = mol.getBondAtom( 1, currentBond );
}
prevBond = currentBond;
}
}
}
}
return fragments;
}
private static boolean fragmentCanResonate(Molecule3D mol, List fragment) {
for (Integer atom : fragment) {
if (mol.getFreeValence(atom) - 1 < 0) {
return false;
}
}
return true;
}
private static int[] sortAtomsByBondLength(Molecule3D mol, int a, List bondAtoms) {
double[] bondLength = new double[bondAtoms.size()];
for (int i = 0; i < bondLength.length; i++ ) {
bondLength[i] = getBondLength(mol, a, mol.getConnAtom(a, bondAtoms.get(i)));
}
// using primitive bubble sort for now to sort the indexes
int[] indexes = new int[bondAtoms.size()];
for (int i = 0; i < bondLength.length; i++) {
indexes[i] = i;
}
for (int i = 0; i < bondLength.length; i++) {
for (int j = i+1; j < bondLength.length; j++ ) {
if( bondLength[indexes[i]] > bondLength[indexes[j]] ) {
int tmp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = tmp;
}
}
}
// replacing sort indexes with atom indexes
for (int i = 0; i < bondLength.length; i++) {
indexes[i] = bondAtoms.get(indexes[i]);
}
return indexes;
}
private static Coordinates calculatePlaneNormal (Coordinates p1, Coordinates p2, Coordinates p3) {
Coordinates u = p1.subC(p2);
Coordinates v = p1.subC(p3);
return u.cross(v);
}
private static double calculatePointToPlaneDistance ( Coordinates normal, Coordinates p0, Coordinates p1 ) {
Coordinates v1 = p0.subC(p1);
return StrictMath.abs(normal.unitC().dot(v1) / v1.dist());
}
private static void setTerminalDoubleBonds( Molecule3D mol, int a, int[] sortedAtoms,
int maxDoubleBondCount ) {
int doubleBondCount = 0;
for (int sortedAtom : sortedAtoms) {
if (doubleBondCount < maxDoubleBondCount) {
mol.setBondOrder(mol.getConnBond(a, sortedAtom), 2);
doubleBondCount++;
} else {
mol.setBondOrder(mol.getConnBond(a, sortedAtom), 1);
mol.setAtomCharge(mol.getConnAtom(a, sortedAtom), -1);
}
}
}
// TODO: ask to add to the molecule
private static boolean isRingAtom(Molecule3D mol, int ring, int a) {
int[] ringAtoms = mol.getRingSet().getRingAtoms(ring);
for (int ringAtom : ringAtoms) {
if (ringAtom == a) return true;
}
return false;
}
private static void identifySquarates(Molecule3D mol, int[] spOrder) {
for (int i = 0; i < mol.getAtoms(); i++) {
if (mol.getAtomRingSize(i) != 4) continue;
if (getRingCount(mol, i) != 1) continue;
int ring = getSmallestRing(mol, i);
int[] ringAtoms = mol.getRingSet().getRingAtoms(ring);
boolean[] allowsExocyclicDoubleBond = new boolean[ringAtoms.length];
int[] exocyclicAtoms = new int[ringAtoms.length];
boolean fitsConstraints = true;
for (int j = 0; j < ringAtoms.length; j++) {
int a = ringAtoms[j];
if (mol.getAtomicNo(a) == 6 && mol.getConnAtoms(a) == 3 &&
spOrder[a] == 2 && mol.getLowestFreeValence(a) > 0) {
int a1 = ringAtoms[(j - 1 + ringAtoms.length) % ringAtoms.length];
int a2 = ringAtoms[(j + 1 + ringAtoms.length) % ringAtoms.length];
if (StrictMath.abs(90 - StrictMath.toDegrees(
getAngle(mol, a1, a, a2))) > 5) {
fitsConstraints = false;
break;
}
for (int k = 0; k < mol.getConnAtoms(a); k++) {
if (!isRingAtom(mol, ring, mol.getConnAtom(a, k))) {
allowsExocyclicDoubleBond[j] = (mol.getLowestFreeValence(mol.getConnAtom(a, k)) > 0);
exocyclicAtoms[j] = mol.getConnAtom(a, k);
break;
}
}
} else {
fitsConstraints = false;
break;
}
}
if (fitsConstraints) {
for (int a1 = 0; a1 < ringAtoms.length; a1++) {
int a2 = (a1 + 1) % ringAtoms.length;
int a3 = (a1 + 2) % ringAtoms.length;
int a4 = (a1 + 3) % ringAtoms.length;
if (allowsExocyclicDoubleBond[a1] && allowsExocyclicDoubleBond[a2]) {
mol.setBondOrder(mol.getBond(ringAtoms[a1], exocyclicAtoms[a1]), 2);
mol.setBondOrder(mol.getBond(ringAtoms[a2], exocyclicAtoms[a2]), 2);
mol.setBondOrder(mol.getBond(ringAtoms[a3], ringAtoms[a4]), 2);
if (allowsExocyclicDoubleBond[a3]) mol.setAtomCharge(exocyclicAtoms[a3], -1);
if (allowsExocyclicDoubleBond[a4]) mol.setAtomCharge(exocyclicAtoms[a4], -1);
break;
}
}
}
}
}
private static int getRingCount(Molecule3D mol, int a) {
int ringCount = 0;
RingCollection rings = mol.getRingSet();
for (int i = 0; i < mol.getRingSet().getSize(); i++) {
int[] ringAtoms = rings.getRingAtoms(i);
for (int ringAtom : ringAtoms) {
if (ringAtom == a) {
ringCount++;
break;
}
}
}
return ringCount;
}
private static int getSmallestRing(Molecule3D mol, int a) {
int smallestRing = -1;
int ringSize = -1;
RingCollection rings = mol.getRingSet();
for (int i = 0; i < mol.getRingSet().getSize(); i++) {
int[] ringAtoms = rings.getRingAtoms(i);
for (int ringAtom : ringAtoms) {
if (ringAtom == a) {
if (ringSize == -1 || ringAtoms.length < ringSize) {
ringSize = ringAtoms.length;
smallestRing = i;
break;
}
}
}
}
return smallestRing;
}
private static int getAllConnAtoms(Molecule3D mol, int a, boolean includeAttachedHydrogens) {
if ( includeAttachedHydrogens ) {
return mol.getAllConnAtoms(a) + mol.getAttachedHydrogenCount(a);
} else {
return mol.getAllConnAtoms(a);
}
}
private static boolean isRingCoordinated(Molecule3D mol, int ringNo) {
int[] ringAtoms = mol.getRingSet().getRingAtoms(ringNo);
boolean atomsAreCoordinated = true;
for (int ringAtom : ringAtoms) {
atomsAreCoordinated = atomsAreCoordinated && hasMetalLigandBonds(mol, ringAtom);
}
return atomsAreCoordinated;
}
/**
* Determines if the given atom is capable of accommodating an additional double bond.
* @param mol
* Molecule that contains the atom.
* @param atom
* Index of the atom inside the molecule data structure.
* @param atomPi
* The number of pi electrons that the atom is currently using.
* @param spOrder
* The sp order (1, 2, 3) of the atom.
* @return
* Logical value denoting is an atom can accommodate an additional double bond.
*/
private static boolean canAccommodateDoubleBond(Molecule3D mol, int atom, int atomPi, int spOrder) {
// Tetravalent silicon (Si) cannot accommodate any double bonds
if (mol.getAtomicNo(atom) == 14 && mol.getOccupiedValence(atom) > 3) return false;
// Germanium (Ge)
if (mol.getAtomicNo(atom) == 32) {
// only sp3 germanium (Ge) can potentially accommodate a double bond
if (mol.getOccupiedValence(atom) > 3) return false;
if (spOrder != 2) return false;
}
// Phosphorus (P)
if (mol.getAtomicNo(atom) == 15) {
if (atomPi > 0 && spOrder != 1) return false;
}
if ( mol.getLowestFreeValence(atom) < 1 ) return false;
if ( mol.getAtomCharge(atom) != 0 ) return false;
// Sp1-like atoms are not allowed to have more than 2 pi electrons
if ( spOrder == 1 && atomPi > 1 ) return false;
// Sp2-like atoms are not allowed to have more than 1 pi electron
if ( spOrder == 2 && atomPi > 0 ) return false;
// Atoms smaller than phosphorus are not allowed a double bond in Sp3-like configuration
if ( spOrder == 3 && mol.getAtomicNo(atom) < 14 ) return false;
return true;
}
/**
* Calculates the bond length deviation from the statistical
* average for the bond assuming it is of the given bond type.
* @param mol
* Molecule that contains the bond.
* @param bond
* The bond that is being evaluated.
* @param bondTypeIndex
* The bond type index that is used to retrieve
* the average bond length and standard deviation
* for the given bond type.
* @return
* The difference between the actual and the average
* bond lengths.
*/
private static double determineExpectedBondDeviation(Molecule3D mol, int bond, int bondTypeIndex) {
return ( getBondLength(mol, bond ) - BondLengthSet.getBondLength(bondTypeIndex));
}
/**
* Calculates the angle a1-a2-a3 between 3 atoms.
* @param mol
* The molecule that contains the atoms.
* @param a1
* Index of the first atom in the molecule.
* @param a2
* Index of the second atom in the molecule.
* @param a3
* Index of the third atom in the molecule.
* @return
* The angle a1-a2-a3 in radians.
*/
private static double getAngle(Molecule mol, int a1, int a2, int a3) {
Coordinates c1 = mol.getCoordinates(a1);
Coordinates c2 = mol.getCoordinates(a2);
Coordinates c3 = mol.getCoordinates(a3);
return c1.subC(c2).getAngle(c3.subC(c2));
}
/**
* Calculates the dihedral angle a1-a2-a3-a4 between 4 atoms.
* @param mol
* The molecule that contains the atoms.
* @param a1
* Index of the first atom in the molecule.
* @param a2
* Index of the second atom in the molecule.
* @param a3
* Index of the third atom in the molecule.
* @param a4
* Index of the fourth atom in the molecule.
* @return
* The dihedral angle a1-a2-a3-a4 in radians.
*/
private static double getDihedral(Molecule mol, int a1, int a2, int a3, int a4) {
Coordinates c1 = mol.getCoordinates(a1);
Coordinates c2 = mol.getCoordinates(a2);
Coordinates c3 = mol.getCoordinates(a3);
Coordinates c4 = mol.getCoordinates(a4);
return c1.getDihedral(c2, c3, c4);
}
public static int connected(StereoMolecule mol, int a, int atomicNo, int bondOrder) {
for(int i=0; i=0 && mol.getAtomicNo(atm)!=atomicNo) continue;
if(bondOrder>0 && mol.getConnBondOrder(a, i)!=bondOrder) continue;
return atm;
}
return -1;
}
public static boolean aromatize(Molecule3D mol, Set aromaticAtoms, Set aromaticBonds) {
ArrayList[] atomToRings = getAtomToRings(mol);
RingCollection ringSet = mol.getRingSetSimple();
return aromatize(mol,atomToRings,ringSet,aromaticAtoms,aromaticBonds);
}
public static boolean aromatize(Molecule3D mol, ArrayList[] atomToRings, RingCollection ringSet, Set aromaticAtoms, Set aromaticBonds) {
//RingCollection ringSet = mol.getRingSetSimple();
//Flag the aromatic rings
boolean[] aromaticRings = new boolean[ringSet.getSize()];
for (int i = 0; i < ringSet.getSize(); i++) {
//Is is an aromatic ring
boolean isAromatic = true;
int ringSize = ringSet.getRingSize(i);
for (int j = -1; j < ringSize; j++) {
int[] ringAtom = ringSet.getRingAtoms(i);
int a1 = j==-1? ringAtom[ringSize-1]: ringAtom[j];
int a2 = j==ringSize-1? ringAtom[0]: ringAtom[j+1];
int b = mol.getBond(a1, a2);
if(!aromaticBonds.contains(b)) {
isAromatic = false;
}
}
aromaticRings[i] = isAromatic;
}
Set nonAromaticAtoms = new HashSet();
for (int i=0;i(), true);
if(!success) success = aromatize(mol, atomToRings, ringSet, i, aromaticRings, nonAromaticAtoms, new boolean[mol.getAllAtoms()], 0, ringSet.getRingSize(i)%2, new ArrayList(), false);
if(!success) {
System.out.println("Could not aromatize ring "+i);
aromaticRings[i] = false;
ok = false;
}
}
}
return ok;
}
private static boolean aromatize(Molecule3D mol, ArrayList[] atomToRings, RingCollection ringSet, int index, boolean[] aromatic, Set nonAromaticAtoms, boolean[] visited, int seed, int left, List bondsMade, boolean easy) {
//RingCollection ringSet = mol.getRingSetSimple();
//Ends if the ring has been fully visited
int[] ring = (int[]) ringSet.getRingAtoms(index);
boolean allVisited = true;
int bestSeed = -1;
for (int i = 0; i < ring.length; i++) {
if(!visited[ring[(seed + i)%ring.length]]) {
if(bestSeed<0) bestSeed = seed + i;
allVisited = false;
}
}
if(allVisited) {
return true;
} else {
seed = bestSeed;
}
int a = ring[seed%ring.length];
int ap = ring[(seed+1)%ring.length];
if(visited[a]) { //already treated
return aromatize(mol, atomToRings, ringSet, index, aromatic, nonAromaticAtoms, visited, seed+1, left, bondsMade, easy);
} else {
//Try to create a double bond from the atom a to a connected one
for (int j = -1; j < mol.getAllConnAtoms(a); j++) {
int a2 = j==-1? ap: mol.getConnAtom(a, j);
if(visited[a2]) continue;
if(j>=0 && a2==ap) continue;
if(nonAromaticAtoms.contains(a2)) continue;
if(nonAromaticAtoms.contains(a)) continue;
if(mol.getAtomicNo(a)==8 || mol.getAtomicNo(a)==16) continue;
if(mol.getAtomicNo(a2)==8 || mol.getAtomicNo(a2)==16) continue;
if(easy && mol.getFreeValence(a) <= 0) continue;
if(easy && mol.getFreeValence(a2) <= 0) continue;
if(connected(mol, a, -1, 2)>=0) continue;
if(connected(mol, a2, -1, 2)>=0) continue;
visited[a] = visited[a2] = true;
int b = mol.getBond(a, a2);
mol.setBondOrder(b, 2);
//Test whole ring
List trackBondsMade = new ArrayList();
boolean success = aromatize(mol, atomToRings, ringSet, index, aromatic, nonAromaticAtoms, visited, seed+1, left, trackBondsMade, easy);
//Test connecting rings
if(success) {
List rings = atomToRings[a2];
if(rings.size()>1) {
for (int r : rings) {
if(r!=index && r>=0 && r " +success+" "+trackBondsMade.size());
if(!success) break;
}
}
}
}
if(success) {
//It works!!!
bondsMade.add(b);
bondsMade.addAll(trackBondsMade);
return true;
} else {
//Backtrack changes
visited[a] = visited[a2] = false;
mol.setBondOrder(b, 1);
for (int b2 : trackBondsMade) {
//System.out.println("retrack "+mol.getBondAtom(0, b2)+"-"+mol.getBondAtom(1, b2));
mol.setBondOrder(b2, 1);
visited[mol.getBondAtom(0, b2)] = visited[mol.getBondAtom(1, b2)] = false;
}
}
}
//Try to skip this atom
if(left>0 && (mol.getAtomicNo(a)!=6)) {
visited[a] = true;
List trackBondsMade = new ArrayList();
boolean success = aromatize(mol, atomToRings, ringSet, index, aromatic, nonAromaticAtoms, visited, seed+1, left-1, trackBondsMade, easy);
if(success) {
bondsMade.addAll(trackBondsMade);
return true;
} else {
visited[a] = false;
for (int b2 : trackBondsMade) {
mol.setBondOrder(b2, 1);
visited[mol.getBondAtom(0, b2)] = visited[mol.getBondAtom(1, b2)] = false;
}
}
}
return false;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy