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   7668 use strict;
  9         20  
  9         282  
3 9     9   37 use warnings;
  9         13  
  9         276  
4 9     9   49 use autodie;
  9         12  
  9         56  
5              
6 9     9   31429 use App::Rangeops -command;
  9         17  
  9         138  
7 9     9   3110 use App::Rangeops::Common;
  9         16  
  9         245  
8              
9 9         20466 use constant abstract =>
10 9     9   49 'replace ranges within links, incorporate hit strands and remove nested links';
  9         13  
11              
12             sub opt_spec {
13             return (
14 4     4 1 33 [ "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 4     4 1 37295 return "rangeops clean [options] ";
28             }
29              
30             sub description {
31 1     1 1 842 my $desc;
32 1         3 $desc .= ucfirst(abstract) . ".\n";
33 1         3 $desc
34             .= "\t are bilateral links files, with or without hit strands\n";
35 1         2 return $desc;
36             }
37              
38             sub validate_args {
39 3     3 1 3090 my ( $self, $opt, $args ) = @_;
40              
41 3 50       4 if ( !@{$args} ) {
  3         12  
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 3         6 for ( @{$args} ) {
  3         14  
48 3 50       14 next if lc $_ eq "stdin";
49 3 50       18 if ( !Path::Tiny::path($_)->is_file ) {
50 0         0 $self->usage_error("The input file [$_] doesn't exist.");
51             }
52             }
53              
54 3 50       289 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 3     3 1 15 my ( $self, $opt, $args ) = @_;
62              
63             #----------------------------#
64             # Load replaces
65             #----------------------------#
66 3         5 my $info_of = {}; # info of ranges
67 3         5 my $replace_of = {};
68 3 100       10 if ( $opt->{replace} ) {
69 1 50       5 print STDERR "==> Load replaces\n" if $opt->{verbose};
70 1         6 for my $line ( App::RL::Common::read_lines( $opt->{replace} ) ) {
71 6         234 $info_of = App::Rangeops::Common::build_info( [$line], $info_of );
72              
73 6         14 my @parts = split /\t/, $line;
74 6 50       14 if ( @parts == 2 ) {
75 6         15 $replace_of->{ $parts[0] } = $parts[1];
76             }
77             }
78             }
79              
80             #----------------------------#
81             # Replacing and incorporating
82             #----------------------------#
83 3 50       11 print STDERR "==> Incorporating strands\n" if $opt->{verbose};
84 3         11 my @lines;
85 3         4 for my $file ( @{$args} ) {
  3         8  
86 3         16 for my $line ( App::RL::Common::read_lines($file) ) {
87 45         771 $info_of = App::Rangeops::Common::build_info( [$line], $info_of );
88              
89 45         50 my @new_parts;
90              
91             # replacing
92 45         112 for my $part ( split /\t/, $line ) {
93              
94 135 100       185 if ( exists $replace_of->{$part} ) {
95 9         10 my $original = $part;
96 9         12 my $replaced = $replace_of->{$part};
97              
98             # create new info, don't touch anything of $info_of
99             # use original strand
100 9         8 my %new = %{ $info_of->{$replaced} };
  9         28  
101 9         183 $new{strand} = $info_of->{$original}{strand};
102              
103 9         49 my $new_part = App::RL::Common::encode_header( \%new, 1 );
104 9         116 push @new_parts, $new_part;
105             }
106             else {
107 126         149 push @new_parts, $part;
108             }
109             }
110 45         92 my $new_line = join "\t", @new_parts;
111 45         102 $info_of
112             = App::Rangeops::Common::build_info( [$new_line], $info_of );
113              
114             # incorporating
115 45 50 33     132 if ( @new_parts == 3 or @new_parts == 2 ) {
116 45         58 my $info_0 = $info_of->{ $new_parts[0] };
117 45         52 my $info_1 = $info_of->{ $new_parts[1] };
118              
119 45 50 33     82 if ( App::RL::Common::info_is_valid($info_0)
120             and App::RL::Common::info_is_valid($info_1) )
121             {
122 45         1400 my @strands;
123              
124 45 50       75 if ( @new_parts == 3 ) {
125 45 50 66     115 if ( $new_parts[2] eq "+" or $new_parts[2] eq "-" ) {
126 45         58 push @strands,
127             pop(@new_parts); # new @new_parts == 2
128             }
129             }
130              
131 45 50       76 if ( @new_parts == 2 ) {
132              
133 45         34 my %new_0 = %{$info_0};
  45         135  
134 45         1053 my %new_1 = %{$info_1};
  45         97  
135              
136 45         1315 push @strands, $new_0{strand};
137 45         53 push @strands, $new_1{strand};
138              
139 45         115 @strands = List::MoreUtils::PP::uniq(@strands);
140 45 100       524 if ( @strands == 1 ) {
141 33         42 $new_0{strand} = "+";
142 33         32 $new_1{strand} = "+";
143             }
144             else {
145 12         20 $new_0{strand} = "+";
146 12         14 $new_1{strand} = "-";
147             }
148              
149 45         93 my $range_0
150             = App::RL::Common::encode_header( \%new_0, 1 );
151 45         602 $info_of->{$range_0} = \%new_0;
152 45         83 my $range_1
153             = App::RL::Common::encode_header( \%new_1, 1 );
154 45         467 $info_of->{$range_1} = \%new_1;
155              
156 45         74 @new_parts = ( $range_0, $range_1 );
157 45         165 $new_line = join "\t", @new_parts;
158             }
159             }
160             }
161             $info_of
162 45         273 = App::Rangeops::Common::build_info( [$new_line], $info_of );
163              
164             # skip identical ranges
165 45 50       96 if ( @new_parts == 2 ) {
166 45         58 my $info_0 = $info_of->{ $new_parts[0] };
167 45         51 my $info_1 = $info_of->{ $new_parts[1] };
168              
169 45 50 33     73 if ( App::RL::Common::info_is_valid($info_0)
170             and App::RL::Common::info_is_valid($info_1) )
171             {
172 45 100 100     777 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         11 $new_line = undef;
177             }
178             }
179             }
180              
181 45         122 push @lines, $new_line;
182             }
183             }
184              
185 3         13 @lines = grep {defined} List::MoreUtils::PP::uniq(@lines);
  38         138  
186 3         7 @lines = @{ App::Rangeops::Common::sort_links( \@lines ) };
  3         14  
187 3         27 $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 3         82 my $flag_nest = 1;
194 3         10 while ($flag_nest) {
195 6 50       21 print STDERR "==> Remove nested links\n" if $opt->{verbose};
196              
197 6         12 my %to_remove = ();
198             my @chr_pairs = map {
199 6         11 my ( $r0, $r1 ) = split /\t/;
  65         519  
200             $info_of->{$r0}{chr} . ":" . $info_of->{$r1}{chr}
201 65         163 } @lines;
202 6         77 for my $i ( 0 .. $#lines - 1 ) {
203             my @rest_idx
204 59         5073 = grep { $chr_pairs[$i] eq $chr_pairs[$_] }
  330         389  
205             ( $i + 1 .. $#lines );
206              
207 59         85 for my $j (@rest_idx) {
208 21         707 my $line_i = $lines[$i];
209 21         79 my ( $range0_i, $range1_i ) = split /\t/, $line_i;
210              
211 21         26 my $line_j = $lines[$j];
212 21         36 my ( $range0_j, $range1_j ) = split /\t/, $line_j;
213              
214 21         66 my $intspan0_i = $info_of->{$range0_i}{intspan};
215 21         129 my $intspan1_i = $info_of->{$range1_i}{intspan};
216              
217 21         104 my $intspan0_j = $info_of->{$range0_j}{intspan};
218 21         105 my $intspan1_j = $info_of->{$range1_j}{intspan};
219              
220 21 100 100     103 if ( $intspan0_i->superset($intspan0_j)
    50 66        
221             and $intspan1_i->superset($intspan1_j) )
222             {
223 5         1536 $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 6         9 @lines = grep { !exists $to_remove{$_} } @lines;
  65         95  
233 6         31 $flag_nest = scalar keys %to_remove; # when no lines to remove, end loop
234             }
235 3         6 @lines = @{ App::Rangeops::Common::sort_links( \@lines ) };
  3         17  
236              
237             #----------------------------#
238             # Bundle links
239             #----------------------------#
240 3 100       20 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         144  
244             $info_of->{$r0}{chr} . ":"
245             . $info_of->{$r0}{strand} . ":"
246             . $info_of->{$r1}{chr} . ":"
247             . $info_of->{$r1}{strand}
248 11         24 } @lines;
249 1         27 my $graph = Graph->new( directed => 0 ); # graph of lines
250              
251 1         267 for my $i ( 0 .. $#lines - 1 ) {
252             my @rest_idx
253 10         506 = grep { $chr_strand_pairs[$i] eq $chr_strand_pairs[$_] }
  55         62  
254             ( $i + 1 .. $#lines );
255              
256 10         14 for my $j (@rest_idx) {
257 3         4 my $line_i = $lines[$i];
258 3         7 my ( $range0_i, $range1_i ) = split /\t/, $line_i;
259              
260 3         4 my $line_j = $lines[$j];
261 3         7 my ( $range0_j, $range1_j ) = split /\t/, $line_j;
262              
263 3 50       10 if ( !$graph->has_vertex($line_i) ) {
264 3         25 $graph->add_vertex($line_i);
265             }
266 3 50       92 if ( !$graph->has_vertex($line_j) ) {
267 3         18 $graph->add_vertex($line_j);
268             }
269              
270 3         68 my $intspan0_i = $info_of->{$range0_i}{intspan};
271 3         18 my $intspan1_i = $info_of->{$range1_i}{intspan};
272              
273 3         17 my $intspan0_j = $info_of->{$range0_j}{intspan};
274 3         23 my $intspan1_j = $info_of->{$range1_j}{intspan};
275              
276 3 100 66     18 if ( $intspan0_i->overlap($intspan0_j) >= $opt->{bundle}
277             and $intspan1_i->overlap($intspan1_j) >= $opt->{bundle} )
278             {
279 1         395 $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         1366  
  5         8  
286 1         3 for my $c (@cc) {
287 0         0 printf STDERR "\n" . " " x 4 . "Merge %s lines\n", scalar @{$c}
288 1 50       5 if $opt->{verbose};
289              
290 1         2 my @merged_range;
291              
292 1         2 for my $i ( 0, 1 ) {
293 2         76 my ( $chr, $strand );
294 2         7 my $merge_intspan = AlignDB::IntSpan->new;
295              
296 2         14 for my $line ( @{$c} ) {
  2         3  
297 4         153 @lines = grep { $_ ne $line } @lines;
  39         47  
298 4         12 my $range = ( split /\t/, $line )[$i];
299 4         14 $chr = $info_of->{$range}{chr};
300 4         25 $strand = $info_of->{$range}{strand};
301 4         25 $merge_intspan->merge( $info_of->{$range}{intspan} );
302             }
303              
304 2         127 $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         50 my $line = join "\t", @merged_range;
314 1         2 push @lines, $line;
315 1 50       3 print STDERR " " x 8 . "$line\n" if $opt->{verbose};
316              
317             # new range introduced, update $info_of
318 1         6 $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         4  
323             }
324              
325             #----------------------------#
326             # Links of nearly identical ranges escaped from merging
327             #----------------------------#
328 3 100       14 if ( $opt->{replace} ) {
329 1 50       8 print STDERR "==> Remove self links\n" if $opt->{verbose};
330              
331             my @same_pair_lines = map {
332 1         2 my ( $r0, $r1 ) = split /\t/;
  8         88  
333 8 50       28 $info_of->{$r0}{chr} eq $info_of->{$r1}{chr} ? ($_) : ()
334             } @lines;
335              
336 1         14 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 3         7 my $out_fh;
357 3 50       14 if ( lc( $opt->{outfile} ) eq "stdout" ) {
358 3         8 $out_fh = \*STDOUT;
359             }
360             else {
361 0         0 open $out_fh, ">", $opt->{outfile};
362             }
363              
364 3         9 print {$out_fh} "$_\n" for @lines;
  29         356  
365              
366 3         36 close $out_fh;
367             }
368              
369             1;