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