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.3'; # 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   57307 use strict;
  30         68  
  30         4431  
27 30     30   265 use warnings;
  30         117  
  30         2613  
28              
29 30         6292 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   3656 );
  30         83  
42 30     30   6025 use Chemistry::OpenSMILES::Parser;
  30         80  
  30         1392  
43 30     30   198 use Chemistry::OpenSMILES::Stereo::Tables qw( @OH @TB );
  30         79  
  30         3788  
44 30     30   15542 use Graph::Traversal::DFS;
  30         12344  
  30         1337  
45 30     30   233 use List::Util qw( all any first min none sum0 uniq );
  30         61  
  30         3495  
46 30     30   20981 use Set::Object qw( set );
  30         294497  
  30         196003  
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 746832 my( $what, $options ) = @_;
72             # Backwards compatibility with the old API where second parameter was
73             # a subroutine reference for ordering:
74 245 50 66     1923 my $order_sub = defined $options && ref $options eq 'CODE' ? $options : \&_order;
75 245 100 66     1413 $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       1351 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       980 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       966 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       1012 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       1016 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       793 $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       772 : \&_depict_atom;
154 245         563 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       888 return $write_atom_sub->( $what, undef, $options ) if ref $what eq 'HASH';
162              
163 225 100       830 my @moieties = ref $what eq 'ARRAY' ? @$what : ( $what );
164 225         478 my @components;
165              
166 225         569 for my $graph (@moieties) {
167 245         865 my %seen;
168             my %discovered_from;
169 245         0 my @ring_bonds;
170              
171             my $operations = {
172 1328     1328   273984 tree_edge => sub { my( $u, $v ) = @_;
173 1328 50       5280 ( $u, $v ) = ( $v, $u ) if $seen{$v};
174 1328         4433 $discovered_from{$v} = $u },
175              
176 154     154   138138 non_tree_edge => sub { push @ring_bonds, [ @_[0..1] ] },
177              
178 1573     1573   72928 pre => sub { $seen{$_[0]} = 1 },
179              
180 245         3262 next_root => undef,
181             };
182              
183             $operations->{first_root} =
184 245     245   1162 sub { $order_sub->( $_[1], $_[0]->graph ) };
  245         13485  
185             $operations->{next_successor} =
186 245     1328   1045 sub { $order_sub->( $_[1], $_[0]->graph ) };
  1328         470566  
187              
188 245         2195 my $traversal = Graph::Traversal::DFS->new( $graph, %$operations );
189 245         86241 $traversal->dfs;
190 245         162486 my @order = $traversal->preorder;
191 245     11591   8776 my $order_by_vertex = sub { first { $order[$_] == $_[0] } 0..$#order };
  11591         46081  
  47403         114982  
192              
193 245 100       1094 if( @order != $graph->vertices ) {
194 1         41 warn $graph->vertices - @order . ' unreachable atom(s) detected in moiety' . "\n";
195             }
196              
197 245 50       7736 next unless @order;
198              
199 245 100       937 if( $options->{unsprout_hydrogens} ) {
200 219         508 @order = grep { !can_unsprout_hydrogen( $graph, $_ ) } @order;
  1343         5987  
201             }
202              
203             # Create both old and new ring data structures
204 245         560 my $rings;
205 245         782 for my $ring_bond (@ring_bonds) {
206 154         701 my @sorted = sort { $order_by_vertex->($a) <=> $order_by_vertex->($b) } @$ring_bond;
  154         412  
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         842 {$order_by_vertex->($ring_bond->[0])} =
211             { bond => _depict_bond( @sorted, $graph, $options ) };
212             }
213              
214             # Deal with chirality
215 245         566 my %chirality;
216 245         608 for my $atom (@order) {
217 1236 100       3113 next unless is_chiral $atom;
218 90 100 100     395 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         448 my @neighbours = $graph->neighbours($atom);
224 88         12957 my $has_lone_pair;
225 88 100       723 if( $atom->{chirality} =~ /^@(@?|SP[123])$/ ) {
226 75 50 33     466 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         230 $has_lone_pair = @neighbours == 3;
234             }
235 88 100       368 if( $atom->{chirality} =~ /^\@TB..?$/ ) {
236 8 50 33     54 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         26 $has_lone_pair = @neighbours == 4;
244             }
245 88 100       321 if( $atom->{chirality} =~ /^\@OH..?$/ ) {
246 5 50 33     34 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         15 $has_lone_pair = @neighbours == 5;
254             }
255              
256 88 50       304 next unless exists $atom->{chirality_neighbours};
257              
258 88         229 my $chirality_now = $atom->{chirality};
259 88         192 my @chirality_neighbours = @{$atom->{chirality_neighbours}};
  88         318  
260 88 50       355 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         213 my $lone_pair = {};
269 88 100       269 if( $has_lone_pair ) {
270 8         28 @chirality_neighbours = ( $chirality_neighbours[0],
271             $lone_pair,
272             @chirality_neighbours[1..$#chirality_neighbours] );
273             }
274              
275 88         312 my $order = $order_by_vertex->($atom);
276              
277             # First, find the initial atom, there can only be one.
278 72         285 my @order_new = map { $order[$_] }
279 0         0 sort { $a <=> $b }
280 338         785 grep { $_ < $order }
281 341         1084 grep { !exists $rings->{$order}{$_} } # ignore ring bonds
282 362         770 grep { defined $_ } # ignore removed H atoms
283 88         469 map { $order_by_vertex->($_) }
  362         708  
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       331 if( $rings->{$order_by_vertex->($atom)} ) {
288 3         13 push @order_new, map { $order[$_] }
289 0         0 sort { $a <=> $b }
290 88         201 keys %{$rings->{$order_by_vertex->($atom)}};
  88         290  
291             }
292             # Finally, all neighbours are added, uniq will remove duplicates
293 341         657 push @order_new, map { $order[$_] }
294 393         799 sort { $a <=> $b }
295 362         884 grep { defined $_ } # ignore removed H atoms
296 88         323 map { $order_by_vertex->($_) }
  362         729  
297             @neighbours;
298 88         852 @order_new = uniq @order_new;
299              
300             # Add unsproutable H atoms
301 88 100       422 if( $options->{unsprout_hydrogens} ) {
302 71 100   210   313 if( any { defined $_ && $_ < $order }
  210 100       897  
303 291         612 map { $order_by_vertex->($_) } @neighbours ) {
304 57         170 splice @order_new, 1, 0, grep { can_unsprout_hydrogen( $graph, $_ ) }
  235         645  
305             @neighbours;
306             } else {
307 14         37 unshift @order_new, grep { can_unsprout_hydrogen( $graph, $_ ) }
  56         143  
308             @neighbours;
309             }
310             }
311             # Add lone pair
312 88 100       410 if( $has_lone_pair ) {
313 8         21 splice @order_new, 1, 0, $lone_pair;
314             }
315              
316 88         401 my @permutation = _array_map( \@chirality_neighbours,
317             \@order_new );
318              
319 88 100       756 if( $atom->{chirality} =~ /^@@?$/ ) {
    100          
    100          
320             # Tetragonal centers
321 69 100       291 if( join( '', _permutation_order( @permutation ) ) ne '0123' ) {
322 12 100       65 $chirality_now = $chirality_now eq '@' ? '@@' : '@';
323             }
324             } elsif( $atom->{chirality} =~ /^\@SP[123]$/ ) {
325             # Square planar centers
326 6         28 $chirality_now = _square_planar_chirality( @permutation, $chirality_now );
327             } elsif( $atom->{chirality} =~ /^\@TB..?$/ ) {
328             # Trigonal bipyramidal centers
329 8         40 $chirality_now = _trigonal_bipyramidal_chirality( @permutation, $chirality_now );
330             } else {
331             # Octahedral centers
332 5         29 $chirality_now = _octahedral_chirality( @permutation, $chirality_now );
333             }
334 88         675 $chirality{$atom} = $chirality_now;
335             }
336              
337             # Write the SMILES
338 245         596 my $component = '';
339 245         2406 my @ring_ids = ( 1..99, 0 );
340 245         916 for my $i (0..$#order) {
341 1236         2469 my $vertex = $order[$i];
342 1236 100       3577 if( $discovered_from{$vertex} ) {
343 995 100 100     17726 if( $options->{explicit_parentheses} ||
344             _has_more_unseen_children( $discovered_from{$vertex}, $i, $order_by_vertex, $graph, $rings ) ) {
345 385         828 $component .= '(';
346             }
347 995         16690 $component .= _depict_bond( $discovered_from{$vertex}, $vertex, $graph, $options );
348             }
349 1236 100       4461 if( $chirality{$vertex} ) {
350             $component .=
351             $write_atom_sub->( $vertex,
352             $graph,
353 88         928 { %$options, chirality => $chirality{$vertex}, raw => 1 } );
354             } else {
355 1148         10543 $component .=
356             $write_atom_sub->( $vertex,
357             $graph,
358             { %$options, chirality => undef } );
359             }
360 1236 100       7359 if( $rings->{$i} ) {
361 233         470 my @rings_closed;
362 233         467 for my $j (sort { $a <=> $b } keys %{$rings->{$i}}) {
  6         29  
  233         1363  
363 154 100       475 if( $i < $j ) {
364 77 50       237 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         344 $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       494 } else {
374             $component .= toggle_cistrans( $rings->{$j}{$i}{bond} ) .
375             ($rings->{$i}{$j}{ring} < 10 ? '' : '%') .
376 77 50       516 $rings->{$j}{$i}{ring};
377 77 100       232 if( $options->{immediately_reuse_ring_numbers} ) {
378             # Ring bond '0' must stay in the end
379 7022 50       13302 @ring_ids = sort { ($a == 0) - ($b == 0) || $a <=> $b }
380 71         563 ($rings->{$j}{$i}{ring}, @ring_ids);
381             } else {
382 6         21 push @rings_closed, $rings->{$j}{$i}{ring};
383             }
384             }
385             }
386 233 100       892 if( !$options->{immediately_reuse_ring_numbers} ) {
387 11 50       54 @ring_ids = sort { ($a == 0) - ($b == 0) || $a <=> $b }
  1084         2317  
388             (@rings_closed, @ring_ids);
389             }
390             }
391 1236 100       4943 my $where = $i < $#order ? $discovered_from{$order[$i+1]} : $order[0];
392 1236         4869 while( $vertex != $where ) {
393 991 100 100     4441 if( $options->{explicit_parentheses} ||
394             _has_more_unseen_children( $discovered_from{$vertex}, $i, $order_by_vertex, $graph, $rings ) ) {
395 385         809 $component .= ')';
396             }
397 991         4796 $vertex = $discovered_from{$vertex};
398             }
399             }
400              
401 245         12160 push @components, $component;
402             }
403              
404 225         2138 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   290 my( $A, $B ) = @_;
414 88 50       353 die "arrays have unequal length\n" unless @$A == @$B;
415              
416 88         184 my @permutation;
417 88         260 for my $element (@$B) {
418 370         952 push @permutation, grep { $A->[$_] eq $element } 0..$#$A;
  1580         7989  
419             }
420 88         304 return @permutation;
421             }
422              
423             sub _depict_atom
424             {
425 1256     1256   2980 my( $vertex, $graph, $options ) = @_;
426 1256 50       3124 $options = {} unless $options;
427             my( $chirality,
428             $raw ) =
429             ( $options->{chirality},
430 1256         3714 $options->{raw} );
431              
432 1256         2866 my $atom = $vertex->{symbol};
433 1256   100     7307 my $is_simple = $atom =~ /^[bcnosp]$/i ||
434             $atom =~ /^(F|Cl|Br|I|\*)$/;
435              
436 1256 100       3450 if( exists $vertex->{isotope} ) {
437 3         9 $atom = $vertex->{isotope} . $atom;
438 3         6 $is_simple = 0;
439             }
440              
441 1256 100       2828 if( exists $options->{chirality} ) {
    50          
442 1236 100       3193 if( $options->{chirality} ) {
443 88         238 $atom .= $options->{chirality};
444 88         196 $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       2979 my $hcount = $vertex->{hcount} ? $vertex->{hcount} : 0;
452 1256 100 100     5349 if( $graph && $options->{unsprout_hydrogens} && $atom ne 'H' ) {
      100        
453 998         11517 $hcount += grep { can_unsprout_hydrogen( $graph, $_ ) }
  2053         139740  
454             $graph->neighbours( $vertex );
455             }
456              
457             # Decide what to do to atoms with usual/unusual valences
458 1256 100 100     12959 if( $is_simple && $graph && !$raw && $normal_valence{ucfirst $atom} &&
      100        
      66        
      100        
      100        
459             !$vertex->{charge} &&
460             !$vertex->{class} ) {
461 429 100       8100 if( _has_unambiguous_normal_valence( $graph, $vertex, $hcount ) ) {
462             # Usual valence detected, no need to keep hcount
463 407 100       1531 $hcount = 0 if $options->{remove_implicit_hydrogens};
464             } else {
465             # Unusual valence detected, need square brackets
466 22         65 $is_simple = 0;
467             }
468             }
469              
470 1256 100       7805 if( $hcount ) { # if non-zero
471 49 100       190 $atom .= 'H' . ($hcount == 1 ? '' : $hcount);
472 49         99 $is_simple = 0;
473             }
474 1256 100 100     4225 $is_simple = 0 if $raw && exists $vertex->{hcount};
475              
476 1256 100       3317 if( $vertex->{charge} ) { # if non-zero
477 12 100       57 $atom .= ($vertex->{charge} > 0 ? '+' : '') . $vertex->{charge};
478 12         92 $atom =~ s/([-+])1$/$1/;
479 12         37 $is_simple = 0;
480             }
481              
482 1256 100       3414 if( $vertex->{class} ) { # if non-zero
483 26         78 $atom .= ':' . $vertex->{class};
484 26         52 $is_simple = 0;
485             }
486              
487 1256 100       5436 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   2929 my( $u, $v, $graph, $options ) = @_;
495              
496 1149         2473 my $n_aromatic = grep { is_aromatic $_ } ( $u, $v );
  2298         7699  
497              
498 1149 100       38873 if( !$graph->has_edge_attribute( $u, $v, 'bond' ) ) {
499 823 100       266122 return $n_aromatic == 2 ? '-' : '';
500             }
501              
502 326         108504 my $bond = $graph->get_edge_attribute( $u, $v, 'bond' );
503 326 50 66     100370 return '' if $bond eq ':' && $n_aromatic && !$options->{explicit_aromatic_bonds};
      66        
504 182 100       1062 return $bond if $u->{number} < $v->{number};
505 24         111 return toggle_cistrans $bond;
506             }
507              
508             sub _has_more_unseen_children
509             {
510 1742     1742   4659 my( $vertex, $i, $order_by_vertex, $graph, $rings ) = @_;
511 4824         11935 my $orders = set( grep { $_ > $i }
512 5254         11313 grep { defined $_ } # ignore removed H atoms
513 1742         5904 map { $order_by_vertex->($_) }
  5254         239320  
514             $graph->neighbours( $vertex ) );
515 1742 100 100     34873 if( defined $order_by_vertex->($vertex) &&
516             $rings->{$order_by_vertex->($vertex)} ) {
517 654         1159 $orders->remove( keys %{$rings->{$order_by_vertex->($vertex)}} );
  654         1447  
518             }
519 1742         15367 return $orders->size;
520             }
521              
522             sub _has_unambiguous_normal_valence
523             {
524 429     429   1088 my( $graph, $vertex, $hcount ) = @_;
525 429         984 my $element = ucfirst $vertex->{symbol};
526 429 50       1181 return '' unless exists $normal_valence{$element};
527              
528 429         1638 my $valence = valence( $graph, $vertex );
529 429     430   2922 my $normal_valence = first { $_ == $valence } @{$normal_valence{$element}};
  430         1061  
  429         2014  
530 429 100       2114 return '' unless $normal_valence;
531              
532 408     408   1031 my $derived_valence = first { $_ >= $normal_valence - $hcount }
533 408         1269 @{$normal_valence{$element}};
  408         1157  
534 408   66     2999 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   311342 if( (scalar @_ != 4) ||
      66        
545 367   100 367   2496 (any { !defined || !/^[0-3]$/ } @_) ||
546             (join( ',', sort @_ ) ne '0,1,2,3') ) {
547 5         64 warn '_permutation_order() accepts only permutations of numbers ' .
548             "'0', '1', '2' and '3', unexpected input received";
549 5         95 return 0..3; # Return original order
550             }
551              
552 89   100     658 while( $_[2] == 0 || $_[3] == 0 ) {
553 56         322 @_ = ( $_[0], @_[2..3], $_[1] );
554             }
555 89 100       282 if( $_[0] != 0 ) {
556 42         167 @_ = ( @_[1..2], $_[0], $_[3] );
557             }
558 89         291 while( $_[1] != 1 ) {
559 55         228 @_[1..3] = ( @_[2..3], $_[1] );
560             }
561 89         511 return @_;
562             }
563              
564             sub _square_planar_chirality
565             {
566 6     6   15 my $chirality = pop @_;
567 6         20 my @source = 0..3;
568 6         17 my @target = @_;
569              
570 6 50       54 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         18 while( $source[0] != $target[0] ) {
577 9         20 push @source, shift @source;
578 9         35 my %tab = ( '@SP1' => '@SP1', '@SP2' => '@SP3', '@SP3' => '@SP2' );
579 9         35 $chirality = $tab{$chirality};
580             }
581              
582 6 100       18 if( $source[3] == $target[1] ) { # Swap the right side
583 3         10 ( $source[2], $source[3] ) = ( $source[3], $source[2] );
584 3         12 my %tab = ( '@SP1' => '@SP3', '@SP2' => '@SP2', '@SP3' => '@SP1' );
585 3         10 $chirality = $tab{$chirality};
586             }
587              
588 6 100       59 if( $source[2] == $target[1] ) { # Swap the center
589 3         9 ( $source[1], $source[2] ) = ( $source[2], $source[1] );
590 3         12 my %tab = ( '@SP1' => '@SP2', '@SP2' => '@SP1', '@SP3' => '@SP3' );
591 3         9 $chirality = $tab{$chirality};
592             }
593              
594 6 100       18 if( $source[3] == $target[2] ) { # Swap the right side
595 3         9 ( $source[2], $source[3] ) = ( $source[3], $source[2] );
596 3         11 my %tab = ( '@SP1' => '@SP3', '@SP2' => '@SP2', '@SP3' => '@SP1' );
597 3         10 $chirality = $tab{$chirality};
598             }
599              
600 6         22 return $chirality;
601             }
602              
603             sub _trigonal_bipyramidal_chirality
604             {
605 4810     4810   2934667 my $chirality = pop @_;
606 4810         12501 my @target = @_;
607              
608 4810 50       35134 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         19172 $chirality =~ s/^\@TB//;
614 4810         11745 $chirality = int $chirality;
615              
616 4810         14226 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         8064 my @axis = map { $_ - 1 } @{$TB->{axis}};
  9620         24394  
  4810         12382  
621 4810 100       10875 my @sides = grep { $_ != $axis[0] && $_ != $axis[1] } 0..4;
  24050         82204  
622              
623             # Find the new location of the axis, remove it from @target
624 14425     14425   36957 my @axis_location = ( ( first { $target[$_] == $axis[0] } 0..4 ),
625 4810     14445   30277 ( first { $target[$_] == $axis[1] } 0..4 ) );
  14445         25773  
626 4810 100       25268 @target = grep { $_ != $axis[0] && $_ != $axis[1] } @target;
  24050         84685  
627              
628             # Invert the axis if needed
629 4810 100       12117 if( $axis_location[0] > $axis_location[1] ) {
630 2402         6217 @axis_location = reverse @axis_location;
631 2402         4123 @target = reverse @target;
632             }
633              
634             # Cycle the sides clockwise until the first is aligned
635 4810         13920 while( $sides[0] != $target[0] ) {
636 4807         14308 push @sides, shift @sides;
637             }
638 4810         10452 my $order = $TB->{order};
639 4810 100       13862 $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   179313 $TB[$_]->{axis}[1] == $axis_location[1] + 1 }
644 4810         28803 0..$#TB;
645 4810         38925 return '@TB' . $chirality;
646             }
647              
648             sub _octahedral_chirality
649             {
650 43205     43205   20847085 my $chirality = pop @_;
651 43205         119133 my @target = @_;
652              
653 43205 50       360885 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         188435 $chirality =~ s/^\@OH//;
659 43205         123600 $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         77767 my @axis = map { $_ - 1 } @{$OH[$chirality-1]->{axis}};
  86410         207455  
  43205         156134  
664 43205 100       101976 my @sides = grep { $_ != $axis[0] && $_ != $axis[1] } 0..5;
  259230         874833  
665              
666 43205 100       157999 if( $OH[$chirality-1]->{shape} eq 'Z' ) {
667 14401         36447 ( $sides[2], $sides[3] ) = ( $sides[3], $sides[2] );
668             }
669              
670 43205 100       128183 if( $OH[$chirality-1]->{shape} eq '4' ) {
671 14401         38244 ( $sides[0], $sides[3] ) = ( $sides[3], $sides[0] );
672             }
673              
674             # Adjust for enumeration direction
675 43205 100       131288 @sides = reverse @sides if $OH[$chirality-1]->{order} eq '@';
676              
677             # Align the axis start
678 43205 100       143663 if( $axis[0] == $target[0] ) { # same axis start, do nothing
    100          
679             } elsif( $axis[1] == $target[0] ) { # axis inversion
680 7201         19272 @axis = reverse @axis;
681 7201         19023 @sides = reverse @sides;
682             } else { # axis start at one of the sides
683 28803     72010   203302 my $axis_index = first { $sides[$_] == $target[0] } 0..3;
  72010         142074  
684 28803         140904 my @axis_now = ( $sides[$axis_index], $sides[($axis_index + 2) % 4] );
685 28803         77486 ( $sides[$axis_index], $sides[($axis_index + 2) % 4] ) = reverse @axis;
686 28803         73951 @axis = @axis_now;
687             }
688              
689 43205         69823 shift @target; # axis start is no longer needed
690 43205     129619   221209 my $axis_end = first { $target[$_] == $axis[1] } 0..4;
  129619         233807  
691 43205         152717 @target = map { $target[$_] } grep { $_ != $axis_end } 0..4; # remove axis end
  172820         394849  
  216025         420449  
692              
693             # Cycle the sides clockwise until the first is aligned
694 43205         153944 while( $sides[0] != $target[0] ) {
695 64810         160164 push @sides, shift @sides;
696             }
697 43205         83726 shift @sides;
698 43205         78456 shift @target;
699              
700             # Check the alignment of the other sides to find the shape and order
701 43205         92385 my $shape;
702             my $order;
703 43205 100 100     323735 if( $target[0] == $sides[0] && $target[1] == $sides[1] ) {
    100 66        
    100 100        
    100 66        
    100 66        
    50 33        
704 7201         21124 ( $shape, $order ) = ( 'U', '@@' );
705             } elsif( $target[0] == $sides[0] && $target[1] == $sides[2] ) {
706 7201         18639 ( $shape, $order ) = ( 'Z', '@@' );
707             } elsif( $target[0] == $sides[1] && $target[1] == $sides[0] ) {
708 7200         18362 ( $shape, $order ) = ( '4', '@' );
709             } elsif( $target[0] == $sides[1] && $target[1] == $sides[2] ) {
710 7202         19895 ( $shape, $order ) = ( '4', '@@' );
711             } elsif( $target[0] == $sides[2] && $target[1] == $sides[0] ) {
712 7200         26246 ( $shape, $order ) = ( 'Z', '@' );
713             } elsif( $target[0] == $sides[2] && $target[1] == $sides[1] ) {
714 7201         17540 ( $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   2091014 $OH[$_]->{axis}[1] == $axis_end + 2 }
721 43205         275587 0..$#OH;
722 43205         415105 return '@OH' . $chirality;
723             }
724              
725             sub _order
726             {
727 1055     1055   10986 my( $vertices ) = @_;
728 1055         4718 my @sorted = sort { $vertices->{$a}{number} <=>
729 2856         7525 $vertices->{$b}{number} } keys %$vertices;
730 1055         4190 return $vertices->{shift @sorted};
731             }
732              
733             1;
734              
735             =head1 AUTHORS
736              
737             Andrius Merkys, Emerkys@cpan.orgE
738              
739             =cut