File Coverage

blib/lib/App/Rangeops/Command/clean.pm
Criterion Covered Total %
statement 187 205 91.2
branch 40 68 58.8
condition 17 30 56.6
subroutine 11 11 100.0
pod 5 5 100.0
total 260 319 81.5


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