File Coverage

blib/lib/App/Rangeops/Command/clean.pm
Criterion Covered Total %
statement 185 203 91.1
branch 40 68 58.8
condition 17 30 56.6
subroutine 11 11 100.0
pod 6 6 100.0
total 259 318 81.4


line stmt bran cond sub pod time code
1             package App::Rangeops::Command::clean;
2 8     8   5508 use strict;
  8         19  
  8         215  
3 8     8   35 use warnings;
  8         15  
  8         174  
4 8     8   32 use autodie;
  8         18  
  8         36  
5              
6 8     8   35233 use App::Rangeops -command;
  8         16  
  8         77  
7 8     8   2535 use App::Rangeops::Common;
  8         15  
  8         18444  
8              
9             sub abstract {
10 2     2 1 42 return 'replace ranges within links, incorporate hit strands and remove nested links';
11             }
12              
13             sub opt_spec {
14             return (
15 5     5 1 39 [ "replace|r=s",
16             "Two-column tsv file, normally produced by command merge."
17             ],
18             [ "bundle|b=i",
19             "Bundle overlapped links. This value is the overlapping size, default is [0]. Suggested value is [500].",
20             { default => 0 },
21             ],
22             [ "outfile|o=s", "Output filename. [stdout] for screen." ],
23             [ "verbose|v", "Verbose mode.", ],
24             );
25             }
26              
27             sub usage_desc {
28 5     5 1 39561 return "rangeops clean [options] ";
29             }
30              
31             sub description {
32 1     1 1 919 my $desc;
33 1         3 $desc .= ucfirst(abstract) . ".\n";
34 1         2 $desc
35             .= "\t are bilateral links files, with or without hit strands\n";
36 1         2 return $desc;
37             }
38              
39             sub validate_args {
40 4     4 1 4559 my ( $self, $opt, $args ) = @_;
41              
42 4 50       6 if ( !@{$args} ) {
  4         15  
43 0         0 my $message = "This command need one or more input files.\n\tIt found";
44 0         0 $message .= sprintf " [%s]", $_ for @{$args};
  0         0  
45 0         0 $message .= ".\n";
46 0         0 $self->usage_error($message);
47             }
48 4         7 for ( @{$args} ) {
  4         11  
49 4 50       13 next if lc $_ eq "stdin";
50 4 50       18 if ( !Path::Tiny::path($_)->is_file ) {
51 0         0 $self->usage_error("The input file [$_] doesn't exist.");
52             }
53             }
54              
55 4 50       321 if ( !exists $opt->{outfile} ) {
56             $opt->{outfile}
57 0         0 = Path::Tiny::path( $args->[0] )->absolute . ".clean.tsv";
58             }
59             }
60              
61             sub execute {
62 4     4 1 24 my ( $self, $opt, $args ) = @_;
63              
64             #----------------------------#
65             # Load replaces
66             #----------------------------#
67 4         6 my $info_of = {}; # info of ranges
68 4         8 my $replace_of = {};
69 4 100       10 if ( $opt->{replace} ) {
70 1 50       4 print STDERR "==> Load replaces\n" if $opt->{verbose};
71 1         5 for my $line ( App::RL::Common::read_lines( $opt->{replace} ) ) {
72 6         2485 $info_of = App::Rangeops::Common::build_info( [$line], $info_of );
73              
74 6         16 my @parts = split /\t/, $line;
75 6 50       12 if ( @parts == 2 ) {
76 6         15 $replace_of->{ $parts[0] } = $parts[1];
77             }
78             }
79             }
80              
81             #----------------------------#
82             # Replacing and incorporating
83             #----------------------------#
84 4 50       12 print STDERR "==> Incorporating strands\n" if $opt->{verbose};
85 4         6 my @lines;
86 4         8 for my $file ( @{$args} ) {
  4         7  
87 4         14 for my $line ( App::RL::Common::read_lines($file) ) {
88 45         10537 $info_of = App::Rangeops::Common::build_info( [$line], $info_of );
89              
90 45         66 my @new_parts;
91              
92             # replacing
93 45         113 for my $part ( split /\t/, $line ) {
94              
95 135 100       200 if ( exists $replace_of->{$part} ) {
96 9         12 my $original = $part;
97 9         50 my $replaced = $replace_of->{$part};
98              
99             # create new info, don't touch anything of $info_of
100             # use original strand
101 9         12 my %new = %{ $info_of->{$replaced} };
  9         34  
102 9         228 $new{strand} = $info_of->{$original}{strand};
103              
104 9         55 my $new_part = App::RL::Common::encode_header( \%new, 1 );
105 9         150 push @new_parts, $new_part;
106             }
107             else {
108 126         192 push @new_parts, $part;
109             }
110             }
111 45         100 my $new_line = join "\t", @new_parts;
112 45         97 $info_of
113             = App::Rangeops::Common::build_info( [$new_line], $info_of );
114              
115             # incorporating
116 45 50 33     110 if ( @new_parts == 3 or @new_parts == 2 ) {
117 45         76 my $info_0 = $info_of->{ $new_parts[0] };
118 45         55 my $info_1 = $info_of->{ $new_parts[1] };
119              
120 45 50 33     74 if ( App::RL::Common::info_is_valid($info_0)
121             and App::RL::Common::info_is_valid($info_1) )
122             {
123 45         1584 my @strands;
124              
125 45 50       77 if ( @new_parts == 3 ) {
126 45 50 66     93 if ( $new_parts[2] eq "+" or $new_parts[2] eq "-" ) {
127 45         64 push @strands,
128             pop(@new_parts); # new @new_parts == 2
129             }
130             }
131              
132 45 50       75 if ( @new_parts == 2 ) {
133              
134 45         51 my %new_0 = %{$info_0};
  45         130  
135 45         1304 my %new_1 = %{$info_1};
  45         104  
136              
137 45         1642 push @strands, $new_0{strand};
138 45         77 push @strands, $new_1{strand};
139              
140 45         93 @strands = List::MoreUtils::PP::uniq(@strands);
141 45 100       568 if ( @strands == 1 ) {
142 33         52 $new_0{strand} = "+";
143 33         48 $new_1{strand} = "+";
144             }
145             else {
146 12         20 $new_0{strand} = "+";
147 12         15 $new_1{strand} = "-";
148             }
149              
150 45         92 my $range_0
151             = App::RL::Common::encode_header( \%new_0, 1 );
152 45         813 $info_of->{$range_0} = \%new_0;
153 45         83 my $range_1
154             = App::RL::Common::encode_header( \%new_1, 1 );
155 45         653 $info_of->{$range_1} = \%new_1;
156              
157 45         95 @new_parts = ( $range_0, $range_1 );
158 45         156 $new_line = join "\t", @new_parts;
159             }
160             }
161             }
162             $info_of
163 45         235 = App::Rangeops::Common::build_info( [$new_line], $info_of );
164              
165             # skip identical ranges
166 45 50       95 if ( @new_parts == 2 ) {
167 45         73 my $info_0 = $info_of->{ $new_parts[0] };
168 45         67 my $info_1 = $info_of->{ $new_parts[1] };
169              
170 45 50 33     74 if ( App::RL::Common::info_is_valid($info_0)
171             and App::RL::Common::info_is_valid($info_1) )
172             {
173 45 100 100     739 if ( $info_0->{chr} eq $info_1->{chr}
      66        
174             and $info_0->{start} == $info_1->{start}
175             and $info_0->{end} == $info_1->{end} )
176             {
177 8         39 $new_line = undef;
178             }
179             }
180             }
181              
182 45         111 push @lines, $new_line;
183             }
184             }
185              
186 4         1835 @lines = grep {defined} List::MoreUtils::PP::uniq(@lines);
  38         154  
187 4         12 @lines = @{ App::Rangeops::Common::sort_links( \@lines ) };
  4         15  
188 4         17 $info_of = App::Rangeops::Common::build_info_intspan( \@lines );
189              
190             #----------------------------#
191             # Remove nested links
192             #----------------------------#
193             # now all @lines (links) are without hit strands
194 4         8 my $flag_nest = 1;
195 4         11 while ($flag_nest) {
196 7 50       18 print STDERR "==> Remove nested links\n" if $opt->{verbose};
197              
198 7         14 my %to_remove = ();
199             my @chr_pairs = map {
200 7         12 my ( $r0, $r1 ) = split /\t/;
  65         608  
201             $info_of->{$r0}{chr} . ":" . $info_of->{$r1}{chr}
202 65         151 } @lines;
203 7         71 for my $i ( 0 .. $#lines - 1 ) {
204             my @rest_idx
205 59         6896 = grep { $chr_pairs[$i] eq $chr_pairs[$_] }
  330         471  
206             ( $i + 1 .. $#lines );
207              
208 59         98 for my $j (@rest_idx) {
209 21         873 my $line_i = $lines[$i];
210 21         61 my ( $range0_i, $range1_i ) = split /\t/, $line_i;
211              
212 21         34 my $line_j = $lines[$j];
213 21         36 my ( $range0_j, $range1_j ) = split /\t/, $line_j;
214              
215 21         66 my $intspan0_i = $info_of->{$range0_i}{intspan};
216 21         135 my $intspan1_i = $info_of->{$range1_i}{intspan};
217              
218 21         125 my $intspan0_j = $info_of->{$range0_j}{intspan};
219 21         120 my $intspan1_j = $info_of->{$range1_j}{intspan};
220              
221 21 100 100     111 if ( $intspan0_i->superset($intspan0_j)
    50 66        
222             and $intspan1_i->superset($intspan1_j) )
223             {
224 5         1951 $to_remove{$line_j}++;
225             }
226             elsif ( $intspan0_j->superset($intspan0_i)
227             and $intspan1_j->superset($intspan1_i) )
228             {
229 0         0 $to_remove{$line_i}++;
230             }
231             }
232             }
233 7         10 @lines = grep { !exists $to_remove{$_} } @lines;
  65         98  
234 7         28 $flag_nest = scalar keys %to_remove; # when no lines to remove, end loop
235             }
236 4         6 @lines = @{ App::Rangeops::Common::sort_links( \@lines ) };
  4         13  
237              
238             #----------------------------#
239             # Bundle links
240             #----------------------------#
241 4 100       16 if ( $opt->{bundle} ) {
242 1 50       4 print STDERR "==> Bundle overlapped links\n" if $opt->{verbose};
243             my @chr_strand_pairs = map {
244 1         3 my ( $r0, $r1 ) = split /\t/;
  11         170  
245             $info_of->{$r0}{chr} . ":"
246             . $info_of->{$r0}{strand} . ":"
247             . $info_of->{$r1}{chr} . ":"
248             . $info_of->{$r1}{strand}
249 11         25 } @lines;
250 1         27 my $graph = Graph->new( directed => 0 ); # graph of lines
251              
252 1         267 for my $i ( 0 .. $#lines - 1 ) {
253             my @rest_idx
254 10         699 = grep { $chr_strand_pairs[$i] eq $chr_strand_pairs[$_] }
  55         81  
255             ( $i + 1 .. $#lines );
256              
257 10         17 for my $j (@rest_idx) {
258 3         5 my $line_i = $lines[$i];
259 3         8 my ( $range0_i, $range1_i ) = split /\t/, $line_i;
260              
261 3         5 my $line_j = $lines[$j];
262 3         7 my ( $range0_j, $range1_j ) = split /\t/, $line_j;
263              
264 3 50       9 if ( !$graph->has_vertex($line_i) ) {
265 3         23 $graph->add_vertex($line_i);
266             }
267 3 50       107 if ( !$graph->has_vertex($line_j) ) {
268 3         17 $graph->add_vertex($line_j);
269             }
270              
271 3         86 my $intspan0_i = $info_of->{$range0_i}{intspan};
272 3         21 my $intspan1_i = $info_of->{$range1_i}{intspan};
273              
274 3         17 my $intspan0_j = $info_of->{$range0_j}{intspan};
275 3         19 my $intspan1_j = $info_of->{$range1_j}{intspan};
276              
277 3 100 66     17 if ( $intspan0_i->overlap($intspan0_j) >= $opt->{bundle}
278             and $intspan1_i->overlap($intspan1_j) >= $opt->{bundle} )
279             {
280 1         609 $graph->add_edge( $line_i, $line_j );
281             }
282             }
283             }
284              
285             # bundle connected lines
286 1         5 my @cc = grep { scalar @{$_} > 1 } $graph->connected_components;
  5         1603  
  5         10  
287 1         3 for my $c (@cc) {
288 0         0 printf STDERR "\n" . " " x 4 . "Merge %s lines\n", scalar @{$c}
289 1 50       4 if $opt->{verbose};
290              
291 1         2 my @merged_range;
292              
293 1         2 for my $i ( 0, 1 ) {
294 2         89 my ( $chr, $strand );
295 2         5 my $merge_intspan = AlignDB::IntSpan->new;
296              
297 2         18 for my $line ( @{$c} ) {
  2         4  
298 4         173 @lines = grep { $_ ne $line } @lines;
  39         57  
299 4         19 my $range = ( split /\t/, $line )[$i];
300 4         17 $chr = $info_of->{$range}{chr};
301 4         30 $strand = $info_of->{$range}{strand};
302 4         25 $merge_intspan->merge( $info_of->{$range}{intspan} );
303             }
304              
305 2         185 $merged_range[$i] = App::RL::Common::encode_header(
306             { chr => $chr,
307             strand => $strand,
308             start => $merge_intspan->min,
309             end => $merge_intspan->max,
310             }
311             );
312             }
313              
314 1         71 my $line = join "\t", @merged_range;
315 1         3 push @lines, $line;
316 1 50       4 print STDERR " " x 8 . "$line\n" if $opt->{verbose};
317              
318             # new range introduced, update $info_of
319 1         5 $info_of = App::Rangeops::Common::build_info_intspan( [$line],
320             $info_of );
321             }
322              
323 1         2 @lines = @{ App::Rangeops::Common::sort_links( \@lines ) };
  1         4  
324             }
325              
326             #----------------------------#
327             # Links of nearly identical ranges escaped from merging
328             #----------------------------#
329 4 100       12 if ( $opt->{replace} ) {
330 1 50       4 print STDERR "==> Remove self links\n" if $opt->{verbose};
331              
332             my @same_pair_lines = map {
333 1         2 my ( $r0, $r1 ) = split /\t/;
  8         68  
334 8 50       23 $info_of->{$r0}{chr} eq $info_of->{$r1}{chr} ? ($_) : ()
335             } @lines;
336              
337 1         10 for my $line (@same_pair_lines) {
338 0         0 my ( $range0, $range1 ) = split /\t/, $line;
339              
340 0         0 my $intspan0 = $info_of->{$range0}{intspan};
341 0         0 my $intspan1 = $info_of->{$range1}{intspan};
342              
343 0         0 my $intspan_i = $intspan0->intersect($intspan1);
344 0 0       0 if ( $intspan_i->is_not_empty ) {
345 0 0 0     0 if ( $intspan_i->size / $intspan0->size > 0.5
346             and $intspan_i->size / $intspan1->size > 0.5 )
347             {
348 0         0 @lines = grep { $_ ne $line } @lines;
  0         0  
349             }
350             }
351             }
352             }
353              
354             #----------------------------#
355             # Output
356             #----------------------------#
357 4         5 my $out_fh;
358 4 50       16 if ( lc( $opt->{outfile} ) eq "stdout" ) {
359 4         9 $out_fh = \*STDOUT;
360             }
361             else {
362 0         0 open $out_fh, ">", $opt->{outfile};
363             }
364              
365 4         8 print {$out_fh} "$_\n" for @lines;
  29         312  
366              
367 4         40 close $out_fh;
368             }
369              
370             1;