File Coverage

blib/lib/Chemistry/OpenSMILES/Aromaticity.pm
Criterion Covered Total %
statement 81 83 97.5
branch 16 26 61.5
condition 16 21 76.1
subroutine 17 17 100.0
pod 3 3 100.0
total 133 150 88.6


line stmt bran cond sub pod time code
1             package Chemistry::OpenSMILES::Aromaticity;
2              
3 2     2   1693 use strict;
  2         6  
  2         82  
4 2     2   10 use warnings;
  2         9  
  2         161  
5              
6             # ABSTRACT: Aromaticity handling routines
7             our $VERSION = '0.12.3'; # VERSION
8              
9 2         246 use Chemistry::OpenSMILES qw(
10             is_aromatic
11             is_aromatic_bond
12             is_double_bond
13             is_single_bond
14 2     2   11 );
  2         4  
15 2     2   1081 use Graph::Traversal::DFS;
  2         1029  
  2         81  
16 2     2   13 use List::Util qw( all first );
  2         5  
  2         3209  
17              
18             =head1 NAME
19              
20             Chemistry::OpenSMILES::Aromaticity - Aromaticity handling routines
21              
22             =head1 DESCRIPTION
23              
24             Chemistry::OpenSMILES::Aromaticity encodes some aromaticity handling
25             subroutines for aromatisation and kekulisation. Both implementations are
26             experimental, handle only some specific cases and are neither stable nor
27             bug-free, thus should be used with caution.
28              
29             =cut
30              
31             require Exporter;
32             our @ISA = qw( Exporter );
33             our @EXPORT_OK = qw(
34             aromatise
35             electron_cycles
36             kekulise
37             );
38              
39             =head1 METHODS
40              
41             =over 4
42              
43             =item aromatise( $moiety )
44              
45             Mark electron cycles as aromatic.
46              
47             =cut
48              
49             sub aromatise
50             {
51 2     2 1 13 my( $moiety ) = @_;
52              
53 2         16 my @electron_cycles = electron_cycles( $moiety );
54 2         5 for my $cycle (@electron_cycles) {
55 3         10 for my $i (0..$#$cycle) {
56             # Set bond to aromatic
57 18         455 $moiety->set_edge_attribute( $cycle->[$i],
58             $cycle->[($i + 1) % scalar @$cycle],
59             'bond',
60             ':' );
61             # Set atom to aromatic
62 18 50       5445 if( $cycle->[$i]{symbol} =~ /^([BCNOPS]|Se|As)$/ ) {
63 18         63 $cycle->[$i]{symbol} = lcfirst $cycle->[$i]{symbol};
64             }
65             }
66             }
67             }
68              
69             =item kekulise( $moiety, $order_sub )
70              
71             Find nonfused even-length aromatic cycles consisting only of B, C, N, P, S and mark them with alternating single and double bonds.
72             Subroutine as well accepts an optional subroutine reference C<$order_sub>, providing external order for atoms.
73             This is needed to stabilise the algorithm, as otherwise the outcomes of bond assignment may turn out different.
74             C<$order_sub> is called with an atom as C<$_[0]> and is expected to return a value providing a distinct order indication for every atom.
75             These can be any scalar values, comparable using Perl's C operator.
76             If C<$order_sub> is not given, initial atom order in input is consulted.
77              
78             =cut
79              
80             sub kekulise
81             {
82 2     2 1 1982 my( $moiety, $order_sub ) = @_;
83              
84 2 50   62   14 $order_sub = sub { $_[0]->{number} } unless $order_sub;
  62         133  
85              
86 2         16 my $aromatic_only = $moiety->copy_graph;
87 2         6195 $aromatic_only->delete_vertices( grep { !is_aromatic $_ }
  34         150  
88             $aromatic_only->vertices );
89 1         701 $aromatic_only->delete_edges( map { @$_ }
90 2         3993 grep { !is_aromatic_bond( $moiety, @$_ ) }
  19         11262  
91             $aromatic_only->edges );
92              
93 2         608 for my $component ($aromatic_only->connected_components) {
94             # Taking only simple even-length cycles into consideration
95 3 50   18   11327 next unless all { $aromatic_only->degree( $_ ) == 2 } @$component;
  18         7372  
96 3 50   18   1470 next unless all { $moiety->degree( $_ ) <= 3 } @$component;
  18         10149  
97 3 50   18   2155 next unless all { $_->{symbol} =~ /^[BCNPS]$/i } @$component;
  18         60  
98 3 50       15 next if @$component % 2;
99              
100 3         19 my( $first ) = sort { $order_sub->($a) cmp $order_sub->($b) }
  28         53  
101             @$component;
102 3         14 my( $second ) = sort { $order_sub->($a) cmp $order_sub->($b) }
  3         358  
103             $aromatic_only->neighbours( $first );
104 3         12 for my $i (0..$#$component) {
105 18         55 $first->{symbol} = ucfirst $first->{symbol};
106 18 100       70 if( $i % 2 ) {
107 9         274 $moiety->set_edge_attribute( $first, $second, 'bond', '=' );
108             } else {
109 9         300 $moiety->delete_edge_attribute( $first, $second, 'bond' );
110             }
111             ( $first, $second ) =
112 18     30   6299 ( $second, first { $_ ne $first } $aromatic_only->neighbours( $second ) );
  30         2400  
113             }
114             }
115             }
116              
117             =item electron_cycles( $moiety )
118              
119             Find electron cycles according to "Finding Electron Cycles" algorithm from
120             L.
121             Use with caution: the implementation is experimental.
122              
123             =cut
124              
125             sub electron_cycles
126             {
127 2     2 1 4 my( $moiety ) = @_;
128              
129 2         3 my @cycles;
130 2         7 for my $start ($moiety->vertices) {
131 34         7617 my %seen;
132             my %prev;
133             my $operations = {
134 34     34   3792 start => sub { $start },
135 628     628   23515 pre => sub { $seen{$_[0]} = 1 },
136             pre_edge => sub {
137 594     594   322983 my( $u, $v ) = @_;
138 594 50       1914 ( $u, $v ) = ( $v, $u ) if $seen{$v};
139 594         1692 $prev{$v} = $u;
140             },
141             non_tree_edge => sub {
142 112     112   95028 my( $u, $v ) = @_;
143 112 100 100     965 if( $u == $start || $v == $start ) {
144 36 100       104 ( $u, $v ) = ( $v, $u ) if $v == $start;
145 36         61 my $current = $v;
146 36         71 my $prev_bond_is_single;
147 36         71 my $cycle_is_alterating = 1;
148 36         81 my @cycle = ( $u );
149 36         102 while( $prev{$current} ) {
150 180 50 66     1057 if( ( !defined $prev_bond_is_single && (
      100        
      66        
      100        
      33        
      66        
151             is_single_bond( $moiety, $current, $prev{$current} ) ||
152             is_double_bond( $moiety, $current, $prev{$current} ) ) ) ||
153             ( $prev_bond_is_single && is_double_bond( $moiety, $current, $prev{$current} ) ) ||
154             ( !$prev_bond_is_single && is_single_bond( $moiety, $current, $prev{$current} ) ) ) {
155             # Logic is inverted here as $prev_bond_is_single is
156             # inverted after the conditional.
157 180         69630 $prev_bond_is_single = !is_single_bond( $moiety, $current, $prev{$current} );
158 180         65246 push @cycle, $current;
159 180         445 $current = $prev{$current};
160             } else {
161 0         0 $cycle_is_alterating = 0;
162 0         0 last;
163             }
164 180 50       428 last unless $cycle_is_alterating;
165 180         522 $prev_bond_is_single = 1 - $prev_bond_is_single;
166             }
167 36 50       157 push @cycles, \@cycle if $cycle_is_alterating;
168             }
169             },
170 34         417 };
171              
172 34         224 my $traversal = Graph::Traversal::DFS->new( $moiety, %$operations );
173 34         12753 $traversal->dfs;
174             }
175              
176 2         841 my %unique;
177 2         5 for (@cycles) {
178 36         269 $unique{join '', sort @$_} = $_;
179             }
180 2         23 return values %unique;
181             }
182              
183             =back
184              
185             =cut
186              
187             1;