File Coverage

blib/lib/Chemistry/OpenSMILES/Writer.pm
Criterion Covered Total %
statement 157 165 95.1
branch 72 82 87.8
condition 25 27 92.5
subroutine 19 20 95.0
pod 0 2 0.0
total 273 296 92.2


line stmt bran cond sub pod time code
1             package Chemistry::OpenSMILES::Writer;
2              
3 13     13   6375 use strict;
  13         41  
  13         386  
4 13     13   97 use warnings;
  13         32  
  13         406  
5              
6 13         745 use Chemistry::OpenSMILES qw(
7             is_aromatic
8             is_chiral
9             toggle_cistrans
10 13     13   1005 );
  13         29  
11 13     13   1053 use Chemistry::OpenSMILES::Parser;
  13         30  
  13         337  
12 13     13   5433 use Graph::Traversal::DFS;
  13         3820  
  13         376  
13 13     13   75 use List::Util qw( any uniq );
  13         29  
  13         29053  
14              
15             # ABSTRACT: OpenSMILES format writer
16             our $VERSION = '0.8.4'; # VERSION
17              
18             require Exporter;
19             our @ISA = qw( Exporter );
20             our @EXPORT_OK = qw(
21             write_SMILES
22             );
23              
24             sub write_SMILES
25             {
26 155     155 0 34474 my( $what, $order_sub ) = @_;
27              
28 155 100       472 if( ref $what eq 'HASH' ) {
29             # subroutine will also accept and properly represent a single
30             # atom:
31 76         204 return _pre_vertex( $what );
32             }
33              
34 79 100       270 my @moieties = ref $what eq 'ARRAY' ? @$what : ( $what );
35 79         130 my @components;
36              
37 79 100       221 $order_sub = \&_order unless $order_sub;
38              
39 79         184 for my $graph (@moieties) {
40 81         144 my @symbols;
41             my %vertex_symbols;
42 81         131 my $nrings = 0;
43 81         208 my %seen_rings;
44             my @chiral;
45 81         0 my %discovered_from;
46              
47 81         151 my $rings = {};
48              
49             my $operations = {
50 423     423   67534 tree_edge => sub { my( $seen, $unseen, $self ) = @_;
51 423 50       1232 if( $vertex_symbols{$unseen} ) {
52 0         0 ( $seen, $unseen ) = ( $unseen, $seen );
53             }
54 423         1018 push @symbols, _tree_edge( $seen, $unseen, $self, $order_sub );
55 423         1586 $discovered_from{$unseen} = $seen },
56              
57 56     56   7413 non_tree_edge => sub { my @sorted = sort { $vertex_symbols{$a} <=>
58 56         261 $vertex_symbols{$b} }
59             @_[0..1];
60             $rings->{$vertex_symbols{$sorted[0]}}
61 56         133 {$vertex_symbols{$sorted[1]}} =
62             _depict_bond( @sorted, $graph ) },
63              
64 504     504   18605 pre => sub { my( $vertex, $dfs ) = @_;
65 504 100       1321 push @chiral, $vertex if is_chiral $vertex;
66             push @symbols,
67 1586         3933 _pre_vertex( { map { $_ => $vertex->{$_} }
68 504         1756 grep { $_ ne 'chirality' }
  1628         3229  
69             keys %$vertex } );
70 504         2248 $vertex_symbols{$vertex} = $#symbols },
71              
72 504     504   106672 post => sub { push @symbols, ')' },
73 81         976 next_root => undef,
74             };
75              
76 81 50       267 if( $order_sub ) {
77             $operations->{first_root} =
78 81     81   269 sub { return $order_sub->( $_[1], $_[0]->graph ) };
  81         3486  
79             $operations->{next_successor} =
80 81     423   256 sub { return $order_sub->( $_[1], $_[0]->graph ) };
  423         76410  
81             }
82              
83 81         539 my $traversal = Graph::Traversal::DFS->new( $graph, %$operations );
84 81         42125 $traversal->dfs;
85              
86 81 100       7310 if( scalar keys %vertex_symbols != scalar $graph->vertices ) {
87 1         25 warn scalar( $graph->vertices ) - scalar( keys %vertex_symbols ) .
88             ' unreachable atom(s) detected in moiety' . "\n";
89             }
90              
91 81 50       1994 next unless @symbols;
92 81         143 pop @symbols;
93              
94             # Dealing with chirality
95 81         231 for my $atom (@chiral) {
96 42 100       261 next unless $atom->{chirality} =~ /^@@?$/;
97              
98 40         157 my @neighbours = $graph->neighbours($atom);
99 40 100 66     4500 if( scalar @neighbours < 3 || scalar @neighbours > 4 ) {
100             # TODO: process also configurations other than tetrahedral
101 1         18 warn "chirality '$atom->{chirality}' observed for atom " .
102             'with ' . scalar @neighbours . ' neighbours, can only ' .
103             'process tetrahedral chiral centers with possible ' .
104             'lone pairs' . "\n";
105 1         8 next;
106             }
107              
108 39         102 my $chirality_now = $atom->{chirality};
109 39 50       118 if( $atom->{chirality_neighbours} ) {
110 39 50       70 if( scalar @neighbours !=
111 39         118 scalar @{$atom->{chirality_neighbours}} ) {
112 0         0 warn 'number of neighbours does not match the length ' .
113             "of 'chirality_neighbours' array, cannot process " .
114             'such chiral centers' . "\n";
115 0         0 next;
116             }
117              
118 39         79 my %indices;
119 39         69 for (0..$#{$atom->{chirality_neighbours}}) {
  39         141  
120 148         230 my $pos = $_;
121 148 100 100     188 if( scalar @{$atom->{chirality_neighbours}} == 3 && $_ != 0 ) {
  148         400  
122             # Lone pair is always second in the chiral neighbours array
123 16         22 $pos++;
124             }
125 148         463 $indices{$vertex_symbols{$atom->{chirality_neighbours}[$_]}} = $pos;
126             }
127              
128 39         87 my @order_new;
129             # In newly established order, the atom from which this one
130             # is discovered (left hand side) will be the first, if any
131 39 100       112 if( $discovered_from{$atom} ) {
132             push @order_new,
133 35         110 $indices{$vertex_symbols{$discovered_from{$atom}}};
134             }
135             # Second, there will be ring bonds as they are added the
136             # first of all the neighbours
137 39 100       110 if( $rings->{$vertex_symbols{$atom}} ) {
138 1         5 push @order_new, map { $indices{$_} }
139 0         0 sort { $a <=> $b }
140 1         3 keys %{$rings->{$vertex_symbols{$atom}}};
  1         5  
141             }
142             # Finally, all neighbours are added, uniq will remove duplicates
143 148         265 push @order_new, map { $indices{$_} }
144 169         291 sort { $a <=> $b }
145 39         87 map { $vertex_symbols{$_} }
  148         418  
146             @neighbours;
147 39         282 @order_new = uniq @order_new;
148              
149 39 100       145 if( scalar @order_new == 3 ) {
150             # Accommodate the lone pair
151 8 100       22 if( $discovered_from{$atom} ) {
152 6         22 @order_new = ( $order_new[0], 1, @order_new[1..2] );
153             } else {
154 2         6 unshift @order_new, 1;
155             }
156             }
157              
158 39 100       107 if( join( '', _permutation_order( @order_new ) ) ne '0123' ) {
159 7 100       45 $chirality_now = $chirality_now eq '@' ? '@@' : '@';
160             }
161             }
162              
163 39         189 my $parser = Chemistry::OpenSMILES::Parser->new;
164 39         221 my( $graph_reparsed ) = $parser->parse( $symbols[$vertex_symbols{$atom}],
165             { raw => 1 } );
166 39         119 my( $atom_reparsed ) = $graph_reparsed->vertices;
167 39         684 $atom_reparsed->{chirality} = $chirality_now;
168 39         197 $symbols[$vertex_symbols{$atom}] =
169             write_SMILES( $atom_reparsed );
170             }
171              
172             # Adding ring numbers
173 81         487 my @ring_ids = ( 1..99, 0 );
174 81         142 my @ring_ends;
175 81         255 for my $i (0..$#symbols) {
176 1350 100       2412 if( $rings->{$i} ) {
177 26         46 for my $j (sort { $a <=> $b } keys %{$rings->{$i}}) {
  2         10  
  26         126  
178 28 50       87 if( !@ring_ids ) {
179             # All 100 rings are open now. There is no other
180             # solution but to terminate the program.
181 0         0 die 'cannot represent more than 100 open ring' .
182             ' bonds';
183             }
184 28 50       119 $symbols[$i] .= $rings->{$i}{$j} .
185             ($ring_ids[0] < 10 ? '' : '%') .
186             $ring_ids[0];
187             $symbols[$j] .= ($rings->{$i}{$j} eq '/' ? '\\' :
188             $rings->{$i}{$j} eq '\\' ? '/' :
189 28 100       164 $rings->{$i}{$j}) .
    100          
    50          
190             ($ring_ids[0] < 10 ? '' : '%') .
191             $ring_ids[0];
192 28         45 push @{$ring_ends[$j]}, shift @ring_ids;
  28         92  
193             }
194             }
195 1350 100       2477 if( $ring_ends[$i] ) {
196             # Ring bond '0' must stay in the end
197 2770 50       4655 @ring_ids = sort { ($a == 0) - ($b == 0) || $a <=> $b }
198 28         49 (@{$ring_ends[$i]}, @ring_ids);
  28         137  
199             }
200             }
201              
202 81         2891 push @components, join '', @symbols;
203             }
204              
205 79         426 return join '.', @components;
206             }
207              
208             # DEPRECATED
209             sub write
210             {
211 0     0 0 0 &write_SMILES;
212             }
213              
214             sub _tree_edge
215             {
216 423     423   783 my( $u, $v, $self ) = @_;
217              
218 423         1053 return '(' . _depict_bond( $u, $v, $self->graph );
219             }
220              
221             sub _pre_vertex
222             {
223 580     580   1099 my( $vertex ) = @_;
224              
225 580         1050 my $atom = $vertex->{symbol};
226 580   100     2942 my $is_simple = $atom =~ /^[bcnosp]$/i ||
227             $atom =~ /^(F|Cl|Br|I|\*)$/;
228              
229 580 100       1352 if( exists $vertex->{isotope} ) {
230 3         8 $atom = $vertex->{isotope} . $atom;
231 3         7 $is_simple = 0;
232             }
233              
234 580 100       1296 if( is_chiral $vertex ) {
235 41         102 $atom .= $vertex->{chirality};
236 41         70 $is_simple = 0;
237             }
238              
239 580 100       1514 if( $vertex->{hcount} ) { # if non-zero
240 6 100       25 $atom .= 'H' . ($vertex->{hcount} == 1 ? '' : $vertex->{hcount});
241 6         41 $is_simple = 0;
242             }
243              
244 580 100       1205 if( $vertex->{charge} ) { # if non-zero
245 8 100       42 $atom .= ($vertex->{charge} > 0 ? '+' : '') . $vertex->{charge};
246 8         42 $atom =~ s/([-+])1$/$1/;
247 8         18 $is_simple = 0;
248             }
249              
250 580 50       1238 if( $vertex->{class} ) { # if non-zero
251 0         0 $atom .= ':' . $vertex->{class};
252 0         0 $is_simple = 0;
253             }
254              
255 580 100       3448 return $is_simple ? $atom : "[$atom]";
256             }
257              
258             sub _depict_bond
259             {
260 479     479   2205 my( $u, $v, $graph ) = @_;
261              
262 479 100       1238 if( !$graph->has_edge_attribute( $u, $v, 'bond' ) ) {
263 345 100 100     70151 return is_aromatic $u && is_aromatic $v ? '-' : '';
264             }
265              
266 134         27231 my $bond = $graph->get_edge_attribute( $u, $v, 'bond' );
267 134 100 100     27088 return $bond if $bond ne '/' && $bond ne '\\';
268 43 100       217 return $bond if $u->{number} < $v->{number};
269 13         39 return toggle_cistrans $bond;
270             }
271              
272             # Reorder a permutation of elements 0, 1, 2 and 3 by taking an element
273             # and moving it two places either forward or backward in the line. This
274             # subroutine is used to check whether a sign change of tetragonal
275             # chirality is required or not.
276             sub _permutation_order
277             {
278             # Safeguard against endless cycles due to undefined values
279 64 100 100 64   16546 if( (scalar @_ != 4) ||
      66        
280 247   100 247   1290 (any { !defined || !/^[0-3]$/ } @_) ||
281             (join( ',', sort @_ ) ne '0,1,2,3') ) {
282 5         56 warn '_permutation_order() accepts only permutations of numbers ' .
283             "'0', '1', '2' and '3', unexpected input received";
284 5         52 return 0..3; # Return original order
285             }
286              
287 59   100     395 while( $_[2] == 0 || $_[3] == 0 ) {
288 50         230 @_ = ( $_[0], @_[2..3], $_[1] );
289             }
290 59 100       164 if( $_[0] != 0 ) {
291 34         100 @_ = ( @_[1..2], $_[0], $_[3] );
292             }
293 59         145 while( $_[1] != 1 ) {
294 42         137 @_[1..3] = ( @_[2..3], $_[1] );
295             }
296 59         286 return @_;
297             }
298              
299             sub _order
300             {
301 364     364   1954 my( $vertices ) = @_;
302 364         1572 my @sorted = sort { $vertices->{$a}{number} <=>
303 901         1924 $vertices->{$b}{number} } keys %$vertices;
304 364         1206 return $vertices->{shift @sorted};
305             }
306              
307             1;