File Coverage

blib/lib/Chemistry/OpenSMILES/Writer.pm
Criterion Covered Total %
statement 336 354 94.9
branch 180 204 88.2
condition 87 105 82.8
subroutine 35 36 97.2
pod 1 2 50.0
total 639 701 91.1


line stmt bran cond sub pod time code
1             package Chemistry::OpenSMILES::Writer;
2              
3             # ABSTRACT: OpenSMILES format writer
4             our $VERSION = '0.12.4'; # VERSION
5              
6             =head1 NAME
7              
8             Chemistry::OpenSMILES::Writer - OpenSMILES format writer
9              
10             =head1 SYNOPSIS
11              
12             use Graph::Undirected;
13             use Chemistry::OpenSMILES::Writer qw( write_SMILES );
14              
15             my $g = Graph::Undirected->new;
16             $g->add_edge( { symbol => 'C' }, { symbol => 'O' } );
17             print write_SMILES( [ $g ] );
18              
19             =head1 DESCRIPTION
20              
21             C is writer for molecular graph objects created by L or other means.
22             It exports a single subroutine, C, which is given an array of molecular graph objects and outputs a SMILES representation of them.
23              
24             =cut
25              
26 30     30   42290 use strict;
  30         45  
  30         888  
27 30     30   94 use warnings;
  30         42  
  30         1653  
28              
29 30         4047 use Chemistry::OpenSMILES qw(
30             %bond_symbol_to_order
31             %normal_valence
32             can_unsprout_hydrogen
33             is_aromatic
34             is_chiral
35             is_chiral_octahedral
36             is_chiral_planar
37             is_chiral_tetrahedral
38             is_chiral_trigonal_bipyramidal
39             toggle_cistrans
40             valence
41 30     30   2832 );
  30         105  
42 30     30   2932 use Chemistry::OpenSMILES::Parser;
  30         74  
  30         827  
43 30     30   123 use Chemistry::OpenSMILES::Stereo::Tables qw( @OH @TB );
  30         50  
  30         2310  
44 30     30   10085 use Graph::Traversal::DFS;
  30         7876  
  30         854  
45 30     30   134 use List::Util qw( all any first min none sum0 uniq );
  30         36  
  30         2000  
46 30     30   124 use Set::Object qw( set );
  30         72  
  30         122769  
47              
48             require Exporter;
49             our @ISA = qw( Exporter );
50             our @EXPORT_OK = qw(
51             write_SMILES
52             );
53              
54             my %shape_to_SP = ( 'U' => '@SP1', '4' => '@SP2', 'Z' => '@SP3' );
55             my %SP_to_shape = reverse %shape_to_SP;
56              
57             =head1 METHODS
58              
59             =head2 C
60              
61             C<@molecules> is an array of molecular graph objects.
62              
63             =head3 Options
64              
65             =over
66              
67             =cut
68              
69             sub write_SMILES
70             {
71 245     245 1 466831 my( $what, $options ) = @_;
72             # Backwards compatibility with the old API where second parameter was
73             # a subroutine reference for ordering:
74 245 50 66     1096 my $order_sub = defined $options && ref $options eq 'CODE' ? $options : \&_order;
75 245 100 66     847 $options = {} unless defined $options && ref $options eq 'HASH';
76              
77             =item C
78              
79             Boolean flag instructing the writer to output all aromatic bonds as ':'.
80             Off by default.
81              
82             =cut
83              
84             $options->{explicit_aromatic_bonds} = ''
85 245 100       778 unless exists $options->{explicit_aromatic_bonds};
86              
87             =item C
88              
89             Boolean flag instructing the writer to always enclose "children" atoms in parentheses.
90             Off by default.
91              
92             =cut
93              
94             $options->{explicit_parentheses} = ''
95 245 100       593 unless exists $options->{explicit_parentheses};
96              
97             =item C
98              
99             Boolean flag instructing the writer to immediately reuse ring closure numbers.
100             On by default.
101             Immediately reused ring numbers might cause some confusion for human readers, but the benefit of reuse is the ability to have more open rings at once.
102              
103             =cut
104              
105             $options->{immediately_reuse_ring_numbers} = 1
106 245 100       608 unless exists $options->{immediately_reuse_ring_numbers};
107              
108             =item C
109              
110             Boolean flag instructing the writer to remove hydrogens, expressed as atom properties, when their number can be unambiguously derived from normal valency.
111             On by default.
112              
113             =cut
114              
115             $options->{remove_implicit_hydrogens} = 1
116 245 100       552 unless exists $options->{remove_implicit_hydrogens};
117              
118             =item C
119              
120             Boolean flag instructing the writer to demote explicit hydrogens ("atoms on their own", "sprouted") to atom properties of their parent heavy atom.
121             On by default.
122             Not all hydrogens can be demoted.
123              
124             =cut
125              
126             $options->{unsprout_hydrogens} = 1
127 245 100       561 unless exists $options->{unsprout_hydrogens};
128              
129             =item C
130              
131             Subroutine reference used to determine the next atom in order upon ambiguity.
132             If none is provided, input order is retained whenever possible.
133             It should be noted, however, that C does not necessary respect the order subroutine: if performs DFS merely guided by the requested order.
134             Thus before calling C the exact postorder is not known.
135             Only preorder is known, thus relative properties, such as cis/trans markers, have to be adjusted to preorder.
136             Other order-dependent markers have to be adjusted to preorder as well.
137              
138             =cut
139              
140 245 100       514 $order_sub = $options->{order_sub} if $options->{order_sub};
141              
142             =item C
143              
144             Subroutine reference used to depict an atom in the output SMILES.
145             Takes three parameters: atom hash, molecular graph and option hash.
146             Must return string.
147             Caveat: interface might change.
148              
149             =cut
150              
151             my $write_atom_sub = $options->{write_atom_sub}
152             ? $options->{write_atom_sub}
153 245 50       549 : \&_depict_atom;
154 245         348 my $raw = $options->{raw};
155              
156             =back
157              
158             =cut
159              
160             # Subroutine will also accept and properly represent a single atom:
161 245 100       548 return $write_atom_sub->( $what, undef, $options ) if ref $what eq 'HASH';
162              
163 225 100       501 my @moieties = ref $what eq 'ARRAY' ? @$what : ( $what );
164 225         345 my @components;
165              
166 225         368 for my $graph (@moieties) {
167 245         478 my %seen;
168             my %discovered_from;
169 245         0 my @ring_bonds;
170              
171             my $operations = {
172 1328     1328   159506 tree_edge => sub { my( $u, $v ) = @_;
173 1328 50       3056 ( $u, $v ) = ( $v, $u ) if $seen{$v};
174 1328         2686 $discovered_from{$v} = $u },
175              
176 154     154   77442 non_tree_edge => sub { push @ring_bonds, [ @_[0..1] ] },
177              
178 1573     1573   43056 pre => sub { $seen{$_[0]} = 1 },
179              
180 245         1914 next_root => undef,
181             };
182              
183             $operations->{first_root} =
184 245     245   670 sub { $order_sub->( $_[1], $_[0]->graph ) };
  245         8206  
185             $operations->{next_successor} =
186 245     1328   603 sub { $order_sub->( $_[1], $_[0]->graph ) };
  1328         270898  
187              
188 245         1408 my $traversal = Graph::Traversal::DFS->new( $graph, %$operations );
189 245         52025 $traversal->dfs;
190 245         90258 my @order = $traversal->preorder;
191 245     11591   5227 my $order_by_vertex = sub { first { $order[$_] == $_[0] } 0..$#order };
  11591         24772  
  47403         67734  
192              
193 245 100       655 if( @order != $graph->vertices ) {
194 1         20 warn $graph->vertices - @order . ' unreachable atom(s) detected in moiety' . "\n";
195             }
196              
197 245 50       4512 next unless @order;
198              
199 245 100       584 if( $options->{unsprout_hydrogens} ) {
200 219         322 @order = grep { !can_unsprout_hydrogen( $graph, $_ ) } @order;
  1343         3619  
201             }
202              
203             # Create both old and new ring data structures
204 245         373 my $rings;
205 245         439 for my $ring_bond (@ring_bonds) {
206 154         409 my @sorted = sort { $order_by_vertex->($a) <=> $order_by_vertex->($b) } @$ring_bond;
  154         234  
207             $rings->{$order_by_vertex->($ring_bond->[0])}
208             {$order_by_vertex->($ring_bond->[1])} =
209             $rings->{$order_by_vertex->($ring_bond->[1])}
210 154         639 {$order_by_vertex->($ring_bond->[0])} =
211             { bond => _depict_bond( @sorted, $graph, $options ) };
212             }
213              
214             # Deal with chirality
215 245         357 my %chirality;
216 245         399 for my $atom (@order) {
217 1236 100       1951 next unless is_chiral $atom;
218 90 100 100     223 next unless is_chiral_tetrahedral( $atom ) ||
      100        
      100        
219             is_chiral_planar( $atom ) ||
220             is_chiral_trigonal_bipyramidal( $atom ) ||
221             is_chiral_octahedral( $atom );
222              
223 88         256 my @neighbours = $graph->neighbours($atom);
224 88         7302 my $has_lone_pair;
225 88 100       432 if( $atom->{chirality} =~ /^@(@?|SP[123])$/ ) {
226 75 50 33     337 if( scalar @neighbours < 3 || scalar @neighbours > 4 ) {
227 0         0 warn "chirality '$atom->{chirality}' observed for atom " .
228             'with ' . scalar @neighbours . ' neighbours, can only ' .
229             'process tetrahedral chiral or square planar centers ' .
230             'with possible lone pairs' . "\n";
231 0         0 next;
232             }
233 75         127 $has_lone_pair = @neighbours == 3;
234             }
235 88 100       224 if( $atom->{chirality} =~ /^\@TB..?$/ ) {
236 8 50 33     35 if( scalar @neighbours < 4 || scalar @neighbours > 5 ) {
237 0         0 warn "chirality '$atom->{chirality}' observed for atom " .
238             'with ' . scalar @neighbours . ' neighbours, can only ' .
239             'process trigonal bipyramidal centers ' .
240             'with possible lone pairs' . "\n";
241 0         0 next;
242             }
243 8         15 $has_lone_pair = @neighbours == 4;
244             }
245 88 100       178 if( $atom->{chirality} =~ /^\@OH..?$/ ) {
246 5 50 33     19 if( scalar @neighbours < 5 || scalar @neighbours > 6 ) {
247 0         0 warn "chirality '$atom->{chirality}' observed for atom " .
248             'with ' . scalar @neighbours . ' neighbours, can only ' .
249             'process octahedral centers ' .
250             'with possible lone pairs' . "\n";
251 0         0 next;
252             }
253 5         9 $has_lone_pair = @neighbours == 5;
254             }
255              
256 88 50       179 next unless exists $atom->{chirality_neighbours};
257              
258 88         134 my $chirality_now = $atom->{chirality};
259 88         114 my @chirality_neighbours = @{$atom->{chirality_neighbours}};
  88         208  
260 88 50       198 if( @neighbours != @chirality_neighbours ) {
261 0         0 warn 'number of neighbours does not match the length ' .
262             "of 'chirality_neighbours' array, cannot process " .
263             'such chiral centers' . "\n";
264 0         0 next;
265             }
266              
267             # Lone pair is simulated by an empty hash
268 88         132 my $lone_pair = {};
269 88 100       188 if( $has_lone_pair ) {
270 8         21 @chirality_neighbours = ( $chirality_neighbours[0],
271             $lone_pair,
272             @chirality_neighbours[1..$#chirality_neighbours] );
273             }
274              
275 88         173 my $order = $order_by_vertex->($atom);
276              
277             # First, find the initial atom, there can only be one.
278 72         170 my @order_new = map { $order[$_] }
279 0         0 sort { $a <=> $b }
280 338         473 grep { $_ < $order }
281 341         611 grep { !exists $rings->{$order}{$_} } # ignore ring bonds
282 362         494 grep { defined $_ } # ignore removed H atoms
283 88         302 map { $order_by_vertex->($_) }
  362         486  
284             @neighbours;
285             # Second, lone pair and unsproutable H atoms, will be added later.
286             # Third, there will be ring bonds as they are added before all of the neighbours
287 88 50       176 if( $rings->{$order_by_vertex->($atom)} ) {
288 3         10 push @order_new, map { $order[$_] }
289 0         0 sort { $a <=> $b }
290 88         131 keys %{$rings->{$order_by_vertex->($atom)}};
  88         139  
291             }
292             # Finally, all neighbours are added, uniq will remove duplicates
293 341         498 push @order_new, map { $order[$_] }
294 404         541 sort { $a <=> $b }
295 362         569 grep { defined $_ } # ignore removed H atoms
296 88         194 map { $order_by_vertex->($_) }
  362         410  
297             @neighbours;
298 88         497 @order_new = uniq @order_new;
299              
300             # Add unsproutable H atoms
301 88 100       283 if( $options->{unsprout_hydrogens} ) {
302 71 100   208   166 if( any { defined $_ && $_ < $order }
  208 100       490  
303 291         372 map { $order_by_vertex->($_) } @neighbours ) {
304 57         86 splice @order_new, 1, 0, grep { can_unsprout_hydrogen( $graph, $_ ) }
  235         378  
305             @neighbours;
306             } else {
307 14         26 unshift @order_new, grep { can_unsprout_hydrogen( $graph, $_ ) }
  56         93  
308             @neighbours;
309             }
310             }
311             # Add lone pair
312 88 100       252 if( $has_lone_pair ) {
313 8         17 splice @order_new, 1, 0, $lone_pair;
314             }
315              
316 88         241 my @permutation = _array_map( \@chirality_neighbours,
317             \@order_new );
318              
319 88 100       388 if( $atom->{chirality} =~ /^@@?$/ ) {
    100          
    100          
320             # Tetragonal centers
321 69 100       181 if( join( '', _permutation_order( @permutation ) ) ne '0123' ) {
322 12 100       36 $chirality_now = $chirality_now eq '@' ? '@@' : '@';
323             }
324             } elsif( $atom->{chirality} =~ /^\@SP[123]$/ ) {
325             # Square planar centers
326 6         19 $chirality_now = _square_planar_chirality( @permutation, $chirality_now );
327             } elsif( $atom->{chirality} =~ /^\@TB..?$/ ) {
328             # Trigonal bipyramidal centers
329 8         33 $chirality_now = _trigonal_bipyramidal_chirality( @permutation, $chirality_now );
330             } else {
331             # Octahedral centers
332 5         17 $chirality_now = _octahedral_chirality( @permutation, $chirality_now );
333             }
334 88         419 $chirality{$atom} = $chirality_now;
335             }
336              
337             # Write the SMILES
338 245         419 my $component = '';
339 245         1479 my @ring_ids = ( 1..99, 0 );
340 245         573 for my $i (0..$#order) {
341 1236         1588 my $vertex = $order[$i];
342 1236 100       2377 if( $discovered_from{$vertex} ) {
343 995 100 100     2642 if( $options->{explicit_parentheses} ||
344             _has_more_unseen_children( $discovered_from{$vertex}, $i, $order_by_vertex, $graph, $rings ) ) {
345 385         509 $component .= '(';
346             }
347 995         2227 $component .= _depict_bond( $discovered_from{$vertex}, $vertex, $graph, $options );
348             }
349 1236 100       2551 if( $chirality{$vertex} ) {
350             $component .=
351             $write_atom_sub->( $vertex,
352             $graph,
353 88         571 { %$options, chirality => $chirality{$vertex}, raw => 1 } );
354             } else {
355 1148         6304 $component .=
356             $write_atom_sub->( $vertex,
357             $graph,
358             { %$options, chirality => undef } );
359             }
360 1236 100       4436 if( $rings->{$i} ) {
361 233         318 my @rings_closed;
362 233         306 for my $j (sort { $a <=> $b } keys %{$rings->{$i}}) {
  6         18  
  233         665  
363 154 100       294 if( $i < $j ) {
364 77 50       147 if( !@ring_ids ) {
365             # All 100 rings are open now.
366             # There is no other solution but to terminate the program.
367 0         0 die 'cannot represent more than 100 open ring bonds' . "\n";
368             }
369 77         208 $rings->{$i}{$j}{ring} = shift @ring_ids;
370             $component .= $rings->{$i}{$j}{bond} .
371             ($rings->{$i}{$j}{ring} < 10 ? '' : '%') .
372             $rings->{$i}{$j}{ring}
373 77 50       283 } else {
374             $component .= toggle_cistrans( $rings->{$j}{$i}{bond} ) .
375             ($rings->{$i}{$j}{ring} < 10 ? '' : '%') .
376 77 50       339 $rings->{$j}{$i}{ring};
377 77 100       158 if( $options->{immediately_reuse_ring_numbers} ) {
378             # Ring bond '0' must stay in the end
379 7022 50       8667 @ring_ids = sort { ($a == 0) - ($b == 0) || $a <=> $b }
380 71         331 ($rings->{$j}{$i}{ring}, @ring_ids);
381             } else {
382 6         12 push @rings_closed, $rings->{$j}{$i}{ring};
383             }
384             }
385             }
386 233 100       536 if( !$options->{immediately_reuse_ring_numbers} ) {
387 11 50       39 @ring_ids = sort { ($a == 0) - ($b == 0) || $a <=> $b }
  1084         1327  
388             (@rings_closed, @ring_ids);
389             }
390             }
391 1236 100       3081 my $where = $i < $#order ? $discovered_from{$order[$i+1]} : $order[0];
392 1236         3010 while( $vertex != $where ) {
393 991 100 100     2522 if( $options->{explicit_parentheses} ||
394             _has_more_unseen_children( $discovered_from{$vertex}, $i, $order_by_vertex, $graph, $rings ) ) {
395 385         523 $component .= ')';
396             }
397 991         2904 $vertex = $discovered_from{$vertex};
398             }
399             }
400              
401 245         7030 push @components, $component;
402             }
403              
404 225         1189 return join '.', @components;
405             }
406              
407             # DEPRECATED
408 0     0 0 0 sub write { &write_SMILES }
409              
410             # Given arrays A and B, return a permutation of indices, making B from A.
411             sub _array_map
412             {
413 88     88   159 my( $A, $B ) = @_;
414 88 50       191 die "arrays have unequal length\n" unless @$A == @$B;
415              
416 88         111 my @permutation;
417 88         155 for my $element (@$B) {
418 370         532 push @permutation, grep { $A->[$_] eq $element } 0..$#$A;
  1580         2443  
419             }
420 88         183 return @permutation;
421             }
422              
423             sub _depict_atom
424             {
425 1256     1256   1930 my( $vertex, $graph, $options ) = @_;
426 1256 50       2063 $options = {} unless $options;
427             my( $chirality,
428             $raw ) =
429             ( $options->{chirality},
430 1256         2175 $options->{raw} );
431              
432 1256         1856 my $atom = $vertex->{symbol};
433 1256   100     4451 my $is_simple = $atom =~ /^[bcnosp]$/i ||
434             $atom =~ /^(F|Cl|Br|I|\*)$/;
435              
436 1256 100       2213 if( exists $vertex->{isotope} ) {
437 3         6 $atom = $vertex->{isotope} . $atom;
438 3         3 $is_simple = 0;
439             }
440              
441 1256 100       1898 if( exists $options->{chirality} ) {
    50          
442 1236 100       2093 if( $options->{chirality} ) {
443 88         167 $atom .= $options->{chirality};
444 88         133 $is_simple = 0;
445             }
446             } elsif( is_chiral $vertex ) {
447 0         0 $atom .= $vertex->{chirality};
448 0         0 $is_simple = 0;
449             }
450              
451 1256 100       2032 my $hcount = $vertex->{hcount} ? $vertex->{hcount} : 0;
452 1256 100 100     3099 if( $graph && $options->{unsprout_hydrogens} && $atom ne 'H' ) {
      100        
453 998         7185 $hcount += grep { can_unsprout_hydrogen( $graph, $_ ) }
  2053         78738  
454             $graph->neighbours( $vertex );
455             }
456              
457             # Decide what to do to atoms with usual/unusual valences
458 1256 100 100     8044 if( $is_simple && $graph && !$raw && $normal_valence{ucfirst $atom} &&
      100        
      66        
      100        
      100        
459             !$vertex->{charge} &&
460             !$vertex->{class} ) {
461 429 100       4725 if( _has_unambiguous_normal_valence( $graph, $vertex, $hcount ) ) {
462             # Usual valence detected, no need to keep hcount
463 407 100       950 $hcount = 0 if $options->{remove_implicit_hydrogens};
464             } else {
465             # Unusual valence detected, need square brackets
466 22         36 $is_simple = 0;
467             }
468             }
469              
470 1256 100       5146 if( $hcount ) { # if non-zero
471 49 100       126 $atom .= 'H' . ($hcount == 1 ? '' : $hcount);
472 49         66 $is_simple = 0;
473             }
474 1256 100 100     2806 $is_simple = 0 if $raw && exists $vertex->{hcount};
475              
476 1256 100       2022 if( $vertex->{charge} ) { # if non-zero
477 12 100       39 $atom .= ($vertex->{charge} > 0 ? '+' : '') . $vertex->{charge};
478 12         65 $atom =~ s/([-+])1$/$1/;
479 12         18 $is_simple = 0;
480             }
481              
482 1256 100       1959 if( $vertex->{class} ) { # if non-zero
483 26         44 $atom .= ':' . $vertex->{class};
484 26         30 $is_simple = 0;
485             }
486              
487 1256 100       3247 return $is_simple ? $atom : "[$atom]";
488             }
489              
490             # _depict_bond() gets vertices in order of their appearance in the post-order.
491             # It flips '/' <=> '\' if post-order is opposite from pre-order.
492             sub _depict_bond
493             {
494 1149     1149   1906 my( $u, $v, $graph, $options ) = @_;
495              
496 1149         1510 my $n_aromatic = grep { is_aromatic $_ } ( $u, $v );
  2298         3723  
497              
498 1149 100       22097 if( !$graph->has_edge_attribute( $u, $v, 'bond' ) ) {
499 823 100       147953 return $n_aromatic == 2 ? '-' : '';
500             }
501              
502 326         61001 my $bond = $graph->get_edge_attribute( $u, $v, 'bond' );
503 326 50 66     54144 return '' if $bond eq ':' && $n_aromatic && !$options->{explicit_aromatic_bonds};
      66        
504 182 100       614 return $bond if $u->{number} < $v->{number};
505 24         62 return toggle_cistrans $bond;
506             }
507              
508             sub _has_more_unseen_children
509             {
510 1742     1742   3261 my( $vertex, $i, $order_by_vertex, $graph, $rings ) = @_;
511 4824         7366 my $orders = set( grep { $_ > $i }
512 5254         6810 grep { defined $_ } # ignore removed H atoms
513 1742         3385 map { $order_by_vertex->($_) }
  5254         133843  
514             $graph->neighbours( $vertex ) );
515 1742 100 100     20529 if( defined $order_by_vertex->($vertex) &&
516             $rings->{$order_by_vertex->($vertex)} ) {
517 654         780 $orders->remove( keys %{$rings->{$order_by_vertex->($vertex)}} );
  654         842  
518             }
519 1742         8542 return $orders->size;
520             }
521              
522             sub _has_unambiguous_normal_valence
523             {
524 429     429   668 my( $graph, $vertex, $hcount ) = @_;
525 429         611 my $element = ucfirst $vertex->{symbol};
526 429 50       736 return '' unless exists $normal_valence{$element};
527              
528 429         970 my $valence = valence( $graph, $vertex );
529 429     430   1551 my $normal_valence = first { $_ == $valence } @{$normal_valence{$element}};
  430         728  
  429         9638  
530 429 100       1434 return '' unless $normal_valence;
531              
532 408     408   706 my $derived_valence = first { $_ >= $normal_valence - $hcount }
533 408         745 @{$normal_valence{$element}};
  408         745  
534 408   66     1845 return $derived_valence && $derived_valence == $normal_valence;
535             }
536              
537             # Reorder a permutation of elements 0, 1, 2 and 3 by taking an element
538             # and moving it two places either forward or backward in the line. This
539             # subroutine is used to check whether a sign change of tetragonal
540             # chirality is required or not.
541             sub _permutation_order
542             {
543             # Safeguard against endless cycles due to undefined values
544 94 100 100 94   232878 if( (scalar @_ != 4) ||
      66        
545 367   100 367   1485 (any { !defined || !/^[0-3]$/ } @_) ||
546             (join( ',', sort @_ ) ne '0,1,2,3') ) {
547 5         36 warn '_permutation_order() accepts only permutations of numbers ' .
548             "'0', '1', '2' and '3', unexpected input received";
549 5         41 return 0..3; # Return original order
550             }
551              
552 89   100     405 while( $_[2] == 0 || $_[3] == 0 ) {
553 55         210 @_ = ( $_[0], @_[2..3], $_[1] );
554             }
555 89 100       196 if( $_[0] != 0 ) {
556 40         146 @_ = ( @_[1..2], $_[0], $_[3] );
557             }
558 89         185 while( $_[1] != 1 ) {
559 56         185 @_[1..3] = ( @_[2..3], $_[1] );
560             }
561 89         357 return @_;
562             }
563              
564             sub _square_planar_chirality
565             {
566 6     6   11 my $chirality = pop @_;
567 6         13 my @source = 0..3;
568 6         9 my @target = @_;
569              
570 6 50       46 if( join( ',', sort @_ ) ne '0,1,2,3' ) {
571 0         0 die '_square_planar_chirality() accepts only permutations of ' .
572             "numbers '0', '1', '2' and '3', unexpected input received\n";
573             }
574              
575             # Rotations until 0 is first
576 6         14 while( $source[0] != $target[0] ) {
577 9         31 push @source, shift @source;
578 9         21 my %tab = ( '@SP1' => '@SP1', '@SP2' => '@SP3', '@SP3' => '@SP2' );
579 9         22 $chirality = $tab{$chirality};
580             }
581              
582 6 100       10 if( $source[3] == $target[1] ) { # Swap the right side
583 3         6 ( $source[2], $source[3] ) = ( $source[3], $source[2] );
584 3         8 my %tab = ( '@SP1' => '@SP3', '@SP2' => '@SP2', '@SP3' => '@SP1' );
585 3         5 $chirality = $tab{$chirality};
586             }
587              
588 6 100       13 if( $source[2] == $target[1] ) { # Swap the center
589 3         6 ( $source[1], $source[2] ) = ( $source[2], $source[1] );
590 3         18 my %tab = ( '@SP1' => '@SP2', '@SP2' => '@SP1', '@SP3' => '@SP3' );
591 3         5 $chirality = $tab{$chirality};
592             }
593              
594 6 100       9 if( $source[3] == $target[2] ) { # Swap the right side
595 3         4 ( $source[2], $source[3] ) = ( $source[3], $source[2] );
596 3         7 my %tab = ( '@SP1' => '@SP3', '@SP2' => '@SP2', '@SP3' => '@SP1' );
597 3         6 $chirality = $tab{$chirality};
598             }
599              
600 6         15 return $chirality;
601             }
602              
603             sub _trigonal_bipyramidal_chirality
604             {
605 4810     4810   1927062 my $chirality = pop @_;
606 4810         9885 my @target = @_;
607              
608 4810 50       27137 if( join( ',', sort @target ) ne '0,1,2,3,4' ) {
609 0         0 die '_trigonal_bipyramidal_chirality() accepts only permutations of ' .
610             "numbers '0', '1', '2', '3' and '4', unexpected input received\n";
611             }
612              
613 4810         15033 $chirality =~ s/^\@TB//;
614 4810         8743 $chirality = int $chirality;
615              
616 4810         8847 my $TB = $TB[$chirality - 1];
617              
618             # First on, decode the source.
619             # Axis will stay on @axis, and sides will be stored on @sides
620 4810         7151 my @axis = map { $_ - 1 } @{$TB->{axis}};
  9620         17267  
  4810         10087  
621 4810 100       8878 my @sides = grep { $_ != $axis[0] && $_ != $axis[1] } 0..4;
  24050         60718  
622              
623             # Find the new location of the axis, remove it from @target
624 14425     14425   28132 my @axis_location = ( ( first { $target[$_] == $axis[0] } 0..4 ),
625 4810     14445   23016 ( first { $target[$_] == $axis[1] } 0..4 ) );
  14445         18867  
626 4810 100       17586 @target = grep { $_ != $axis[0] && $_ != $axis[1] } @target;
  24050         59602  
627              
628             # Invert the axis if needed
629 4810 100       9323 if( $axis_location[0] > $axis_location[1] ) {
630 2402         3634 @axis_location = reverse @axis_location;
631 2402         3788 @target = reverse @target;
632             }
633              
634             # Cycle the sides clockwise until the first is aligned
635 4810         9320 while( $sides[0] != $target[0] ) {
636 4807         9717 push @sides, shift @sides;
637             }
638 4810         7776 my $order = $TB->{order};
639 4810 100       10645 $order = $order eq '@' ? '@@' : '@' unless $sides[1] == $target[1];
    100          
640              
641             $chirality = 1 + first { $TB[$_]->{order} eq $order &&
642             $TB[$_]->{axis}[0] == $axis_location[0] + 1 &&
643 50506 100 100 50506   120419 $TB[$_]->{axis}[1] == $axis_location[1] + 1 }
644 4810         21103 0..$#TB;
645 4810         29034 return '@TB' . $chirality;
646             }
647              
648             sub _octahedral_chirality
649             {
650 43205     43205   15558872 my $chirality = pop @_;
651 43205         98825 my @target = @_;
652              
653 43205 50       299079 if( join( ',', sort @target ) ne '0,1,2,3,4,5' ) {
654 0         0 die '_octahedral_chirality() accepts only permutations of ' .
655             "numbers '0', '1', '2', '3', '4' and '5, unexpected input received\n";
656             }
657              
658 43205         164446 $chirality =~ s/^\@OH//;
659 43205         97981 $chirality = int $chirality;
660              
661             # First on, decode the source.
662             # Axis will stay on @axis, and sides will be stored on @sides in contiguous clockwise order.
663 43205         71330 my @axis = map { $_ - 1 } @{$OH[$chirality-1]->{axis}};
  86410         169906  
  43205         135298  
664 43205 100       85103 my @sides = grep { $_ != $axis[0] && $_ != $axis[1] } 0..5;
  259230         717847  
665              
666 43205 100       130389 if( $OH[$chirality-1]->{shape} eq 'Z' ) {
667 14401         34748 ( $sides[2], $sides[3] ) = ( $sides[3], $sides[2] );
668             }
669              
670 43205 100       104812 if( $OH[$chirality-1]->{shape} eq '4' ) {
671 14401         32569 ( $sides[0], $sides[3] ) = ( $sides[3], $sides[0] );
672             }
673              
674             # Adjust for enumeration direction
675 43205 100       111315 @sides = reverse @sides if $OH[$chirality-1]->{order} eq '@';
676              
677             # Align the axis start
678 43205 100       112817 if( $axis[0] == $target[0] ) { # same axis start, do nothing
    100          
679             } elsif( $axis[1] == $target[0] ) { # axis inversion
680 7201         12310 @axis = reverse @axis;
681 7201         11734 @sides = reverse @sides;
682             } else { # axis start at one of the sides
683 28803     72010   166775 my $axis_index = first { $sides[$_] == $target[0] } 0..3;
  72010         126249  
684 28803         117743 my @axis_now = ( $sides[$axis_index], $sides[($axis_index + 2) % 4] );
685 28803         65763 ( $sides[$axis_index], $sides[($axis_index + 2) % 4] ) = reverse @axis;
686 28803         64834 @axis = @axis_now;
687             }
688              
689 43205         62478 shift @target; # axis start is no longer needed
690 43205     129619   172394 my $axis_end = first { $target[$_] == $axis[1] } 0..4;
  129619         192909  
691 43205         128456 @target = map { $target[$_] } grep { $_ != $axis_end } 0..4; # remove axis end
  172820         310139  
  216025         353038  
692              
693             # Cycle the sides clockwise until the first is aligned
694 43205         102875 while( $sides[0] != $target[0] ) {
695 64810         128662 push @sides, shift @sides;
696             }
697 43205         54906 shift @sides;
698 43205         57277 shift @target;
699              
700             # Check the alignment of the other sides to find the shape and order
701 43205         72292 my $shape;
702             my $order;
703 43205 100 100     263929 if( $target[0] == $sides[0] && $target[1] == $sides[1] ) {
    100 66        
    100 100        
    100 66        
    100 66        
    50 33        
704 7201         14387 ( $shape, $order ) = ( 'U', '@@' );
705             } elsif( $target[0] == $sides[0] && $target[1] == $sides[2] ) {
706 7201         14441 ( $shape, $order ) = ( 'Z', '@@' );
707             } elsif( $target[0] == $sides[1] && $target[1] == $sides[0] ) {
708 7200         15457 ( $shape, $order ) = ( '4', '@' );
709             } elsif( $target[0] == $sides[1] && $target[1] == $sides[2] ) {
710 7202         15497 ( $shape, $order ) = ( '4', '@@' );
711             } elsif( $target[0] == $sides[2] && $target[1] == $sides[0] ) {
712 7200         14292 ( $shape, $order ) = ( 'Z', '@' );
713             } elsif( $target[0] == $sides[2] && $target[1] == $sides[1] ) {
714 7201         13918 ( $shape, $order ) = ( 'U', '@' );
715             } else {
716 0         0 die 'unexpected situation achieved in _octahedral_chirality()' . "\n";
717             }
718             $chirality = 1 + first { $OH[$_]->{shape} eq $shape &&
719             $OH[$_]->{order} eq $order &&
720 669654 100 100 669654   1550920 $OH[$_]->{axis}[1] == $axis_end + 2 }
721 43205         219842 0..$#OH;
722 43205         295421 return '@OH' . $chirality;
723             }
724              
725             sub _order
726             {
727 1055     1055   4138 my( $vertices ) = @_;
728 1055         2872 my @sorted = sort { $vertices->{$a}{number} <=>
729 2858         4468 $vertices->{$b}{number} } keys %$vertices;
730 1055         2571 return $vertices->{shift @sorted};
731             }
732              
733             1;
734              
735             =head1 AUTHORS
736              
737             Andrius Merkys, Emerkys@cpan.orgE
738              
739             =cut