1 /* 2 * Copyright (c) 2014 European Bioinformatics Institute (EMBL-EBI) 3 * John May <jwmay@users.sf.net> 4 * 5 * Contact: cdk-devel@lists.sourceforge.net 6 * 7 * This program is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU Lesser General Public License as published by 9 * the Free Software Foundation; either version 2.1 of the License, or (at 10 * your option) any later version. All we ask is that proper credit is given 11 * for our work, which includes - but is not limited to - adding the above 12 * copyright notice to the beginning of your source code files, and to any 13 * copyright notice that you may distribute with programs based on this work. 14 * 15 * This program is distributed in the hope that it will be useful, but WITHOUT 16 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 17 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 18 * License for more details. 19 * 20 * You should have received a copy of the GNU Lesser General Public License 21 * along with this program; if not, write to the Free Software 22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 U 23 */ 24 25 package org.openscience.cdk.stereo; 26 27 import org.openscience.cdk.config.Elements; 28 import org.openscience.cdk.interfaces.IAtom; 29 import org.openscience.cdk.interfaces.IAtomContainer; 30 import org.openscience.cdk.interfaces.IBond; 31 import org.openscience.cdk.interfaces.IStereoElement; 32 import org.openscience.cdk.interfaces.ITetrahedralChirality; 33 import org.openscience.cdk.ringsearch.RingSearch; 34 35 import javax.vecmath.Point2d; 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 43 import static org.openscience.cdk.config.Elements.Carbon; 44 import static org.openscience.cdk.graph.GraphUtil.EdgeToBondMap; 45 import static org.openscience.cdk.interfaces.ITetrahedralChirality.Stereo.ANTI_CLOCKWISE; 46 import static org.openscience.cdk.stereo.Stereocenters.Type.Tetracoordinate; 47 48 /** 49 * Recognize the configuration of tetrahedral stereocenters depicted as 50 * Fischer projection. Fischer projection is a convenient means of depicting 51 * 3D geometry commonly used in depicting carbohydrates. 52 * 53 * Fischer projection depicts tetrahedral stereocenters as though they were 54 * coplanar with the four substituents at cardinal directions (up,right,down, 55 * and left). The horizontal bonds (right and left) are interpreted as pointing 56 * out of the plane towards the viewer; They are not depicted with non-planar 57 * wedge bonds. 58 * 59 * This class provides the recognition of Fischer projections. Each asymmetric 60 * carbon is checked as to whether it's 2D depiction is coplanar with cardinal 61 * directions. All of these bonds must be planar (i.e. not wedge or hatch) and 62 * sigma bonds. In a hydrogen suppressed representation, one of the left or 63 * right bonds (to the implied hydrogen) may be omitted but can be correctly 64 * interpreted. 65 * 66 * @author John May 67 * @cdk.githash 68 * @see <a href="http://en.wikipedia.org/wiki/Fischer_projection">Fischer 69 * projection (Wikipedia)</a> 70 */ 71 final class FischerRecognition { 72 73 /** 74 * The threshold at which to snap bonds to the cardinal direction. The 75 * threshold allows bonds slightly of absolute directions to be interpreted. 76 * The tested vector is of unit length and so the threshold is simply the 77 * angle (in radians). 78 */ 79 public static final double CARDINALITY_THRESHOLD = Math.toRadians(5); 80 81 /** Cardinal direction, North index. */ 82 public static final int NORTH = 0; 83 84 /** Cardinal direction, East index. */ 85 public static final int EAST = 1; 86 87 /** Cardinal direction, South index. */ 88 public static final int SOUTH = 2; 89 90 /** Cardinal direction, West index. */ 91 public static final int WEST = 3; 92 93 private final IAtomContainer container; 94 private final int[][] graph; 95 private final EdgeToBondMap bonds; 96 private final Stereocenters stereocenters; 97 98 /** 99 * Required information to recognise stereochemistry. 100 * 101 * @param container input structure 102 * @param graph adjacency list representation 103 * @param bonds edge to bond index 104 * @param stereocenters location and type of asymmetries 105 */ FischerRecognition(IAtomContainer container, int[][] graph, EdgeToBondMap bonds, Stereocenters stereocenters)106 FischerRecognition(IAtomContainer container, 107 int[][] graph, 108 EdgeToBondMap bonds, 109 Stereocenters stereocenters) { 110 this.container = container; 111 this.graph = graph; 112 this.bonds = bonds; 113 this.stereocenters = stereocenters; 114 } 115 116 117 118 /** 119 * Recognise the tetrahedral stereochemistry in the provided structure. 120 * 121 * @param projections allowed projection types 122 * @return zero of more stereo elements 123 */ recognise(Set<Projection> projections)124 List<IStereoElement> recognise(Set<Projection> projections) { 125 126 if (!projections.contains(Projection.Fischer)) 127 return Collections.emptyList(); 128 129 // build atom index and only recognize 2D depictions 130 Map<IAtom,Integer> atomToIndex = new HashMap<IAtom, Integer>(); 131 for (IAtom atom : container.atoms()) { 132 if (atom.getPoint2d() == null) 133 return Collections.emptyList(); 134 atomToIndex.put(atom, atomToIndex.size()); 135 } 136 137 RingSearch ringSearch = new RingSearch(container, graph); 138 139 final List<IStereoElement> elements = new ArrayList<IStereoElement>(5); 140 141 for (int v = 0; v < container.getAtomCount(); v++) { 142 143 IAtom focus = container.getAtom(v); 144 Elements elem = Elements.ofNumber(focus.getAtomicNumber()); 145 146 if (elem != Carbon) 147 continue; 148 if (ringSearch.cyclic(v)) 149 continue; 150 if (stereocenters.elementType(v) != Tetracoordinate) 151 continue; 152 if (!stereocenters.isStereocenter(v)) 153 continue; 154 155 ITetrahedralChirality element = newTetrahedralCenter(focus, 156 neighbors(v, graph, bonds)); 157 158 if (element == null) 159 continue; 160 161 // east/west bonds must be to terminal atoms 162 IAtom east = element.getLigands()[EAST]; 163 IAtom west = element.getLigands()[WEST]; 164 165 if (!east.equals(focus) && !isTerminal(east, atomToIndex)) 166 continue; 167 if (!west.equals(focus) && !isTerminal(west, atomToIndex)) 168 continue; 169 170 elements.add(element); 171 } 172 173 return elements; 174 } 175 176 177 /** 178 * Create a new tetrahedral stereocenter of the given focus and neighboring 179 * bonds. This is an internal method and is presumed the atom can support 180 * tetrahedral stereochemistry and it has three or four explicit neighbors. 181 * 182 * The stereo element is only created if the local arrangement looks like 183 * a Fischer projection. 184 * 185 * @param focus central atom 186 * @param bonds adjacent bonds 187 * @return a stereo element, or null if one could not be created 188 */ newTetrahedralCenter(IAtom focus, IBond[] bonds)189 static ITetrahedralChirality newTetrahedralCenter(IAtom focus, IBond[] bonds) { 190 191 // obtain the bonds of a centre arranged by cardinal direction 192 IBond[] cardinalBonds = cardinalBonds(focus, bonds); 193 194 if (cardinalBonds == null) 195 return null; 196 197 // vertical bonds must be present and be sigma and planar (no wedge/hatch) 198 if (!isPlanarSigmaBond(cardinalBonds[NORTH]) || !isPlanarSigmaBond(cardinalBonds[SOUTH])) 199 return null; 200 201 // one of the horizontal bonds can be missing but not both 202 if (cardinalBonds[EAST] == null && cardinalBonds[WEST] == null) 203 return null; 204 205 // the neighbors of our tetrahedral centre, the EAST or WEST may 206 // be missing so we initialise these with the implicit (focus) 207 IAtom[] neighbors = new IAtom[]{cardinalBonds[NORTH].getOther(focus), 208 focus, 209 cardinalBonds[SOUTH].getOther(focus), 210 focus}; 211 212 213 // fill in the EAST/WEST bonds, if they are define, single and planar we add the 214 // connected atom. else if bond is defined (but not single or planar) or we 215 // have 4 neighbours something is wrong and we skip this atom 216 if (isPlanarSigmaBond(cardinalBonds[EAST])) { 217 neighbors[EAST] = cardinalBonds[EAST].getOther(focus); 218 } 219 else if (cardinalBonds[EAST] != null || bonds.length == 4) { 220 return null; 221 } 222 223 if (isPlanarSigmaBond(cardinalBonds[WEST])) { 224 neighbors[WEST] = cardinalBonds[WEST].getOther(focus); 225 } 226 else if (cardinalBonds[WEST] != null || bonds.length == 4) { 227 return null; 228 } 229 230 return new TetrahedralChirality(focus, neighbors, ANTI_CLOCKWISE); 231 } 232 233 /** 234 * Arrange the bonds adjacent to an atom (focus) in cardinal direction. The 235 * cardinal directions are that of a compass. Bonds are checked as to 236 * whether they are horizontal or vertical within a predefined threshold. 237 * 238 * @param focus an atom 239 * @param bonds bonds adjacent to the atom 240 * @return array of bonds organised (N,E,S,W), or null if a bond was found 241 * that exceeded the threshold 242 */ cardinalBonds(IAtom focus, IBond[] bonds)243 static IBond[] cardinalBonds(IAtom focus, IBond[] bonds) { 244 245 final Point2d centerXy = focus.getPoint2d(); 246 final IBond[] cardinal = new IBond[4]; 247 248 for (final IBond bond : bonds) { 249 250 IAtom other = bond.getOther(focus); 251 Point2d otherXy = other.getPoint2d(); 252 253 double deltaX = otherXy.x - centerXy.x; 254 double deltaY = otherXy.y - centerXy.y; 255 256 // normalise vector length so thresholds are independent 257 double mag = Math.sqrt(deltaX * deltaX + deltaY * deltaY); 258 deltaX /= mag; 259 deltaY /= mag; 260 261 double absDeltaX = Math.abs(deltaX); 262 double absDeltaY = Math.abs(deltaY); 263 264 // assign the bond to the cardinal direction 265 if (absDeltaX < CARDINALITY_THRESHOLD 266 && absDeltaY > CARDINALITY_THRESHOLD) { 267 cardinal[deltaY > 0 ? NORTH : SOUTH] = bond; 268 } 269 else if (absDeltaX > CARDINALITY_THRESHOLD 270 && absDeltaY < CARDINALITY_THRESHOLD) { 271 cardinal[deltaX > 0 ? EAST : WEST] = bond; 272 } 273 else { 274 return null; 275 } 276 } 277 278 return cardinal; 279 } 280 281 /** 282 * Is the atom terminal having only one connection. 283 * 284 * @param atom an atom 285 * @param atomToIndex a map of atoms to index 286 * @return the atom is terminal 287 */ isTerminal(IAtom atom, Map<IAtom, Integer> atomToIndex)288 private boolean isTerminal(IAtom atom, Map<IAtom, Integer> atomToIndex) { 289 return graph[atomToIndex.get(atom)].length == 1; 290 } 291 292 /** 293 * Helper method determines if a bond is defined (not null) and whether 294 * it is a sigma (single) bond with no stereo attribute (wedge/hatch). 295 * 296 * @param bond the bond to test 297 * @return the bond is a planar sigma bond 298 */ isPlanarSigmaBond(IBond bond)299 private static boolean isPlanarSigmaBond(IBond bond) { 300 return bond != null && 301 IBond.Order.SINGLE.equals(bond.getOrder()) && 302 IBond.Stereo.NONE.equals(bond.getStereo()); 303 } 304 305 /** 306 * Helper method to obtain the neighbouring bonds from an adjacency list 307 * graph and edge->bond map. 308 * 309 * @param v vertex 310 * @param g graph (adj list) 311 * @param bondMap map of edges to bonds 312 * @return neighboring bonds 313 */ neighbors(int v, int[][] g, EdgeToBondMap bondMap)314 private static IBond[] neighbors(int v, int[][] g, EdgeToBondMap bondMap) { 315 int[] ws = g[v]; 316 IBond[] bonds = new IBond[ws.length]; 317 for (int i = 0; i < ws.length; i++) { 318 bonds[i] = bondMap.get(v, ws[i]); 319 } 320 return bonds; 321 } 322 } 323