make the stemma a property of the tradition
[scpubgit/stemmatology.git] / lib / Text / Tradition / Stemma.pm
1 package Text::Tradition::Stemma;
2
3 use Bio::Phylo::IO;
4 use Encode qw( decode_utf8 );
5 use File::chdir;
6 use File::Temp;
7 use Graph;
8 use Graph::Convert;
9 use Graph::Reader::Dot;
10 use IPC::Run qw/ run binary /;
11 use Moose;
12 use Text::Balanced qw/ extract_bracketed /;
13
14 has collation => (
15     is => 'ro',
16     isa => 'Text::Tradition::Collation',
17     required => 1,
18     weak_ref => 1,
19     );  
20
21 has graph => (
22     is => 'rw',
23     isa => 'Graph',
24     predicate => 'has_graph',
25     );
26     
27 has distance_trees => (
28     is => 'ro',
29     isa => 'ArrayRef[Graph]',
30     writer => '_save_distance_trees',
31     predicate => 'has_distance_trees',
32     );
33     
34 sub BUILD {
35     my( $self, $args ) = @_;
36     # If we have been handed a dotfile, initialize it into a graph.
37     if( exists $args->{'dot'} ) {
38         $self->graph_from_dot( $args->{'dot'} );
39     }
40 }
41
42 sub graph_from_dot {
43         my( $self, $dotfh ) = @_;
44         # Assume utf-8
45         binmode( $dotfh, ':utf8' );
46         my $reader = Graph::Reader::Dot->new();
47         my $graph = $reader->read_graph( $dotfh );
48         $graph 
49                 ? $self->graph( $graph ) 
50                 : warn "Failed to parse dot in $dotfh";
51 }
52
53 sub as_dot {
54     my( $self, $opts ) = @_;
55     # TODO add options for display, someday
56     # TODO see what happens with Graph::Writer::Dot someday
57     my $dgraph = Graph::Convert->as_graph_easy( $self->graph );
58     # Set some class display attributes for 'hypothetical' and 'extant' nodes
59     $dgraph->set_attribute( 'flow', 'south' );
60     foreach my $n ( $dgraph->nodes ) {
61         if( $n->attribute( 'class' ) eq 'hypothetical' ) {
62             $n->set_attribute( 'shape', 'point' );
63             $n->set_attribute( 'pointshape', 'diamond' );
64         } else {
65             $n->set_attribute( 'shape', 'ellipse' );
66         }
67     }
68     
69     # Render to svg via graphviz
70     my @lines = split( /\n/, $dgraph->as_graphviz() );
71     # Add the size attribute
72     if( $opts->{'size'} ) {
73         my $sizeline = "  graph [ size=\"" . $opts->{'size'} . "\" ]";
74         splice( @lines, 1, 0, $sizeline );
75     }
76     return join( "\n", @lines );
77 }
78         
79
80 # Render the stemma as SVG.
81 sub as_svg {
82     my( $self, $opts ) = @_;
83     my $dot = $self->as_dot( $opts );
84     my @cmd = qw/dot -Tsvg/;
85     my( $svg, $err );
86     my $dotfile = File::Temp->new();
87     ## TODO REMOVE
88     # $dotfile->unlink_on_destroy(0);
89     binmode $dotfile, ':utf8';
90     print $dotfile $dot;
91     push( @cmd, $dotfile->filename );
92     run( \@cmd, ">", binary(), \$svg );
93     $svg = decode_utf8( $svg );
94     return $svg;
95 }
96
97 sub witnesses {
98     my $self = shift;
99     my @wits = grep { $self->graph->get_vertex_attribute( $_, 'class' ) eq 'extant' }
100         $self->graph->vertices;
101     return @wits;
102 }
103
104 #### Methods for calculating phylogenetic trees ####
105
106 before 'distance_trees' => sub {
107     my $self = shift;
108     my %args = @_;
109     # TODO allow specification of method for calculating distance tree
110     if( $args{'recalc'} || !$self->has_distance_trees ) {
111         # We need to make a tree before we can return it.
112         my( $ok, $result ) = $self->run_phylip_pars();
113         if( $ok ) {
114             # Save the resulting trees
115             my $trees = _parse_newick( $result );
116             $self->_save_distance_trees( $trees );
117         } else {
118             warn "Failed to calculate distance trees: $result";
119         }
120     }
121 };
122         
123 sub make_character_matrix {
124     my $self = shift;
125     unless( $self->collation->linear ) {
126         warn "Need a linear graph in order to make an alignment table";
127         return;
128     }
129     my $table = $self->collation->make_alignment_table;
130     # Push the names of the witnesses to initialize the rows of the matrix.
131     my @matrix = map { [ $self->_normalize_ac( $_ ) ] } @{$table->[0]};
132     foreach my $token_index ( 1 .. $#{$table} ) {
133         # First implementation: make dumb alignment table, caring about
134         # nothing except which reading is in which position.
135         my @chars = convert_characters( $table->[$token_index] );
136         foreach my $idx ( 0 .. $#matrix ) {
137             push( @{$matrix[$idx]}, $chars[$idx] );
138         }
139     }
140     return \@matrix;
141
142
143 sub _normalize_ac {
144     my( $self, $witname ) = @_;
145     my $ac = $self->collation->ac_label;
146     if( $witname =~ /(.*)\Q$ac\E$/ ) {
147         $witname = $1 . '_ac';
148     }
149     return sprintf( "%-10s", $witname );
150 }
151
152 sub convert_characters {
153     my $row = shift;
154     # This is a simple algorithm that treats every reading as different.
155     # Eventually we will want to be able to specify how relationships
156     # affect the character matrix.
157     my %unique = ( '__UNDEF__' => 'X',
158                    '#LACUNA#'  => '?',
159                  );
160     my %count;
161     my $ctr = 0;
162     foreach my $word ( @$row ) {
163         if( $word && !exists $unique{$word} ) {
164             $unique{$word} = chr( 65 + $ctr );
165             $ctr++;
166         }
167         $count{$word}++ if $word;
168     }
169     # Try to keep variants under 8 by lacunizing any singletons.
170     if( scalar( keys %unique ) > 8 ) {
171                 foreach my $word ( keys %count ) {
172                         if( $count{$word} == 1 ) {
173                                 $unique{$word} = '?';
174                         }
175                 }
176     }
177     my %u = reverse %unique;
178     if( scalar( keys %u ) > 8 ) {
179         warn "Have more than 8 variants on this location; phylip will break";
180     }
181     my @chars = map { $_ ? $unique{$_} : $unique{'__UNDEF__' } } @$row;
182     return @chars;
183 }
184
185 sub phylip_pars_input {
186     my $self = shift;
187     my $character_matrix = $self->make_character_matrix;
188     my $input = '';
189     my $rows = scalar @{$character_matrix};
190     my $columns = scalar @{$character_matrix->[0]} - 1;
191     $input .= "\t$rows\t$columns\n";
192     foreach my $row ( @{$character_matrix} ) {
193         $input .= join( '', @$row ) . "\n";
194     }
195     return $input;
196 }
197
198 sub run_phylip_pars {
199     my $self = shift;
200
201     # Set up a temporary directory for all the default Phylip files.
202     my $phylip_dir = File::Temp->newdir();
203     # $phylip_dir->unlink_on_destroy(0);
204     # We need an infile, and we need a command input file.
205     open( MATRIX, ">$phylip_dir/infile" ) or die "Could not write $phylip_dir/infile";
206     print MATRIX $self->phylip_pars_input();
207     close MATRIX;
208
209     open( CMD, ">$phylip_dir/cmdfile" ) or die "Could not write $phylip_dir/cmdfile";
210     ## TODO any configuration parameters we want to set here
211 #   U                 Search for best tree?  Yes
212 #   S                        Search option?  More thorough search
213 #   V              Number of trees to save?  100
214 #   J     Randomize input order of species?  No. Use input order
215 #   O                        Outgroup root?  No, use as outgroup species 1
216 #   T              Use Threshold parsimony?  No, use ordinary parsimony
217 #   W                       Sites weighted?  No
218 #   M           Analyze multiple data sets?  No
219 #   I            Input species interleaved?  Yes
220 #   0   Terminal type (IBM PC, ANSI, none)?  ANSI
221 #   1    Print out the data at start of run  No
222 #   2  Print indications of progress of run  Yes
223 #   3                        Print out tree  Yes
224 #   4          Print out steps in each site  No
225 #   5  Print character at all nodes of tree  No
226 #   6       Write out trees onto tree file?  Yes
227     print CMD "Y\n";
228     close CMD;
229
230     # And then we run the program.
231     ### HACKY HACKY
232     my $PHYLIP_PATH = '/Users/tla/Projects/phylip-3.69/exe';
233     my $program = "pars";
234     if( $^O eq 'darwin' ) {
235         $program = "$PHYLIP_PATH/$program.app/Contents/MacOS/$program";
236     } else {
237         $program = "$PHYLIP_PATH/$program";
238     }
239
240     {
241         # We need to run it in our temporary directory where we have created
242         # all the expected files.
243         local $CWD = $phylip_dir;
244         my @cmd = ( $program );
245         run \@cmd, '<', 'cmdfile', '>', '/dev/null';
246     }
247     # Now our output should be in 'outfile' and our tree in 'outtree',
248     # both in the temp directory.
249
250     my @outtree;
251     if( -f "$phylip_dir/outtree" ) {
252         open( TREE, "$phylip_dir/outtree" ) or die "Could not open outtree for read";
253         @outtree = <TREE>;
254         close TREE;
255     }
256     return( 1, join( '', @outtree ) ) if @outtree;
257
258     my @error;
259     if( -f "$phylip_dir/outfile" ) {
260         open( OUTPUT, "$phylip_dir/outfile" ) or die "Could not open output for read";
261         @error = <OUTPUT>;
262         close OUTPUT;
263     } else {
264         push( @error, "Neither outtree nor output file was produced!" );
265     }
266     return( undef, join( '', @error ) );
267 }
268
269 sub _parse_newick {
270     my $newick = shift;
271     my @trees;
272     # Parse the result into a tree
273     my $forest = Bio::Phylo::IO->parse( 
274         -format => 'newick',
275         -string => $newick,
276         );
277     # Turn the tree into a graph, starting with the root node
278     foreach my $tree ( @{$forest->get_entities} ) {
279         push( @trees, _graph_from_bio( $tree ) );
280     }
281     return \@trees;
282 }
283
284 sub _graph_from_bio {
285     my $tree = shift;
286     my $graph = Graph->new( 'undirected' => 1 );
287     # Give all the intermediate anonymous nodes a name.
288     my $i = 0;
289     foreach my $n ( @{$tree->get_entities} ) {
290         next if $n->get_name;
291         $n->set_name( $i++ );
292     }
293     my $root = $tree->get_root->get_name;
294     $graph->add_vertex( $root );
295     _add_tree_children( $graph, $root, $tree->get_root->get_children() );
296     return $graph;
297 }
298
299 sub _add_tree_children {
300     my( $graph, $parent, $tree_children ) = @_;
301     foreach my $c ( @$tree_children ) {
302         my $child = $c->get_name;
303         $graph->add_vertex( $child );
304         $graph->add_path( $parent, $child );
305         _add_tree_children( $graph, $child, $c->get_children() );
306     }
307 }
308
309 no Moose;
310 __PACKAGE__->meta->make_immutable;
311     
312 1;