File Coverage

blib/lib/Chemistry/Mok.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Chemistry::Mok;
2              
3             $VERSION = '0.25';
4             # $Id: Mok.pm,v 1.10 2005/05/16 21:54:21 itubert Exp $
5              
6 1     1   100489 use strict;
  1         4  
  1         53  
7 1     1   6 use warnings;
  1         2  
  1         40  
8 1     1   2174 use Chemistry::Mol;
  1         55682  
  1         83  
9 1     1   1362 use Chemistry::File ':auto';
  1         19456  
  1         8  
10 1     1   74412 use Chemistry::Pattern;
  1         3  
  1         68  
11 1     1   6 use Chemistry::Bond::Find qw(find_bonds assign_bond_orders);
  1         2  
  1         66  
12 1     1   1157 use Chemistry::Ring 'aromatize_mol';
  1         7809  
  1         80  
13 1     1   2232 use Chemistry::3DBuilder 'build_3d';
  0            
  0            
14             use Text::Balanced ':ALL';
15             use Scalar::Util 'blessed';
16             use Data::Dumper;
17             use Carp;
18              
19             our $DEBUG = 0;
20              
21             =head1 NAME
22              
23             Chemistry::Mok - molecular awk interpreter
24              
25             =head1 SYNOPSIS
26              
27             use Chemistry::Mok;
28             $code = '/CS/g{ $n++; $l += $match->bond_map(0)->length }
29             END { printf "Average C-S bond length: %.3f\n", $l/$n; }';
30              
31             my $mok = Chemistry::Mok->new($code);
32             $mok->run({ format => mdlmol }, glob("*.mol"));
33              
34             =head1 DESCRIPTION
35              
36             This module is the engine behind the mok program. See mok(1) for a detailed
37             description of the language. Mok is part of the PerlMol project,
38             L.
39              
40             =head1 METHODS
41              
42             =over
43              
44             =cut
45              
46             sub tokenize {
47             my ($self, $code) = @_;
48              
49             $code =~ s/\s*$//; # Text::Balanced complains about trailing whitespace
50             #$code =~ s/^\s*#.*//g; # remove comments at the top of the file
51             #unless($code =~ /^\s*([\/{#]|sub|BEGIN|END)/) {
52             unless($code =~ /^(\s*#.*)*\s*([\/{]|sub|BEGIN|END|\w+:\s*\/)/) {
53             print "MOK: adding implicit braces\n" if $DEBUG;
54             $code = "{$code}"; # add implicit brackets for simple one-liners
55             }
56             #print "code = '$code'\n";
57             # (patt opt?)? code | sub code
58             my @toks = extract_multiple(my $c = $code,
59             [
60             { 'Chemistry::Mok::Comment' =>
61             qr/\s*#.*\s*/ },
62             { 'Chemistry::Mok::Patt' =>
63             sub { scalar extract_delimited($_[0],'/') } },
64             { 'Chemistry::Mok::Sub' =>
65             qr/\s*(?:END|BEGIN|sub\s+\w+)\s*/ },
66             { 'Chemistry::Mok::Block' =>
67             sub { scalar extract_codeblock($_[0],'{') } },
68             { 'Chemistry::Mok::PattLang' =>
69             qr/(\s*\w+):(?=\s*\/)/ },
70             { 'Chemistry::Mok::Opts' =>
71             qr/[gopGOP]+/ },
72             ],
73             );
74             die "Mok: error extracting: $@" if $@;
75             print "MOK: TOKENS:\n", Dumper(\@toks), "\nCODE:<<<<$code>>>>\n\n"
76             if $DEBUG;
77             @toks;
78             }
79              
80             sub parse {
81             my ($self, @toks) = @_;
82              
83             my (@subs, @blocks);
84             for my $tok (@toks) {
85             blessed $tok or die "unparsable token '$tok'\n";
86             }
87              
88             ### new parser
89              
90             my $st = 1;
91             my ($patt, $opts, $block, $sub, $pattlang) = ('') x 5;
92             my ($save) = 0;
93             my $line;
94             my $next_line = 1;
95             while (my $tok = shift @toks) {
96             $line = $next_line;
97             $next_line += $$tok =~ y/\n//;
98             print "MOK: LINE=$line;\nTOK=<<<<$$tok>>>>;\nNEXT_LINE=$next_line\n\n"
99             if $DEBUG;
100             next if $tok->isa("Chemistry::Mok::Comment");
101             if ($st == 1) {
102             if ($tok->isa("Chemistry::Mok::Block")){
103             $block = $$tok, $save = 1;
104             } elsif ($tok->isa("Chemistry::Mok::Sub")) {
105             $sub = $$tok, $st = 5, next;
106             } elsif ($tok->isa("Chemistry::Mok::PattLang")) {
107             $pattlang = $$tok, $st = 4, next;
108             } elsif ($tok->isa("Chemistry::Mok::Patt")) {
109             $patt = $$tok, $st = 2, next;
110             }
111             } elsif ($st == 2) {
112             if ($tok->isa("Chemistry::Mok::Block")){
113             $block = $$tok, $save = 1;
114             } elsif ($tok->isa("Chemistry::Mok::Opts")){
115             $opts = $$tok, $st = 3, next;
116             }
117             } elsif ($st == 3) {
118             if ($tok->isa("Chemistry::Mok::Block")){
119             $block = $$tok, $save = 1;
120             }
121             } elsif ($st == 4) {
122             if ($tok->isa("Chemistry::Mok::Patt")){
123             $patt = $$tok, $st = 2, next;
124             }
125             } elsif ($st == 5) {
126             if ($tok->isa("Chemistry::Mok::Block")){
127             $block = $$tok, $save = 1;
128             }
129             } else {
130             confess "unknown state '$st'";
131             }
132             if ($save) { # save block and go back to state 1
133             if ($sub) {
134             push @subs, { block => "$sub $$tok", line => $line };
135             } else {
136             push @blocks, { patt => $patt, opts => $opts,
137             pattlang => $pattlang, block => $$tok,
138             line => $line};
139             }
140             $patt = $opts = $pattlang = $block = $sub = '';
141             $st = 1, $save = 0, next;
142             } else {
143             die "unexpected token '$$tok' (type '" . ref($tok) . "'\n";
144             }
145             }
146             print "MOK: BLOCKS\n", Dumper(\@blocks), "\nSUBS:\n", Dumper(\@subs), "\n"
147             if $DEBUG;
148              
149             \@subs, \@blocks;
150             }
151              
152             sub compile_subs {
153             my ($self, @subs) = @_;
154             my $pack = $self->{package};
155              
156             for my $sub (@subs) {
157             my $code = <
158             package Chemistry::Mok::UserCode::$pack;
159             no strict;
160             no warnings;
161             #line $sub->{line} "mok code"
162             $sub->{block}
163             END
164             print "MOK: COMPILING SUB: <<<<$code>>>>\n\n" if $DEBUG;
165             eval $code;
166             die "Mok: error compiling sub: $@" if $@;
167             }
168             }
169              
170             sub compile_blocks {
171             my ($self, @blocks) = @_;
172             my $pack = $self->{package};
173             my $format = $self->{pattern_format};
174             my @compiled_blocks;
175              
176             for my $block (@blocks) {
177             #use Data::Dumper; print Dumper $block;
178             my $code = <
179             package Chemistry::Mok::UserCode::$pack;
180             no strict;
181             no warnings;
182             sub {
183             my (\$mol, \$file, \$match, \$patt) = \@_;
184             my (\$MOL, \$FILE, \$MATCH, \$PATT, \$FH) = \@_;
185             my (\@A) = \$MATCH ? \$MATCH->atom_map : \$MOL->atoms;
186             my (\@B) = \$MATCH ? \$MATCH->bond_map : \$MOL->bonds;
187             #line $block->{line} "mok code"
188             $block->{block};
189             }
190             END
191             print "MOK: COMPILING BLOCK: <<<<$code>>>>\n\n" if $DEBUG;
192             my $sub = eval $code;
193             die "Mol: Error compiling block: $@" if $@;
194              
195             my ($patt, $patt_str);
196             if ($block->{patt}) {
197             $block->{patt} =~ m#^/(.*)/$#;
198             $patt_str = $1;
199             $patt = Chemistry::Pattern->parse($patt_str,
200             format => $block->{pattlang} || $format);
201             $patt->attr(global => 1) if $block->{opts} =~ /g/;
202             $patt->options(overlap => 0) if $block->{opts} =~ /O/;
203             $patt->options(permute => 1) if $block->{opts} =~ /p/;
204             }
205             push @compiled_blocks, {'sub' => $sub,
206             patt => $patt, patt_str => $patt_str};
207             }
208             \@compiled_blocks;
209             }
210              
211             =item Chemistry::Mok->new($code, %options)
212              
213             Compile the code and return a Chemistry::Mok object. Available options:
214              
215             =over
216              
217             =item C
218              
219             If the C option is given, the code runs in the
220             Chemistry::Mok::UserCode::$options{package} package instead of the
221             Chemistry::Mok::UserCode::Default package. Specifying a package name is
222             recommended if you have more than one mok object and you are using global
223             varaibles, in order to avoid namespace clashes.
224              
225             =item C
226              
227             The name of the format which will be used for parsing slash-delimited patterns
228             that don't define an explicit format. Mok versions until 0.16 only used the
229             'smiles' format, but newer versions can use other formats such as 'smarts',
230             'midas', 'formula_pattern', and 'sln', if available. The default is 'smarts'.
231              
232             =back
233              
234             =cut
235              
236             sub new {
237             my ($class, $code, @a) = @_;
238             my %opts;
239              
240             # for backwards compatibility with Chemistry::Mok->new($code, $package)
241             unshift @a, "package" if (@a == 1);
242             %opts = @a;
243            
244             my $self = bless {
245             'package' => $opts{package} || "Default",
246             pattern_format => $opts{pattern_format} || "smarts",
247             }, $class;
248              
249             $self->setup_package;
250             my @toks = $self->tokenize($code);
251             my ($subs, $blocks) = $self->parse(@toks);
252             $self->compile_subs(@$subs);
253             $self->{blocks} = $self->compile_blocks(@$blocks);
254            
255             return $self;
256             }
257              
258             sub setup_package {
259             my ($self) = @_;
260             my $usr_pack = $self->{package};
261             # import convenience functions into the user's namespace
262             eval <
263             package Chemistry::Mok::UserCode::$usr_pack;
264             use Chemistry::Atom ':all';
265             use Chemistry::Ring ':all';
266             use Chemistry::Ring::Find ':all';
267             use Chemistry::Bond::Find ':all';
268             use Chemistry::Canonicalize ':all';
269             use Chemistry::InternalCoords::Builder ':all';
270             use Chemistry::Isotope ':all';
271             use Math::VectorReal ':all';
272             use Chemistry::3DBuilder ':all';
273             sub println { print "\@_", "\n" }
274             EVAL
275             die "Mok: error setting up 'Chemistry::Mok::UserCode::$usr_pack' $@" if $@;
276             }
277              
278             =item $mok->run($options, @args)
279              
280             Run the code on the filenames contained in @args. $options is a hash reference
281             with runtime options. Available options:
282              
283             =over
284              
285             =item build_3d
286              
287             Generate 3D coordinates using Chemistry::3DBuilder.
288              
289             =item aromatize
290              
291             "Aromatize" each molecule as it is read. This is needed for example for
292             matching SMARTS patterns that use aromaticity or ring primitives.
293              
294             =item delete_dummies
295              
296             Delete dummy atoms after reading each molecule. A dummy atom is defined as an
297             atom with an unknown symbol (i.e., it doesn't appear on the periodic table), or
298             an atomic number of zero.
299              
300             =item find_bonds
301              
302             If set to a true value, find bonds. Use it when reading files with no bond
303             information but 3D coordinates to detect the bonds if needed (for example, if
304             you want to do match a pattern that includes bonds). If the file has explicit
305             bonds, mok will not try to find the bonds, but it will reassign the bond orders
306             from scratch.
307              
308             =item format
309              
310             The format used when calling $mol_class->read. If not given, $mol_class->read
311             tries to identify the format automatically.
312              
313             =item mol_class
314              
315             The molecule class used for reading the files. Defaults to Chemistry::Mol.
316              
317             =back
318              
319             =cut
320              
321             sub run {
322             my ($self, $opt, @args) = @_;
323             # MAIN LOOP
324             my $mol_class = $opt->{mol_class} || "Chemistry::Mol";
325             FILE: for my $file (@args) {
326             #my (@mols) = $mol_class->read(
327             my %reader_opts = (
328             format => $opt->{format},
329             mol_class => $opt->{mol_class},
330             );
331             my $reader = $mol_class->file(
332             $file,
333             %reader_opts,
334             );
335             $reader->open('<');
336             $reader->read_header;
337             while (my @mols = $reader->read_mol($reader->fh, %reader_opts)) {
338             MOL: for my $mol (@mols) {
339             if ($opt->{delete_dummies}) {
340             $_->delete for grep { ! $_->Z } $mol->atoms;
341             }
342             if ($opt->{find_bonds}) {
343             find_bonds($mol) unless $mol->bonds;
344             assign_bond_orders($mol);
345             }
346             if ($opt->{aromatize}) {
347             aromatize_mol($mol);
348             }
349             if ($opt->{build_3d}) {
350             build_3d($mol);
351             }
352             BLOCK: for my $block (@{$self->{blocks}}) {
353             my ($code_block, $patt, $patt_str) =
354             @{$block}{qw(sub patt patt_str)};
355             if ($patt) {
356             MATCH: while ($patt->match($mol)) {
357             $code_block->($mol, $file, $patt,
358             $patt_str, $reader->fh);
359             last unless $patt->attr('global');
360             }
361             } else {
362             $code_block->($mol, $file, $patt,
363             $patt_str, $reader->fh);
364             }
365             }
366             }
367             }
368             }
369             }
370              
371             1;
372              
373             __END__