File Coverage

blib/lib/Chemistry/3DBuilder.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Chemistry::3DBuilder;
2             # $Id: 3DBuilder.pm,v 1.2 2005/05/06 21:34:46 itubert Exp $
3              
4             $VERSION = '0.10';
5              
6 1     1   40355 use strict;
  1         2  
  1         40  
7 1     1   5 use warnings;
  1         2  
  1         29  
8 1     1   1088 use Chemistry::Ring;
  1         76127  
  1         47  
9 1     1   2013 use Chemistry::InternalCoords;
  0            
  0            
10             use List::Util qw(first);
11              
12             use base qw(Exporter);
13              
14             our @EXPORT_OK = qw(build_3d);
15             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
16              
17             our $DEBUG = 0;
18              
19             =head1 NAME
20              
21             Chemistry::3DBuilder - Generate 3D coordinates from a connection table
22              
23             =head1 SYNOPSIS
24              
25             # example: convert SMILES to MDL molfile
26             use Chemistry::3DBuilder qw(build_3d);
27             use Chemistry::File::SMILES;
28             use Chemistry::File::MDLMol;
29              
30             my $s = '[O-]C(=O)C(N)C(C)CC';
31             my $mol = Chemistry::Mol->parse($s, format => 'smiles');
32              
33             build_3d($mol);
34              
35             print $mol->print(format => 'mdl');
36              
37             =head1 DESCRIPTION
38              
39             This module generates a three-dimensional molecular structure from a connection
40             table, such as that obtained by a 2D representation of the molecule or from a
41             SMILES string.
42              
43             B: this module is still at a very early stage of development so it has
44             important limitations. 1) It doesn't handle rings or stereochemistry yet! 2)
45             The bond lengths and atoms are very approximate as they don't really account
46             for different elements. 3) Only the sp3, sp2, and sp hybridizations are
47             supported.
48              
49             =head1 SUBROUTINES
50              
51             These subroutines may be exported; to export all, use the ':all' tag.
52              
53             =over
54              
55             =item build_3d($mol)
56              
57             Add internal and cartesian coordinates to the molecule C<$mol>.
58              
59             =cut
60              
61             # POSSIBLE PROBLEMS:
62             # allenes
63             # stereochemistry
64             # (E)-esters and acids
65              
66             sub build_3d {
67             my ($mol) = @_;
68              
69             # prepare molecule
70             $mol->collapse_hydrogens;
71             $_->{internal_coords} = undef for $mol->atoms;
72             Chemistry::Ring::aromatize_mol($mol);
73              
74             # check for rings
75             my $rings = $mol->attr('ring/rings');
76             if (@$rings) {
77             warn "mol has " . @$rings . " rings\n";
78             return;
79             }
80              
81             my $user = { n => 0 };
82             build_chain($mol, $user);
83              
84             #print "sprout_hydrogens\n" if $DEBUG;
85             # add the hydrogens
86             $mol->sprout_hydrogens;
87             for my $h (
88             grep { $_->symbol eq 'H' and ! $_->internal_coords } $mol->atoms
89             ) {
90             my ($bond) = $h->bonds;
91             my ($nei) = $h->neighbors;
92             add_atom(atom => $h, bond => $bond, from => $nei, user => $user);
93             }
94             #warn "IC($_): " . $_->internal_coords for $mol->atoms;
95              
96             # calculate the cartesian coordinates
97             $_->internal_coords->add_cartesians for $mol->atoms;
98             }
99              
100             sub build_chain {
101             my ($mol, $user) = @_;
102             dfs($mol, on_atom => \&add_atom, user => $user);
103             }
104              
105             sub bond_length {
106             my ($bond) = @_;
107             my (@atoms) = $bond->atoms;
108             if (grep { $_->symbol eq 'H' } @atoms) {
109             return 1.1;
110             } else {
111             return 1.5 - ($bond->order - 1) * 0.15;
112             }
113             }
114              
115             sub choose_bond {
116             my ($atom, $bond, $from) = @_;
117             ($from, bond_length($bond));
118             }
119              
120             my %angle_table = (
121             sp => 180,
122             sp2 => 120,
123             sp3 => 109.47,
124             );
125              
126             sub bond_angle {
127             my ($atom1, $atom2, $atom3) = @_;
128             my $hyb = atom_hybridization($atom2);
129             #warn "hyb angle($atom)=$hyb\n";
130             $angle_table{$hyb} || 109.47;
131             }
132              
133             sub atom_hybridization {
134             no warnings qw(uninitialized);
135             my ($atom) = @_;
136             my %ord_hist;
137             $ord_hist{$_}++ for map { $_->order } $atom->bonds;
138             my $deg = $atom->bonds + $atom->implicit_hydrogens;
139             if ($deg == 2 and ($ord_hist{3} or $ord_hist{2} == 2)) {
140             return "sp";
141             } elsif ($deg == 3 and $ord_hist{2} == 1) {
142             return "sp2";
143             } else {
144             return "sp3";
145             }
146             }
147              
148             sub bond_dihedral {
149             my ($atom, $from, $ang_ref) = @_;
150             180;
151             }
152              
153             sub choose_angle {
154             my ($atom, $bond, $from) = @_;
155             my ($len_ref, $len_val) = choose_bond($atom, $bond, $from);
156             my ($ang_ref) = first { $_->internal_coords } $from->neighbors($atom);
157             ($len_ref, $len_val, $ang_ref, bond_angle($atom, $from, $ang_ref));
158             }
159              
160             sub choose_dihedral {
161             my ($atom, $bond, $from) = @_;
162             my ($len_ref, $len_val, $ang_ref, $ang_val) =
163             choose_angle($atom, $bond, $from);
164             my ($dih_ref, $dih_val);
165             my $hyb = atom_hybridization($len_ref);
166             my $fd;
167            
168             #warn "hyb($len_ref)=$hyb\n";
169             if ($fd = $len_ref->attr("3dbuilder/first_dihedral")) {
170             $fd = $atom->parent->by_id($fd); # XXX
171             $dih_ref = $fd;
172             ($ang_ref, $ang_val) = $fd->internal_coords->angle;
173             $dih_val = $hyb eq 'sp2' ? 180 :
174             $len_ref->attr("3dbuilder/second_dihedral") ?
175             240 : 120;
176             $len_ref->attr("3dbuilder/second_dihedral", $atom->id);
177             } elsif ($fd = $len_ref->attr("3dbuilder/first_dihedral_ang")) {
178             $fd = $atom->parent->by_id($fd); # XXX
179             ($dih_ref) = $fd->internal_coords->dihedral;
180             ($ang_ref) = $fd->internal_coords->distance;
181             $dih_val = $hyb eq 'sp2' ? 180 :
182             $len_ref->attr("3dbuilder/second_dihedral_ang") ?
183             240 : 120;
184             $len_ref->attr("3dbuilder/second_dihedral_ang", $atom->id);
185             } else {
186             # for fd(first_dihedralang): ang_ref = ok; dih_ref = dih_ref(fd)
187             $dih_ref = first { $_->internal_coords } $ang_ref->neighbors($len_ref);
188             if ($dih_ref) {
189             $dih_val = 180;
190             $ang_ref->attr("3dbuilder/first_dihedral_ang", $atom->id);
191             } else {
192             $dih_ref = first { $_->internal_coords and $_ ne $ang_ref }
193             $len_ref->neighbors($atom);
194             $dih_val = $hyb eq 'sp2' ? 180 : 120;
195             }
196             $len_ref->attr("3dbuilder/first_dihedral", $atom->id);
197             }
198            
199             ($len_ref, $len_val, $ang_ref, $ang_val, $dih_ref, $dih_val);
200             }
201              
202             sub add_atom {
203             my (%opts) = @_;
204             my ($atom, $bond, $from, $user) = @opts{qw(atom bond from user)};
205             my $stack = $user->{stack};
206             my $n = ++$user->{n};
207              
208             #print "adding atom $n: $atom!\n" if $DEBUG;
209             my $ic;
210             if ($n == 1) {
211             # first atom; place at origin
212             $ic = Chemistry::InternalCoords->new($atom);
213             } elsif ($n == 2) {
214             # second atom only needs a bond
215             $ic = Chemistry::InternalCoords->new(
216             $atom, choose_bond($atom, $bond, $from));
217             } elsif ($n == 3) {
218             # third atom, needs an angle
219             $ic = Chemistry::InternalCoords->new(
220             $atom, choose_angle($atom, $bond, $from));
221             } else {
222             # other atoms need dihedral
223             $ic = Chemistry::InternalCoords->new(
224             $atom, choose_dihedral($atom, $bond, $from));
225             }
226             $atom->internal_coords($ic);
227             }
228              
229             # do a depth-first traversal of a molecule. This should be added to
230             # Chemistry::Mol
231              
232             sub dfs {
233             my ($mol, %opts) = @_;
234             my ($on_atom, $on_bond, $user) = @opts{qw(on_atom on_bond user)};
235             my $dfs;
236             my %visited;
237             $dfs = sub {
238             my ($atom) = @_;
239             $visited{$atom} = 1;
240             for my $bn ($atom->bonds_neighbors) {
241             my $nei = $bn->{to};
242             my $bond = $bn->{bond};
243              
244             next if $visited{$bond}++;
245             $on_bond->(mol => $mol, from => $atom, to => $nei, bond => $bond,
246             user => $user)
247             if $on_bond;
248              
249             next if $visited{$nei};
250             $on_atom->(mol => $mol, from => $atom, atom => $nei,
251             bond => $bond, user => $user) if $on_atom;
252             $dfs->($nei);
253             }
254             };
255             return unless $mol->atoms;
256             $on_atom->(mol => $mol, atom => $mol->atoms(1), from => undef,
257             bond => undef, user => $user) if $on_atom;
258             $dfs->($mol->atoms(1));
259             }
260              
261             1;
262              
263             =back
264              
265             =head1 VERSION
266              
267             0.10
268              
269             =head1 SEE ALSO
270              
271             L, L.
272              
273             The PerlMol website L
274              
275             =head1 AUTHOR
276              
277             Ivan Tubert-Brohman Eitub@cpan.orgE
278              
279             =head1 COPYRIGHT
280              
281             Copyright (c) 2005 Ivan Tubert-Brohman. All rights reserved. This program is
282             free software; you can redistribute it and/or modify it under the same terms as
283             Perl itself.
284              
285             =cut
286