File Coverage

blib/lib/Chemistry/OpenSMILES/Stereo.pm
Criterion Covered Total %
statement 202 308 65.5
branch 70 144 48.6
condition 20 60 33.3
subroutine 21 27 77.7
pod 0 6 0.0
total 313 545 57.4


line stmt bran cond sub pod time code
1             package Chemistry::OpenSMILES::Stereo;
2              
3             # ABSTRACT: Stereochemistry handling routines
4             our $VERSION = '0.12.3'; # VERSION
5              
6 7     7   5820 use strict;
  7         19  
  7         304  
7 7     7   41 use warnings;
  7         15  
  7         624  
8              
9 7         919 use Chemistry::OpenSMILES qw(
10             is_chiral
11             is_chiral_octahedral
12             is_chiral_planar
13             is_chiral_tetrahedral
14             is_chiral_trigonal_bipyramidal
15             is_cis_trans_bond
16             is_double_bond
17             is_ring_bond
18             is_single_bond
19             toggle_cistrans
20 7     7   71 );
  7         15  
21 7     7   52 use Chemistry::OpenSMILES::Stereo::Tables qw( @OH @TB );
  7         16  
  7         1154  
22 7     7   4505 use Chemistry::OpenSMILES::Writer qw( write_SMILES );
  7         36  
  7         796  
23 7     7   57 use Graph::Traversal::BFS;
  7         19  
  7         226  
24 7     7   41 use Graph::Undirected;
  7         16  
  7         271  
25 7     7   39 use List::Util qw( all any first max min sum sum0 uniq );
  7         15  
  7         749  
26 7     7   48 use Set::Object qw( set );
  7         16  
  7         37903  
27              
28             require Exporter;
29             our @ISA = qw( Exporter );
30             our @EXPORT_OK = qw(
31             chirality_to_pseudograph
32             cis_trans_to_pseudoedges
33             mark_all_double_bonds
34             mark_cis_trans
35             );
36              
37             sub mark_all_double_bonds
38             {
39 3     3 0 1492 my( $graph, $setting_sub, $order_sub, $color_sub ) = @_;
40              
41 3         13 my @double_bonds = grep { is_double_bond( $graph, @$_ ) } $graph->edges;
  22         6701  
42 3 100       852 if( ref $setting_sub eq 'ARRAY' ) {
43             # List of double bonds with their setting are given
44 1         6 @double_bonds = map { [ @{$_}[1..2] ] } @$setting_sub;
  0         0  
  0         0  
45 1         5 my %cis = map { ( join( '', sort @{$_}[1..2] ) => { atoms => set( $_->[0], $_->[3] ), setting => $_->[4] } ) }
  0         0  
  0         0  
46             @$setting_sub;
47             $setting_sub = sub {
48 0     0   0 my $key = join '', sort @_[1..2];
49 0 0       0 return undef unless exists $cis{$key};
50              
51 0         0 my $setting = $cis{$key}->{setting};
52 0 0       0 return $setting unless ($cis{$key}->{atoms} * set( $_[0], $_[3] ))->size == 1;
53 0 0       0 return $setting eq 'cis' ? 'trans' : 'cis';
54 1         8 };
55             }
56              
57             # By default, whenever there is a choice between atoms, the one with
58             # lowest position in the input SMILES is chosen:
59 3 50   6   21 $order_sub = sub { $_[0]->{number} } unless $order_sub;
  6         18  
60              
61             # Select non-ring double bonds
62 3   66     9 @double_bonds = grep { !is_ring_bond( $graph, @$_ ) &&
  2         8  
63             !is_unimportant_double_bond( $graph, @$_, $color_sub ) }
64             @double_bonds;
65              
66 3 100       293 return unless @double_bonds;
67              
68             # Construct a double bond incidence graph. Vertices are double bonds
69             # and edges are between those double bonds that separated by a single
70             # single ('-') bond. Interestingly, incidence graph for SMILES C=C(C)=C
71             # is connected, but for C=C=C not. This is because allenal systems
72             # cannot be represented yet.
73 1         5 my $bond_graph = Graph::Undirected->new;
74 1         157 my %incident_double_bonds;
75 1         2 for my $bond (@double_bonds) {
76 1         7 $bond_graph->add_vertex( join '', sort @$bond );
77 1         35 push @{$incident_double_bonds{$bond->[0]}}, $bond;
  1         4  
78 1         2 push @{$incident_double_bonds{$bond->[1]}}, $bond;
  1         3  
79             }
80 1         3 for my $bond ($graph->edges) {
81 11 100       416 next unless is_single_bond( $graph, @$bond );
82 10         1604 my @adjacent_bonds;
83 10 100       22 if( $incident_double_bonds{$bond->[0]} ) {
84             push @adjacent_bonds,
85 3         5 @{$incident_double_bonds{$bond->[0]}};
  3         4  
86             }
87 10 100       18 if( $incident_double_bonds{$bond->[1]} ) {
88             push @adjacent_bonds,
89 1         2 @{$incident_double_bonds{$bond->[1]}};
  1         14  
90             }
91 10         15 for my $bond1 (@adjacent_bonds) {
92 4         5 for my $bond2 (@adjacent_bonds) {
93 4 50       12 next if $bond1 == $bond2;
94 0         0 $bond_graph->add_edge( join( '', sort @$bond1 ),
95             join( '', sort @$bond2 ) );
96             }
97             }
98             }
99              
100             # In principle, bond graph could be splitted into separate components
101             # to reduce the number of cycles needed by Morgan algorithm, but I do
102             # not think there is a failure case because of keeping them together.
103              
104             # Set up initial invariants
105 1         4 my %invariants;
106 1         3 for ($bond_graph->vertices) {
107 1         16 $invariants{$_} = $bond_graph->degree( $_ );
108             }
109 1         145 my %distinct_invariants = map { $_ => 1 } values %invariants;
  1         3  
110              
111             # Perform Morgan algorithm
112 1         2 while( 1 ) {
113 1         2 my %invariants_now;
114 1         2 for ($bond_graph->vertices) {
115 1         13 $invariants_now{$_} = sum0 map { $invariants{$_} }
  0         0  
116             $bond_graph->neighbours( $_ );
117             }
118              
119 1         48 my %distinct_invariants_now = map { $_ => 1 } values %invariants_now;
  1         3  
120 1 50       4 last if %distinct_invariants_now <= %distinct_invariants;
121              
122 0         0 %invariants = %invariants_now;
123 0         0 %distinct_invariants = %distinct_invariants_now;
124             }
125              
126             # Establish a deterministic order favouring bonds with higher invariants.
127             # If invariants are equal, order bonds by their atom numbers.
128 1         3 @double_bonds = sort { $invariants{join '', sort @$b} <=>
129             $invariants{join '', sort @$a} ||
130 0         0 (min map { $order_sub->($_) } @$a) <=>
131 0         0 (min map { $order_sub->($_) } @$b) ||
132 0         0 (max map { $order_sub->($_) } @$a) <=>
133 0 0 0     0 (max map { $order_sub->($_) } @$b) } @double_bonds;
  0         0  
134              
135 1         2 for (@double_bonds) {
136 1         4 mark_cis_trans( $graph, @$_, $setting_sub, $order_sub );
137             }
138             }
139              
140             # Requires double bonds in input. Does not check whether a bond belongs
141             # to a ring or not.
142             sub mark_cis_trans
143             {
144 1     1 0 3 my( $graph, $atom2, $atom3, $setting_sub, $order_sub ) = @_;
145              
146             # By default, whenever there is a choice between atoms, the one with
147             # lowest position in the input SMILES is chosen:
148 1 50   0   2 $order_sub = sub { $_[0]->{number} } unless $order_sub;
  0         0  
149              
150 1         15 my @neighbours2 = $graph->neighbours( $atom2 );
151 1         93 my @neighbours3 = $graph->neighbours( $atom3 );
152 1 50 33     67 return if @neighbours2 < 2 || @neighbours3 < 2;
153              
154             # TODO: Currently we are choosing either a pair of
155             # neighbouring atoms which have no cis/trans markers or
156             # a pair of which a single atom has a cis/trans marker.
157             # The latter case allows to accommodate adjacent double
158             # bonds. However, there may be a situation where both
159             # atoms already have cis/trans markers, but could still
160             # be reconciled.
161              
162             my @cistrans_bonds2 =
163 1         3 grep { is_cis_trans_bond( $graph, $atom2, $_ ) } @neighbours2;
  3         325  
164             my @cistrans_bonds3 =
165 1         340 grep { is_cis_trans_bond( $graph, $atom3, $_ ) } @neighbours3;
  3         479  
166              
167 1 50       147 if( @cistrans_bonds2 + @cistrans_bonds3 > 1 ) {
168             warn 'cannot represent cis/trans bond between atoms ' .
169 0         0 join( ' and ', sort { $a <=> $b } map { $_->{number} } $atom2, $atom3 ) .
  0         0  
  0         0  
170             ' as there are other cis/trans bonds nearby' . "\n";
171 0         0 return;
172             }
173              
174 1 0 33     30 if( (@neighbours2 == 2 && !@cistrans_bonds2 &&
      33        
      33        
      33        
      33        
175 0     0   0 !any { is_single_bond( $graph, $atom2, $_ ) } @neighbours2) ||
176             (@neighbours3 == 2 && !@cistrans_bonds3 &&
177 0     0   0 !any { is_single_bond( $graph, $atom3, $_ ) } @neighbours3) ) {
178             # Azide group (N=N#N) or conjugated allene-like systems (=C=)
179             warn 'atoms ' .
180 0         0 join( ' and ', sort { $a <=> $b } map { $_->{number} } $atom2, $atom3 ) .
  0         0  
  0         0  
181             ' are part of conjugated double/triple bond system, thus ' .
182             'cis/trans setting of their bond is impossible to represent ' .
183             '(not supported yet)' . "\n";
184 0         0 return;
185             }
186              
187             # Making the $atom2 be the one which has a defined cis/trans bond.
188             # Also, a deterministic ordering of atoms in bond is achieved here.
189 1 50 33     12 if( @cistrans_bonds3 ||
      33        
190             (!@cistrans_bonds2 && $order_sub->($atom2) > $order_sub->($atom3)) ) {
191 0         0 ( $atom2, $atom3 ) = ( $atom3, $atom2 );
192 0         0 @neighbours2 = $graph->neighbours( $atom2 );
193 0         0 @neighbours3 = $graph->neighbours( $atom3 );
194              
195 0         0 @cistrans_bonds2 = @cistrans_bonds3;
196 0         0 @cistrans_bonds3 = ();
197             }
198              
199             # Establishing the canonical order
200 1         315 @neighbours2 = sort { $order_sub->($a) <=> $order_sub->($b) }
201 1         2 grep { is_single_bond( $graph, $atom2, $_ ) } @neighbours2;
  3         440  
202 1         192 @neighbours3 = sort { $order_sub->($a) <=> $order_sub->($b) }
203 1         2 grep { is_single_bond( $graph, $atom3, $_ ) } @neighbours3;
  3         484  
204              
205             # Check if there is a chance to have anything marked
206 1         2 my $bond_will_be_marked;
207 1         4 for my $atom1 (@cistrans_bonds2, @neighbours2) {
208 2         3 for my $atom4 (@neighbours3) {
209 2         6 my $setting = $setting_sub->( $atom1, $atom2, $atom3, $atom4 );
210 2 50       786 if( $setting ) {
211 2         4 $bond_will_be_marked = 1;
212 2         5 last;
213             }
214             }
215             }
216              
217 1 50       4 if( !$bond_will_be_marked ) {
218             warn 'cannot represent cis/trans bond between atoms ' .
219 0         0 join( ' and ', sort { $a <=> $b } map { $_->{number} } $atom2, $atom3 ) .
  0         0  
  0         0  
220             ' as there are no eligible single bonds nearby' . "\n";
221 0         0 return;
222             }
223              
224             # If there is an atom with cis/trans bond, then this is this one.
225             # Adjustment to pre-order (neither the requested order, nor the post-order!) is needed to maintain relative settings in order.
226             # Otherwise nondeterminism may occur and result in different (albeit isomorphic) output SMILES like:
227             # C/C=C\CCCCC/C=C\C
228             # C/C=C\CCCCC\C=C/C
229 1 50       4 my( $first_atom ) = @cistrans_bonds2 ? @cistrans_bonds2 : @neighbours2;
230 1 50       18 if( !@cistrans_bonds2 ) {
231 1 50       30 $graph->set_edge_attribute( $first_atom, $atom2, 'bond', $first_atom->{number} < $atom2->{number} ? '/' : '\\' );
232             }
233              
234             # Adjustments to pre-order (neither the requested order, nor the post-order!) are done here.
235 1         196 my $atom4_marked;
236 1         2 for my $atom4 (@neighbours3) {
237 2         3 my $atom1 = $first_atom;
238 2         5 my $setting = $setting_sub->( $atom1, $atom2, $atom3, $atom4 );
239 2 50       785 next unless $setting;
240 2         22 my $other = $graph->get_edge_attribute( $atom1, $atom2, 'bond' );
241 2 100       321 $other = toggle_cistrans $other if $setting eq 'cis';
242 2 50       5 $other = toggle_cistrans $other if $atom1->{number} > $atom2->{number};
243 2 50       5 $other = toggle_cistrans $other if $atom3->{number} > $atom4->{number};
244 2         26 $graph->set_edge_attribute( $atom3, $atom4, 'bond', $other );
245 2 100       348 $atom4_marked = $atom4 unless $atom4_marked;
246             }
247              
248 1         2 for my $atom1 (@neighbours2) {
249 2 100       13 next if $atom1 eq $first_atom; # Marked already
250 1         2 my $atom4 = $atom4_marked;
251 1         4 my $setting = $setting_sub->( $atom1, $atom2, $atom3, $atom4 );
252 1 50       425 next unless $setting;
253 1         12 my $other = $graph->get_edge_attribute( $atom3, $atom4, 'bond' );
254 1 50       151 $other = toggle_cistrans $other if $setting eq 'cis';
255 1 50       5 $other = toggle_cistrans $other if $atom1->{number} > $atom2->{number};
256 1 50       3 $other = toggle_cistrans $other if $atom3->{number} > $atom4->{number};
257 1         13 $graph->set_edge_attribute( $atom1, $atom2, 'bond', $other );
258             }
259             }
260              
261             # Store chirality character as additional pseudo vertices and edges.
262             sub chirality_to_pseudograph
263             {
264 1     1 0 7 my( $moiety ) = @_;
265              
266 1         3 for my $atom ($moiety->vertices) {
267 11 100       45 next unless is_chiral $atom;
268              
269 1 50       4 next unless exists $atom->{chirality_neighbours};
270 1         1 my @chirality_neighbours = @{$atom->{chirality_neighbours}};
  1         3  
271              
272 1         1 my $has_lone_pair;
273 1 50 33     4 if( is_chiral_tetrahedral( $atom ) || is_chiral_planar( $atom ) ) {
    0          
    0          
274 1 50 33     4 next unless @chirality_neighbours >= 3 &&
275             @chirality_neighbours <= 4;
276 1         2 $has_lone_pair = @chirality_neighbours == 3;
277             } elsif( is_chiral_trigonal_bipyramidal( $atom ) ) {
278 0 0 0     0 next unless @chirality_neighbours >= 4 &&
279             @chirality_neighbours <= 5;
280 0         0 $has_lone_pair = @chirality_neighbours == 4;
281             } elsif( is_chiral_octahedral( $atom ) ) {
282 0 0 0     0 next unless @chirality_neighbours >= 5 &&
283             @chirality_neighbours <= 6;
284 0         0 $has_lone_pair = @chirality_neighbours == 5;
285             }
286              
287 1 50       12 if( $has_lone_pair ) {
288 0         0 @chirality_neighbours = ( $chirality_neighbours[0],
289             {}, # marking the lone pair
290             @chirality_neighbours[1..$#chirality_neighbours] );
291             }
292              
293 1 50       2 if( is_chiral_tetrahedral( $atom ) ) {
    0          
    0          
294             # Algorithm is described in detail in doi:10.1186/s13321-023-00692-1
295 1 50       3 if( $atom->{chirality} eq '@' ) {
296             # Reverse the order if counter-clockwise
297 1         3 @chirality_neighbours = ( $chirality_neighbours[0],
298             reverse @chirality_neighbours[1..3] );
299             }
300              
301 1         3 for my $i (0..3) {
302 4         9 my $neighbour = $chirality_neighbours[$i];
303 4         10 my @chirality_neighbours_now = @chirality_neighbours;
304              
305 4 100       10 if( $i % 2 ) {
306             # Reverse the order due to projected atom change
307 2         5 @chirality_neighbours_now = ( $chirality_neighbours_now[0],
308             reverse @chirality_neighbours_now[1..3] );
309             }
310              
311 4         8 my @other = grep { $_ != $neighbour } @chirality_neighbours_now;
  16         34  
312 4         9 for my $offset (0..2) {
313 12         21 my $connector = {};
314 12         312 $moiety->set_edge_attribute( $neighbour, $connector, 'chiral', 'from' );
315 12         6138 $moiety->set_edge_attribute( $atom, $connector, 'chiral', 'to' );
316              
317 12         5990 $moiety->set_edge_attribute( $connector, $other[0], 'chiral', 1 );
318 12         6021 $moiety->set_edge_attribute( $connector, $other[1], 'chiral', 2 );
319 12         6149 $moiety->set_edge_attribute( $connector, $other[2], 'chiral', 3 );
320              
321 12         5718 push @other, shift @other;
322             }
323             }
324             } elsif( is_chiral_planar( $atom ) ) {
325             # For square planar environments it is enough to retain the enumeration order of atoms.
326             # To do so, "neighbouring neighbours" are connected together and a link to central atom is placed.
327 0 0       0 if( $atom->{chirality} eq '@SP2' ) { # 4
    0          
328 0         0 @chirality_neighbours = map { $chirality_neighbours[$_] } ( 0, 2, 1, 3 );
  0         0  
329             } elsif( $atom->{chirality} eq '@SP3' ) { # Z
330 0         0 @chirality_neighbours = map { $chirality_neighbours[$_] } ( 0, 1, 3, 2 );
  0         0  
331             }
332              
333 0         0 for my $i (0..3) {
334 0         0 my $connector = {};
335 0         0 $moiety->set_edge_attribute( $atom, $connector, 'chiral', 'center' );
336 0         0 $moiety->set_edge_attribute( $connector, $chirality_neighbours[$i], 'chiral', 'neighbour' );
337 0         0 $moiety->set_edge_attribute( $connector, $chirality_neighbours[($i + 1) % 4], 'chiral', 'neighbour' );
338             }
339             } elsif( is_chiral_trigonal_bipyramidal( $atom ) ) {
340 0         0 my $number = substr $atom->{chirality}, 3;
341 0         0 my $setting = $TB[$number - 1];
342              
343 0         0 my @axis = map { $chirality_neighbours[$_ - 1] } @{$setting->{axis}};
  0         0  
  0         0  
344 0 0       0 my @other = grep { $_ != $axis[0] && $_ != $axis[1] }
345 0         0 map { $chirality_neighbours[$_] } 0..4;
  0         0  
346 0 0       0 @other = reverse @other if $setting->{order} eq '@@';
347              
348 0         0 for my $from (@axis) {
349 0     0   0 my $to = first { $_ != $from } @axis;
  0         0  
350 0         0 for (0..2) {
351 0         0 my $connector = {};
352 0         0 $moiety->set_edge_attribute( $from, $connector, 'chiral', 'from' );
353 0         0 $moiety->set_edge_attribute( $atom, $connector, 'chiral', 'center' );
354 0         0 $moiety->set_edge_attribute( $to, $connector, 'chiral', 'to' );
355              
356 0         0 $moiety->set_edge_attribute( $connector, $other[-1], 'chiral', 'counter-clockwise' );
357 0         0 $moiety->set_edge_attribute( $connector, $other[ 1], 'chiral', 'clockwise' );
358              
359 0         0 push @other, shift @other;
360             }
361 0         0 @other = reverse @other; # Inverting the axis
362             }
363             } else { # Chiral octahedral
364 0         0 my $chirality = int substr $atom->{chirality}, 3;
365 0         0 my @axis = map { $chirality_neighbours[$_-1] }
366 0         0 @{$OH[$chirality-1]->{axis}};
  0         0  
367 0 0       0 my @sides = grep { $_ != $axis[0] && $_ != $axis[1] }
  0         0  
368             @chirality_neighbours;
369              
370 0 0       0 if( $OH[$chirality-1]->{shape} eq 'Z' ) {
371 0         0 ( $sides[2], $sides[3] ) = ( $sides[3], $sides[2] );
372             }
373              
374 0 0       0 if( $OH[$chirality-1]->{shape} eq '4' ) {
375 0         0 ( $sides[0], $sides[3] ) = ( $sides[3], $sides[0] );
376             }
377              
378 0         0 @chirality_neighbours = ( $axis[0], @sides, $axis[1] );
379              
380 0         0 for my $side (( [ [ 0, 5 ], [ 1, 2, 3, 4 ] ],
381             [ [ 1, 3 ], [ 0, 4, 5, 2 ] ],
382             [ [ 2, 4 ], [ 0, 1, 5, 3 ] ] )) {
383 0         0 my @axis = map { $chirality_neighbours[$_] } @{$side->[0]};
  0         0  
  0         0  
384 0         0 my @other = map { $chirality_neighbours[$_] } @{$side->[1]};
  0         0  
  0         0  
385              
386 0         0 for my $from (@axis) {
387 0     0   0 my $to = first { $_ != $from } @axis;
  0         0  
388 0         0 for (0..3) {
389 0         0 my $connector = {};
390 0         0 $moiety->set_edge_attribute( $from, $connector, 'chiral', 'from' );
391 0         0 $moiety->set_edge_attribute( $atom, $connector, 'chiral', 'center' );
392 0         0 $moiety->set_edge_attribute( $to, $connector, 'chiral', 'to' );
393              
394 0         0 $moiety->set_edge_attribute( $connector, $other[-1], 'chiral', 'counter-clockwise' );
395 0         0 $moiety->set_edge_attribute( $connector, $other[ 1], 'chiral', 'clockwise' );
396              
397 0         0 push @other, shift @other;
398             }
399 0         0 @other = reverse @other; # Inverting the axis
400             }
401             }
402             }
403             }
404             }
405              
406             sub cis_trans_to_pseudoedges
407             {
408 3     3 0 18026 my( $moiety ) = @_;
409              
410             # Select non-ring double bonds
411             my @double_bonds =
412 3 100 66     32 grep { is_double_bond( $moiety, @$_ ) &&
  22         5989  
413             !is_ring_bond( $moiety, @$_ ) &&
414             !is_unimportant_double_bond( $moiety, @$_ ) } $moiety->edges;
415              
416             # Connect cis/trans atoms in double bonds with pseudo-edges
417 3         1352 for my $bond (@double_bonds) {
418 2         8 my( $atom2, $atom3 ) = @$bond;
419 2         9 my @atom2_neighbours = grep { !is_pseudoedge( $moiety, $atom2, $_ ) }
  5         1314  
420             $moiety->neighbours( $atom2 );
421 2         653 my @atom3_neighbours = grep { !is_pseudoedge( $moiety, $atom3, $_ ) }
  5         1304  
422             $moiety->neighbours( $atom3 );
423 2 50 33     674 next if @atom2_neighbours < 2 || @atom2_neighbours > 3 ||
      33        
      33        
424             @atom3_neighbours < 2 || @atom3_neighbours > 3;
425              
426 3     3   687 my $atom1 = first { is_cis_trans_bond( $moiety, $atom2, $_ ) }
427 2         19 @atom2_neighbours;
428 5     5   1701 my $atom4 = first { is_cis_trans_bond( $moiety, $atom3, $_ ) }
429 2         1421 @atom3_neighbours;
430 2 100 66     1375 next unless $atom1 && $atom4;
431              
432 1 100   2   9 my $atom1_para = first { $_ != $atom1 && $_ != $atom3 } @atom2_neighbours;
  2         28  
433 1 50   2   8 my $atom4_para = first { $_ != $atom4 && $_ != $atom2 } @atom3_neighbours;
  2         10  
434              
435 1         31 my $is_cis = $moiety->get_edge_attribute( $atom1, $atom2, 'bond' ) ne
436             $moiety->get_edge_attribute( $atom3, $atom4, 'bond' );
437              
438             # Here atom numbers have to be compared to differentiate between cases like:
439             # C/C=C\C and C(\C)=C/C
440 1 50       734 $is_cis = !$is_cis if $atom1->{number} > $atom2->{number};
441 1 50       5 $is_cis = !$is_cis if $atom3->{number} > $atom4->{number};
442              
443 1 50       29 $moiety->set_edge_attribute( $atom1, $atom4, 'pseudo',
444             $is_cis ? 'cis' : 'trans' );
445 1 50       657 if( $atom1_para ) {
446 1 50       52 $moiety->set_edge_attribute( $atom1_para, $atom4, 'pseudo',
447             $is_cis ? 'trans' : 'cis' );
448             }
449 1 50       623 if( $atom4_para ) {
450 1 50       33 $moiety->set_edge_attribute( $atom1, $atom4_para, 'pseudo',
451             $is_cis ? 'trans' : 'cis' );
452             }
453 1 50 33     633 if( $atom1_para && $atom4_para ) {
454 1 50       31 $moiety->set_edge_attribute( $atom1_para, $atom4_para, 'pseudo',
455             $is_cis ? 'cis' : 'trans' );
456             }
457             }
458              
459             # Unset cis/trans bond markers during second pass
460 3         691 for my $bond ($moiety->edges) {
461 26 100       9955 next unless is_cis_trans_bond( $moiety, @$bond );
462 5         3347 $moiety->delete_edge_attribute( @$bond, 'bond' );
463             }
464             }
465              
466             sub is_pseudoedge
467             {
468 10     10 0 26 my( $moiety, $a, $b ) = @_;
469 10         250 return $moiety->has_edge_attribute( $a, $b, 'pseudo' );
470             }
471              
472             # An "unimportant" double bond is one which has chemically identical atoms on one of its sides.
473             # If C<$color_sub> is given, it is used to determine chemical identity of atoms.
474             # If not, only leaf atoms are considered and compared.
475             sub is_unimportant_double_bond
476             {
477 7     7 0 517 my( $moiety, $a, $b, $color_sub ) = @_;
478 7         24 my @a_neighbours = grep { $_ != $b } $moiety->neighbours( $a );
  17         929  
479 7         27 my @b_neighbours = grep { $_ != $a } $moiety->neighbours( $b );
  19         855  
480              
481 7         22 for (\@a_neighbours, \@b_neighbours) {
482 13 100       1285 next unless @$_ == 2;
483              
484 8         14 my @representations;
485 8 50       20 if( $color_sub ) {
486 0         0 @representations = map { $color_sub->( $_ ) } @$_;
  0         0  
487             } else {
488 8 100   14   49 next if any { $moiety->degree( $_ ) != 1 } @$_;
  14         2360  
489 4         1575 @representations = map { write_SMILES( $_ ) } @$_;
  8         29  
490             }
491 4 50       37 return 1 if uniq( @representations ) == 1;
492             }
493              
494 3         1204 return;
495             }
496              
497             1;