File Coverage

blib/lib/App/Rangeops/Command/connect.pm
Criterion Covered Total %
statement 137 145 94.4
branch 27 54 50.0
condition n/a
subroutine 11 11 100.0
pod 6 6 100.0
total 181 216 83.8


line stmt bran cond sub pod time code
1             package App::Rangeops::Command::connect;
2 8     8   4681 use strict;
  8         18  
  8         203  
3 8     8   34 use warnings;
  8         17  
  8         201  
4 8     8   36 use autodie;
  8         13  
  8         35  
5              
6 8     8   34951 use App::Rangeops -command;
  8         17  
  8         61  
7 8     8   2180 use App::Rangeops::Common;
  8         12  
  8         12560  
8              
9             sub abstract {
10 2     2 1 41 return 'connect bilaterial links into multilateral ones';
11             }
12              
13             sub opt_spec {
14             return (
15 2     2 1 14 [ "outfile|o=s", "Output filename. [stdout] for screen." ],
16             [ "ratio|r=f",
17             "Break links if length identities less than this ratio, default is [0.9].",
18             { default => 0.9 }
19             ],
20             [ "numeric|n", "Sort chromosome names numerically.", ],
21             [ "verbose|v", "Verbose mode.", ],
22             );
23             }
24              
25             sub usage_desc {
26 2     2 1 28103 return "rangeops connect [options] ";
27             }
28              
29             sub description {
30 1     1 1 909 my $desc;
31 1         3 $desc .= ucfirst(abstract) . ".\n";
32 1         3 $desc .= "\t are bilaterial links files without hit strands\n";
33 1         2 return $desc;
34             }
35              
36             sub validate_args {
37 1     1 1 1060 my ( $self, $opt, $args ) = @_;
38              
39 1 50       2 if ( !@{$args} ) {
  1         4  
40 0         0 $self->usage_error("This command need one or more input files.");
41             }
42 1         2 for ( @{$args} ) {
  1         2  
43 1 50       4 next if lc $_ eq "stdin";
44 1 50       11 if ( !Path::Tiny::path($_)->is_file ) {
45 0         0 $self->usage_error("The input file [$_] doesn't exist.");
46             }
47             }
48              
49 1 50       97 if ( !exists $opt->{outfile} ) {
50             $opt->{outfile}
51 0         0 = Path::Tiny::path( $args->[0] )->absolute . ".cc.tsv";
52             }
53             }
54              
55             sub execute {
56 1     1 1 6 my ( $self, $opt, $args ) = @_;
57              
58             #----------------------------#
59             # Loading
60             #----------------------------#
61 1         7 my $graph = Graph->new( directed => 0 );
62 1         234 my $info_of = {}; # info of ranges
63 1         2 for my $file ( @{$args} ) {
  1         2  
64 1         5 for my $line ( App::RL::Common::read_lines($file) ) {
65 8         3750 $info_of = App::Rangeops::Common::build_info( [$line], $info_of );
66              
67 8         15 my @parts;
68 8         19 for my $part ( split /\t/, $line ) {
69 16 50       29 next unless exists $info_of->{$part};
70 16         28 push @parts, $part;
71             }
72              
73             # all ranges will be converted to positive strands
74 8 50       15 next unless @parts == 2;
75              
76 8         13 my %new_0 = %{ $info_of->{ $parts[0] } };
  8         28  
77 8         269 my %new_1 = %{ $info_of->{ $parts[1] } };
  8         23  
78              
79 8         355 my @strands;
80 8         11 my $hit_strand = "+";
81             {
82 8         11 push @strands, $new_0{strand};
  8         14  
83 8         12 push @strands, $new_1{strand};
84 8         19 @strands = List::MoreUtils::PP::uniq(@strands);
85              
86 8 100       93 if ( @strands != 1 ) {
87 2         4 $hit_strand = "-";
88             }
89              
90 8         12 $new_0{strand} = "+";
91 8         10 $new_1{strand} = "+";
92             }
93              
94 8         17 my $range_0 = App::RL::Common::encode_header( \%new_0, 1 );
95 8         158 $info_of->{$range_0} = \%new_0;
96 8         15 my $range_1 = App::RL::Common::encode_header( \%new_1, 1 );
97 8         126 $info_of->{$range_1} = \%new_1;
98              
99             # add range
100 8         15 for my $range ( $range_0, $range_1 ) {
101 16 100       47 if ( !$graph->has_vertex($range) ) {
102 14         90 $graph->add_vertex($range);
103 14 50       455 print STDERR "Add range $range\n" if $opt->{verbose};
104             }
105             }
106              
107             # add edge
108 8 50       21 if ( !$graph->has_edge( $range_0, $range_1 ) ) {
109 8         167 $graph->add_edge( $range_0, $range_1 );
110 8         667 $graph->set_edge_attribute( $range_0, $range_1, "strand",
111             $hit_strand );
112              
113 8 50       997 print STDERR join "\t", @parts, "\n" if $opt->{verbose};
114             printf STDERR "Nodes %d \t Edges %d\n",
115             scalar $graph->vertices, scalar $graph->edges
116 8 50       28 if $opt->{verbose};
117             }
118             else {
119 0 0       0 print STDERR " " x 4 . "Edge exists, next\n" if $opt->{verbose};
120             }
121             }
122              
123 1 50       4 print STDERR "==>" . "Finish processing [$file]\n" if $opt->{verbose};
124             }
125              
126             #----------------------------#
127             # Create cc and sort
128             #----------------------------#
129 1         2 my @lines;
130 1         4 for my $cc ( $graph->connected_components ) {
131 6         4727 my @ranges = @{$cc};
  6         10  
132 6         8 my $copy = scalar @ranges;
133              
134 6 50       11 next if $copy == 1;
135              
136 6         12 my $line = join "\t", @ranges;
137 6         11 push @lines, $line;
138             }
139              
140             my @sorted_lines
141 1         2 = @{ App::Rangeops::Common::sort_links( \@lines, $opt->{numeric} ) };
  1         6  
142              
143             #----------------------------#
144             # Change strands of ranges based on first range in cc
145             #----------------------------#
146 1         6 my $new_graph = Graph->new( directed => 0 );
147 1         194 for my $line (@sorted_lines) {
148 6         469 my @ranges = split /\t/, $line;
149 6         8 my $copy = scalar @ranges;
150 6 50       14 print STDERR "Copy number of this cc is $copy\n" if $opt->{verbose};
151              
152             # transform range to info
153             # set strand to undef
154             # create intspan
155             my @infos = map {
156 6         12 my $info = App::RL::Common::decode_header($_);
  14         26  
157 14         1570 $info->{strand} = undef;
158 14         137 $info->{intspan} = AlignDB::IntSpan->new;
159 14         284 $info->{intspan}->add_pair( $info->{start}, $info->{end} );
160 14         896 $info;
161             } @ranges;
162              
163             # set first node to positive strand
164 6         21 $infos[0]->{strand} = "+";
165              
166 6         55 my $assigned = AlignDB::IntSpan->new(0);
167 6         365 my $unhandled = AlignDB::IntSpan->new;
168 6         51 $unhandled->add_pair( 0, $copy - 1 );
169 6         270 $unhandled->remove($assigned);
170              
171 6         777 my @edges; # store edge pairs. not all nodes connected
172 6         14 while ( $assigned->size < $copy ) {
173 6         104 for my $i ( $assigned->elements ) {
174 6         101 for my $j ( $unhandled->elements ) {
175 8 50       103 next if !$graph->has_edge( $ranges[$i], $ranges[$j] );
176             next
177 8 50       178 if !$graph->has_edge_attribute( $ranges[$i],
178             $ranges[$j], "strand" );
179 8 50       775 next if !defined $infos[$i]->{strand};
180              
181             # assign strands
182 8         62 my $hit_strand
183             = $graph->get_edge_attribute( $ranges[$i], $ranges[$j],
184             "strand" );
185 8 100       762 if ( $hit_strand eq "-" ) {
186             printf " " x 4 . "change strand of %s\n", $ranges[$j]
187 2 50       6 if $opt->{verbose};
188 2 50       8 if ( $infos[$i]->{strand} eq "+" ) {
189 2         15 $infos[$j]->{strand} = "-";
190             }
191             else {
192 0         0 $infos[$j]->{strand} = "+";
193             }
194             }
195             else {
196 6         17 $infos[$j]->{strand} = $infos[$i]->{strand};
197             }
198 8         124 $unhandled->remove($j);
199 8         992 $assigned->add($j);
200              
201             # break bad links
202 8         474 my $size_i = $infos[$i]->{intspan}->size;
203 8         165 my $size_j = $infos[$j]->{intspan}->size;
204 8         154 my ( $size_min, $size_max )
205             = List::MoreUtils::PP::minmax( $size_i, $size_j );
206 8         134 my $diff_ratio = sprintf "%.3f", $size_min / $size_max;
207              
208 8 50       26 if ( $diff_ratio < $opt->{ratio} ) {
209             printf STDERR " " x 4 . "Break links between %s %s\n",
210             $ranges[$i], $ranges[$j]
211 0 0       0 if $opt->{verbose};
212             printf STDERR " " x 4
213             . "Ratio[%s]\tMin [%s]\tMax[%s]\n",
214             $diff_ratio, $size_min, $size_max
215 0 0       0 if $opt->{verbose};
216             }
217             else {
218 8         29 push @edges, [ $i, $j ];
219             }
220             }
221             }
222             }
223              
224             # transform array_ref back to string
225 6         98 my @nodes_new = map { App::RL::Common::encode_header( $_, 1 ) } @infos;
  14         465  
226              
227 6         319 for my $edge (@edges) {
228 8         231 $new_graph->add_edge( $nodes_new[ $edge->[0] ],
229             $nodes_new[ $edge->[1] ] );
230             }
231             }
232              
233             #----------------------------#
234             # Recreate cc
235             #----------------------------#
236 1         103 my @new_lines;
237 1         12 for my $cc ( $new_graph->connected_components ) {
238 6         3683 my @ranges = @{$cc};
  6         10  
239 6         7 my $copy = scalar @ranges;
240              
241 6 50       11 next if $copy == 1;
242              
243 6         14 my $line = join "\t", @ranges;
244 6         12 push @new_lines, $line;
245             }
246              
247             my @new_sorted_lines
248 1         2 = @{ App::Rangeops::Common::sort_links( \@new_lines, $opt->{numeric} )
249 1         5 };
250              
251             #----------------------------#
252             # Output
253             #----------------------------#
254 1         2 my $out_fh;
255 1 50       5 if ( lc( $opt->{outfile} ) eq "stdout" ) {
256 1         2 $out_fh = \*STDOUT;
257             }
258             else {
259 0         0 open $out_fh, ">", $opt->{outfile};
260             }
261              
262 1         4 print {$out_fh} "$_\n" for @new_sorted_lines;
  6         79  
263              
264 1         13 close $out_fh;
265             }
266              
267             1;