File Coverage

blib/lib/Chemistry/OpenSMILES/Stereo.pm
Criterion Covered Total %
statement 182 210 86.6
branch 62 102 60.7
condition 22 57 38.6
subroutine 16 19 84.2
pod 0 6 0.0
total 282 394 71.5


line stmt bran cond sub pod time code
1             package Chemistry::OpenSMILES::Stereo;
2              
3 1     1   661 use strict;
  1         3  
  1         32  
4 1     1   6 use warnings;
  1         2  
  1         76  
5              
6             # ABSTRACT: Stereochemistry handling routines
7             our $VERSION = '0.8.4'; # VERSION
8              
9             require Exporter;
10             our @ISA = qw( Exporter );
11             our @EXPORT_OK = qw(
12             chirality_to_pseudograph
13             cis_trans_to_pseudoedges
14             mark_all_double_bonds
15             mark_cis_trans
16             );
17              
18 1         76 use Chemistry::OpenSMILES qw(
19             is_cis_trans_bond
20             is_double_bond
21             is_ring_bond
22             is_single_bond
23             toggle_cistrans
24 1     1   12 );
  1         2  
25 1     1   472 use Chemistry::OpenSMILES::Writer qw( write_SMILES );
  1         3  
  1         63  
26 1     1   9 use Graph::Traversal::BFS;
  1         2  
  1         20  
27 1     1   8 use Graph::Undirected;
  1         1  
  1         22  
28 1     1   10 use List::Util qw( all any max min sum sum0 );
  1         2  
  1         2453  
29              
30             sub mark_all_double_bonds
31             {
32 2     2 0 793 my( $graph, $setting_sub, $order_sub ) = @_;
33              
34             # By default, whenever there is a choice between atoms, the one with
35             # lowest position in the input SMILES is chosen:
36 2 50   6   11 $order_sub = sub { return $_[0]->{number} } unless $order_sub;
  6         27  
37              
38             # Select non-ring double bonds
39 2 100 66     19 my @double_bonds = grep { is_double_bond( $graph, @$_ ) &&
  15         2189  
40             !is_ring_bond( $graph, @$_ ) &&
41             !is_unimportant_double_bond( $graph, @$_ ) }
42             $graph->edges;
43              
44             # Construct a double bond incidence graph. Vertices are double bonds
45             # and edges are between those double bonds that separated by a single
46             # single ('-') bond. Interestingly, incidence graph for SMILES C=C(C)=C
47             # is connected, but for C=C=C not. This is because allenal systems
48             # cannot be represented yet.
49 2         392 my $bond_graph = Graph::Undirected->new;
50 2         404 my %incident_double_bonds;
51 2         6 for my $bond (@double_bonds) {
52 1         9 $bond_graph->add_vertex( join '', sort @$bond );
53 1         41 push @{$incident_double_bonds{$bond->[0]}}, $bond;
  1         4  
54 1         2 push @{$incident_double_bonds{$bond->[1]}}, $bond;
  1         7  
55             }
56 2         7 for my $bond ($graph->edges) {
57 15 100       875 next unless is_single_bond( $graph, @$bond );
58 13         2538 my @adjacent_bonds;
59 13 100       35 if( $incident_double_bonds{$bond->[0]} ) {
60             push @adjacent_bonds,
61 3         5 @{$incident_double_bonds{$bond->[0]}};
  3         7  
62             }
63 13 100       31 if( $incident_double_bonds{$bond->[1]} ) {
64             push @adjacent_bonds,
65 1         3 @{$incident_double_bonds{$bond->[1]}};
  1         3  
66             }
67 13         31 for my $bond1 (@adjacent_bonds) {
68 4         15 for my $bond2 (@adjacent_bonds) {
69 4 50       14 next if $bond1 == $bond2;
70 0         0 $bond_graph->add_edge( join( '', sort @$bond1 ),
71             join( '', sort @$bond2 ) );
72             }
73             }
74             }
75              
76             # In principle, bond graph could be splitted into separate components
77             # to reduce the number of cycles needed by Morgan algorithm, but I do
78             # not think there is a failure case because of keeping them together.
79              
80             # Set up initial invariants
81 2         7 my %invariants;
82 2         9 for ($bond_graph->vertices) {
83 1         19 $invariants{$_} = $bond_graph->degree( $_ );
84             }
85 2         178 my %distinct_invariants = map { $_ => 1 } values %invariants;
  1         4  
86              
87             # Perform Morgan algorithm
88 2         4 while( 1 ) {
89 2         4 my %invariants_now;
90 2         5 for ($bond_graph->vertices) {
91 1         20 $invariants_now{$_} = sum0 map { $invariants{$_} }
  0         0  
92             $bond_graph->neighbours( $_ );
93             }
94              
95 2         81 my %distinct_invariants_now = map { $_ => 1 } values %invariants_now;
  1         4  
96 2 50       10 last if %distinct_invariants_now <= %distinct_invariants;
97              
98 0         0 %invariants = %invariants_now;
99 0         0 %distinct_invariants = %distinct_invariants_now;
100             }
101              
102             # Establish a deterministic order favouring bonds with higher invariants.
103             # If invariants are equal, order bonds by their atom numbers.
104 2         18 @double_bonds = sort { $invariants{join '', sort @$b} <=>
105             $invariants{join '', sort @$a} ||
106 0         0 (min map { $order_sub->($_) } @$a) <=>
107 0         0 (min map { $order_sub->($_) } @$b) ||
108 0         0 (max map { $order_sub->($_) } @$a) <=>
109 0 0 0     0 (max map { $order_sub->($_) } @$b) } @double_bonds;
  0         0  
110              
111 2         22 for (@double_bonds) {
112 1         8 mark_cis_trans( $graph, @$_, $setting_sub, $order_sub );
113             }
114             }
115              
116             # Requires double bonds in input. Does not check whether a bond belongs
117             # to a ring or not.
118             sub mark_cis_trans
119             {
120 1     1 0 4 my( $graph, $atom2, $atom3, $setting_sub, $order_sub ) = @_;
121              
122             # By default, whenever there is a choice between atoms, the one with
123             # lowest position in the input SMILES is chosen:
124 1 50   0   4 $order_sub = sub { return $_[0]->{number} } unless $order_sub;
  0         0  
125              
126 1         4 my @neighbours2 = $graph->neighbours( $atom2 );
127 1         115 my @neighbours3 = $graph->neighbours( $atom3 );
128 1 50 33     103 return if @neighbours2 < 2 || @neighbours3 < 2;
129              
130             # TODO: Currently we are choosing either a pair of
131             # neighbouring atoms which have no cis/trans markers or
132             # a pair of which a single atom has a cis/trans marker.
133             # The latter case allows to accommodate adjacent double
134             # bonds. However, there may be a situation where both
135             # atoms already have cis/trans markers, but could still
136             # be reconciled.
137              
138             my @cistrans_bonds2 =
139 1         4 grep { is_cis_trans_bond( $graph, $atom2, $_ ) } @neighbours2;
  3         577  
140             my @cistrans_bonds3 =
141 1         191 grep { is_cis_trans_bond( $graph, $atom3, $_ ) } @neighbours3;
  3         566  
142              
143 1 50       211 if( @cistrans_bonds2 + @cistrans_bonds3 > 1 ) {
144             warn 'cannot represent cis/trans bond between atoms ' .
145 0         0 join( ' and ', sort map { $_->{number} } $atom2, $atom3 ) .
  0         0  
146             ' as there are other cis/trans bonds nearby' . "\n";
147 0         0 return;
148             }
149              
150 1 0 33     11 if( (@neighbours2 == 2 && !@cistrans_bonds2 &&
      33        
      33        
      33        
      33        
151 0     0   0 !any { is_single_bond( $graph, $atom2, $_ ) } @neighbours2) ||
152             (@neighbours3 == 2 && !@cistrans_bonds3 &&
153 0     0   0 !any { is_single_bond( $graph, $atom3, $_ ) } @neighbours3) ) {
154             # Azide group (N=N#N) or conjugated allene-like systems (=C=)
155             warn 'atoms ' .
156 0         0 join( ' and ', sort map { $_->{number} } $atom2, $atom3 ) .
  0         0  
157             ' are part of conjugated double/triple bond system, thus ' .
158             'cis/trans setting of their bond is impossible to represent ' .
159             '(not supported yet)' . "\n";
160 0         0 return;
161             }
162              
163             # Making the $atom2 be the one which has a defined cis/trans bond.
164             # Also, a deterministic ordering of atoms in bond is achieved here.
165 1 50 33     9 if( @cistrans_bonds3 ||
      33        
166             (!@cistrans_bonds2 && $order_sub->($atom2) > $order_sub->($atom3)) ) {
167 0         0 ( $atom2, $atom3 ) = ( $atom3, $atom2 );
168 0         0 @neighbours2 = $graph->neighbours( $atom2 );
169 0         0 @neighbours3 = $graph->neighbours( $atom3 );
170              
171 0         0 @cistrans_bonds2 = @cistrans_bonds3;
172 0         0 @cistrans_bonds3 = ();
173             }
174              
175             # Establishing the canonical order
176 1         197 @neighbours2 = sort { $order_sub->($a) <=> $order_sub->($b) }
177 1         6 grep { is_single_bond( $graph, $atom2, $_ ) } @neighbours2;
  3         633  
178 1         195 @neighbours3 = sort { $order_sub->($a) <=> $order_sub->($b) }
179 1         4 grep { is_single_bond( $graph, $atom3, $_ ) } @neighbours3;
  3         572  
180              
181             # Check if there is a chance to have anything marked
182 1         2 my $bond_will_be_marked;
183 1         10 for my $atom1 (@cistrans_bonds2, @neighbours2) {
184 2         5 for my $atom4 (@neighbours3) {
185 2         7 my $setting = $setting_sub->( $atom1, $atom2, $atom3, $atom4 );
186 2 50       960 if( $setting ) {
187 2         5 $bond_will_be_marked = 1;
188 2         10 last;
189             }
190             }
191             }
192              
193 1 50       4 if( !$bond_will_be_marked ) {
194             warn 'cannot represent cis/trans bond between atoms ' .
195 0         0 join( ' and ', sort map { $_->{number} } $atom2, $atom3 ) .
  0         0  
196             ' as there are no eligible single bonds nearby' . "\n";
197 0         0 return;
198             }
199              
200             # If there is an atom with cis/trans bond, then this is this one
201 1 50       5 my( $first_atom ) = @cistrans_bonds2 ? @cistrans_bonds2 : @neighbours2;
202 1 50       3 if( !@cistrans_bonds2 ) {
203 1         4 $graph->set_edge_attribute( $first_atom, $atom2, 'bond', '/' );
204             }
205              
206 1         241 my $atom4_marked;
207 1         3 for my $atom4 (@neighbours3) {
208 2         4 my $atom1 = $first_atom;
209 2         6 my $setting = $setting_sub->( $atom1, $atom2, $atom3, $atom4 );
210 2 50       940 next unless $setting;
211 2         9 my $other = $graph->get_edge_attribute( $atom1, $atom2, 'bond' );
212 2 100       384 $other = toggle_cistrans $other if $setting eq 'cis';
213 2 50       10 $other = toggle_cistrans $other if $atom1->{number} > $atom2->{number};
214 2 50       7 $other = toggle_cistrans $other if $atom4->{number} < $atom3->{number};
215 2         8 $graph->set_edge_attribute( $atom3, $atom4, 'bond', $other );
216 2 100       485 $atom4_marked = $atom4 unless $atom4_marked;
217             }
218              
219 1         6 for my $atom1 (@neighbours2) {
220 2 100       9 next if $atom1 eq $first_atom; # Marked already
221 1         3 my $atom4 = $atom4_marked;
222 1         4 my $setting = $setting_sub->( $atom1, $atom2, $atom3, $atom4 );
223 1 50       464 next unless $setting;
224 1         32 my $other = $graph->get_edge_attribute( $atom3, $atom4, 'bond' );
225 1 50       253 $other = toggle_cistrans $other if $setting eq 'cis';
226 1 50       8 $other = toggle_cistrans $other if $atom1->{number} > $atom2->{number};
227 1 50       11 $other = toggle_cistrans $other if $atom4->{number} < $atom3->{number};
228 1         46 $graph->set_edge_attribute( $atom1, $atom2, 'bond', $other );
229             }
230             }
231              
232             # Store the tetrahedral chirality character as additional pseudo vertices
233             # and edges.
234             sub chirality_to_pseudograph
235             {
236 1     1 0 6 my( $moiety ) = @_;
237              
238 1         4 for my $atom ($moiety->vertices) {
239 11 100       49 next unless Chemistry::OpenSMILES::is_chiral_tetrahedral( $atom );
240 1         16 next unless @{$atom->{chirality_neighbours}} >= 3 &&
241 1 50 33     2 @{$atom->{chirality_neighbours}} <= 4;
  1         7  
242              
243 1         3 my @chirality_neighbours = @{$atom->{chirality_neighbours}};
  1         4  
244 1 50       12 if( @chirality_neighbours == 3 ) {
245 0         0 @chirality_neighbours = ( $chirality_neighbours[0],
246             {}, # marking the lone pair
247             @chirality_neighbours[1..2] );
248             }
249 1 50       6 if( $atom->{chirality} eq '@' ) {
250             # Reverse the order if counter-clockwise
251 1         5 @chirality_neighbours = ( $chirality_neighbours[0],
252             reverse @chirality_neighbours[1..3] );
253             }
254              
255 1         4 for my $i (0..3) {
256 4         8 my $neighbour = $chirality_neighbours[$i];
257 4         10 my @chirality_neighbours_now = @chirality_neighbours;
258            
259 4 100       14 if( $i % 2 ) {
260             # Reverse the order due to projected atom change
261 2         6 @chirality_neighbours_now = ( $chirality_neighbours_now[0],
262             reverse @chirality_neighbours_now[1..3] );
263             }
264              
265 4         9 my @other = grep { $_ != $neighbour } @chirality_neighbours_now;
  16         38  
266 4         9 for my $offset (0..2) {
267 12         23 my $connector = {};
268 12         42 $moiety->set_edge_attribute( $neighbour, $connector, 'chiral', 'from' );
269 12         4817 $moiety->set_edge_attribute( $atom, $connector, 'chiral', 'to' );
270              
271 12         4274 $moiety->set_edge_attribute( $connector, $other[0], 'chiral', 1 );
272 12         4310 $moiety->set_edge_attribute( $connector, $other[1], 'chiral', 2 );
273 12         4321 $moiety->set_edge_attribute( $connector, $other[2], 'chiral', 3 );
274              
275 12         4364 push @other, shift @other;
276             }
277             }
278             }
279             }
280              
281             sub cis_trans_to_pseudoedges
282             {
283 2     2 0 9837 my( $moiety ) = @_;
284              
285             # Select non-ring double bonds
286             my @double_bonds =
287 2 100 66     7 grep { is_double_bond( $moiety, @$_ ) &&
  15         3089  
288             !is_ring_bond( $moiety, @$_ ) &&
289             !is_unimportant_double_bond( $moiety, @$_ ) } $moiety->edges;
290              
291             # Connect cis/trans atoms in double bonds with pseudo-edges
292 2         571 for my $bond (@double_bonds) {
293 1         4 my( $atom2, $atom3 ) = @$bond;
294 1         4 my @atom2_neighbours = grep { !is_pseudoedge( $moiety, $atom2, $_ ) }
  3         511  
295             $moiety->neighbours( $atom2 );
296 1         194 my @atom3_neighbours = grep { !is_pseudoedge( $moiety, $atom3, $_ ) }
  3         487  
297             $moiety->neighbours( $atom3 );
298 1 50 33     199 next if @atom2_neighbours < 2 || @atom2_neighbours > 3 ||
      33        
      33        
299             @atom3_neighbours < 2 || @atom3_neighbours > 3;
300              
301 1         3 my( $atom1 ) = grep { is_cis_trans_bond( $moiety, $atom2, $_ ) }
  3         593  
302             @atom2_neighbours;
303 1         380 my( $atom4 ) = grep { is_cis_trans_bond( $moiety, $atom3, $_ ) }
  3         638  
304             @atom3_neighbours;
305 1 50 33     383 next unless $atom1 && $atom4;
306              
307 1 100       3 my( $atom1_para ) = grep { $_ != $atom1 && $_ != $atom3 } @atom2_neighbours;
  3         13  
308 1 100       3 my( $atom4_para ) = grep { $_ != $atom4 && $_ != $atom2 } @atom3_neighbours;
  3         11  
309              
310 1         4 my $is_cis = $moiety->get_edge_attribute( $atom1, $atom2, 'bond' ) ne
311             $moiety->get_edge_attribute( $atom3, $atom4, 'bond' );
312              
313 1 50       392 $is_cis = !$is_cis if $atom1->{number} > $atom2->{number};
314 1 50       7 $is_cis = !$is_cis if $atom3->{number} > $atom4->{number};
315              
316 1 50       8 $moiety->set_edge_attribute( $atom1, $atom4, 'pseudo',
317             $is_cis ? 'cis' : 'trans' );
318 1 50       374 if( $atom1_para ) {
319 1 50       6 $moiety->set_edge_attribute( $atom1_para, $atom4, 'pseudo',
320             $is_cis ? 'trans' : 'cis' );
321             }
322 1 50       363 if( $atom4_para ) {
323 1 50       23 $moiety->set_edge_attribute( $atom1, $atom4_para, 'pseudo',
324             $is_cis ? 'trans' : 'cis' );
325             }
326 1 50 33     387 if( $atom1_para && $atom4_para ) {
327 1 50       7 $moiety->set_edge_attribute( $atom1_para, $atom4_para, 'pseudo',
328             $is_cis ? 'cis' : 'trans' );
329             }
330             }
331              
332             # Unset cis/trans bond markers during second pass
333 2         368 for my $bond ($moiety->edges) {
334 19 100       3899 next unless is_cis_trans_bond( $moiety, @$bond );
335 5         1985 $moiety->delete_edge_attribute( @$bond, 'bond' );
336             }
337             }
338              
339             sub is_pseudoedge
340             {
341 6     6 0 13 my( $moiety, $a, $b ) = @_;
342 6         15 return $moiety->has_edge_attribute( $a, $b, 'pseudo' );
343             }
344              
345             # An "unimportant" double bond is one which has leaf atoms on one of its
346             # sides and both of these atoms are identical.
347             sub is_unimportant_double_bond
348             {
349 4     4 0 13 my( $moiety, $a, $b ) = @_;
350 4         16 my @a_neighbours = grep { $_ != $b } $moiety->neighbours( $a );
  10         440  
351 4         18 my @b_neighbours = grep { $_ != $a } $moiety->neighbours( $b );
  12         401  
352              
353 4 50 66     26 if( @a_neighbours == 2 &&
354 4     4   764 all { $moiety->degree( $_ ) == 1 } @a_neighbours ) {
355 0 0       0 return 1 if write_SMILES( $a_neighbours[0] ) eq
356             write_SMILES( $a_neighbours[1] );
357             }
358              
359 4 100 66     1226 if( @b_neighbours == 2 &&
360 8     8   1453 all { $moiety->degree( $_ ) == 1 } @b_neighbours ) {
361 2 50       684 return 1 if write_SMILES( $b_neighbours[0] ) eq
362             write_SMILES( $b_neighbours[1] );
363             }
364              
365 2         1216 return;
366             }
367              
368             1;