File Coverage

blib/lib/App/Fasops/Command/refine.pm
Criterion Covered Total %
statement 138 150 92.0
branch 36 48 75.0
condition 8 9 88.8
subroutine 17 18 94.4
pod 6 8 75.0
total 205 233 87.9


line stmt bran cond sub pod time code
1             package App::Fasops::Command::refine;
2 21     21   15532 use strict;
  21         53  
  21         645  
3 21     21   117 use warnings;
  21         84  
  21         572  
4 21     21   114 use autodie;
  21         40  
  21         127  
5              
6 21     21   108810 use MCE;
  21         52  
  21         203  
7 21     21   940 use MCE::Flow Sereal => 1;
  21         81  
  21         227  
8 21     21   3054 use MCE::Candy;
  21         69  
  21         212  
9              
10 21     21   775 use App::Fasops -command;
  21         52  
  21         229  
11 21     21   7605 use App::RL::Common;
  21         62  
  21         585  
12 21     21   118 use App::Fasops::Common;
  21         49  
  21         37618  
13              
14             sub abstract {
15 2     2 1 49 return 'realign blocked fasta file with external programs';
16             }
17              
18             sub opt_spec {
19             return (
20 7     7 1 104 [ "outfile|o=s", "Output filename. [stdout] for screen" ],
21             [ "outgroup", "Has outgroup at the end of blocks", ],
22             [ "parallel|p=i", "run in parallel mode", { default => 1 }, ],
23             [ "msa=s", "Aligning program", { default => "mafft" }, ],
24             [ "quick",
25             "Quick mode, only aligning indel adjacent regions. Suitable for multiz outputs",
26             ],
27             [ "pad=i", "In quick mode, enlarge indel regions", { default => 50 }, ],
28             [ "fill=i", "In quick mode, join indel regions", { default => 50 }, ],
29             [ "chop=i", "Chop head and tail indels", { default => 0 }, ],
30             { show_defaults => 1, }
31             );
32             }
33              
34             sub usage_desc {
35 7     7 1 63739 return "fasops refine [options] ";
36             }
37              
38             sub description {
39 1     1 1 1978 my $desc;
40 1         6 $desc .= ucfirst(abstract) . ".\n";
41 1         3 $desc .= <<'MARKDOWN';
42              
43             * List of msa:
44             * mafft
45             * muscle
46             * clustalw
47             * none: means skip realigning
48             * are paths to blocked fasta files, .fas.gz is supported
49             * infile == stdin means reading from STDIN
50              
51             MARKDOWN
52              
53 1         4 return $desc;
54             }
55              
56             sub validate_args {
57 6     6 1 12960 my ( $self, $opt, $args ) = @_;
58              
59 6 100       19 if ( @{$args} != 1 ) {
  6         25  
60 1         4 my $message = "This command need one input file.\n\tIt found";
61 1         2 $message .= sprintf " [%s]", $_ for @{$args};
  1         4  
62 1         3 $message .= ".\n";
63 1         10 $self->usage_error($message);
64             }
65 5         15 for ( @{$args} ) {
  5         23  
66 5 50       25 next if lc $_ eq "stdin";
67 5 100       37 if ( !Path::Tiny::path($_)->is_file ) {
68 1         126 $self->usage_error("The input file [$_] doesn't exist.");
69             }
70             }
71              
72 4 50       379 if ( !exists $opt->{outfile} ) {
73 0         0 $opt->{outfile} = Path::Tiny::path( $args->[0] )->absolute . ".fas";
74             }
75             }
76              
77             sub execute {
78 4     4 1 27 my ( $self, $opt, $args ) = @_;
79              
80 4         8 my $in_fh;
81 4 50       15 if ( lc $args->[0] eq "stdin" ) {
82 0         0 $in_fh = *STDIN{IO};
83             }
84             else {
85 4         53 $in_fh = IO::Zlib->new( $args->[0], "rb" );
86             }
87              
88 4         7772 my $out_fh;
89 4 50       16 if ( lc( $opt->{outfile} ) eq "stdout" ) {
90 4         12 $out_fh = *STDOUT{IO};
91             }
92             else {
93 0         0 open $out_fh, ">", $opt->{outfile};
94             }
95              
96 4         8 my @infos; # collect blocks for parallelly refining
97 4         10 my $content = ''; # content of one block
98 4         8 while (1) {
99 92 100 100     627 last if $in_fh->eof and $content eq '';
100 88         4091 my $line = '';
101 88 100       277 if ( !$in_fh->eof ) {
102 87         3500 $line = $in_fh->getline;
103             }
104 88 100 100     10667 if ( ( $line eq '' or $line =~ /^\s+$/ ) and $content ne '' ) {
      66        
105 10         56 my $info_ary = App::Fasops::Common::parse_block_array( $content );
106 10         23 $content = '';
107              
108 10 100       27 if ( $opt->{parallel} >= 2 ) {
109 3         7 push @infos, $info_ary;
110             }
111             else {
112 7         22 my $out_string = proc_block( $info_ary, $opt );
113 7         12 print {$out_fh} $out_string;
  7         49  
114             }
115             }
116             else {
117 78         181 $content .= $line;
118             }
119             }
120 4         718 $in_fh->close;
121              
122 4 100       707 if ( $opt->{parallel} >= 2 ) {
123             my $worker = sub {
124 0     0   0 my ( $self, $chunk_ref, $chunk_id ) = @_;
125              
126 0         0 my $info_ary = $chunk_ref->[0];
127 0         0 my $out_string = proc_block( $info_ary, $opt );
128              
129             # preserving output order
130 0         0 MCE->gather( $chunk_id, $out_string );
131 1         9 };
132              
133             MCE::Flow::init {
134             chunk_size => 1,
135             max_workers => $opt->{parallel},
136 1         10 gather => MCE::Candy::out_iter_fh($out_fh),
137             };
138 1         71 mce_flow $worker, \@infos;
139 1         28299 MCE::Flow::finish;
140             }
141              
142 4         186 close $out_fh;
143             }
144              
145             sub proc_block {
146 7     7 0 14 my $info_ary = shift;
147 7         11 my $opt = shift;
148              
149             #----------------------------#
150             # processing seqs, leave headers untouched
151             #----------------------------#
152             {
153 7         11 my $seq_refs = [];
  7         16  
154 7         11 for my $info ( @{$info_ary} ) {
  7         21  
155 27         139 push @{$seq_refs}, $info->{seq};
  27         78  
156             }
157              
158             #----------------------------#
159             # realigning
160             #----------------------------#
161 7 50       58 if ( $opt->{msa} ne "none" ) {
162 0 0       0 if ( $opt->{quick} ) {
163             $seq_refs
164             = App::Fasops::Common::align_seqs_quick( $seq_refs,
165 0         0 $opt->{msa}, $opt->{pad}, $opt->{fill} );
166             }
167             else {
168 0         0 $seq_refs = App::Fasops::Common::align_seqs( $seq_refs, $opt->{msa} );
169             }
170             }
171              
172             #----------------------------#
173             # trimming
174             #----------------------------#
175 7         44 App::Fasops::Common::trim_pure_dash($seq_refs);
176 7 50       25 if ( $opt->{outgroup} ) {
177 0         0 App::Fasops::Common::trim_outgroup($seq_refs);
178 0         0 App::Fasops::Common::trim_complex_indel($seq_refs);
179             }
180              
181 7         13 for my $i ( 0 .. scalar @{$seq_refs} - 1 ) {
  7         17  
182 27         301 $info_ary->[$i]{seq} = uc $seq_refs->[$i];
183             }
184             }
185              
186             #----------------------------#
187             # change headers
188             #----------------------------#
189 7 100       94 if ( $opt->{chop} ) {
190 3         17 trim_head_tail( $info_ary, $opt->{chop} );
191             }
192              
193 7         45 my $out_string;
194 7         16 for my $info ( @{$info_ary} ) {
  7         17  
195 27         189 $out_string .= sprintf ">%s\n", App::RL::Common::encode_header($info);
196 27         4057 $out_string .= sprintf "%s\n", $info->{seq};
197             }
198 7         53 $out_string .= "\n";
199              
200 7         19 return $out_string;
201             }
202              
203             #----------------------------#
204             # trim head and tail indels
205             #----------------------------#
206             # If head length set to 1, the first indel will be trimmed
207             # Length set to 5 and the second indel will also be trimmed
208             # GAAA--C...
209             # --AAAGC...
210             # GAAAAGC...
211             sub trim_head_tail {
212 3     3 0 12 my $info_ary = shift;
213 3         5 my $chop_length = shift; # indels in this region will also be trimmed
214              
215             # default value means only trimming indels starting at the first base
216 3 50       12 $chop_length = defined $chop_length ? $chop_length : 1;
217              
218 3         11 my $align_length = length $info_ary->[0]{seq};
219              
220             # chop region covers all
221 3 50       25 return if $chop_length * 2 >= $align_length;
222              
223 3         9 my $indel_set = AlignDB::IntSpan->new;
224 3         34 for my $info ( @{$info_ary} ) {
  3         12  
225 12         580 my $seq_indel_set = App::Fasops::Common::indel_intspan( $info->{seq} );
226 12         60 $indel_set->merge($seq_indel_set);
227             }
228              
229             # There're no indels at all
230             # Leave $info_ary untouched
231 3 100       484 return if $indel_set->is_empty;
232              
233             { # head indel(s) to be trimmed
234 1         5 my $head_set = AlignDB::IntSpan->new;
235 1         14 $head_set->add_pair( 1, $chop_length );
236 1         65 my $head_indel_set = $indel_set->find_islands($head_set);
237              
238             # head indels
239 1 50       3463 if ( $head_indel_set->is_not_empty ) {
240 1         36 for ( 1 .. $head_indel_set->max ) {
241 3         53 for my $info ( @{$info_ary} ) {
  3         9  
242 12         151 my $base = substr( $info->{seq}, 0, 1, '' );
243 12 100       183 if ( $base ne '-' ) {
244 11 100       29 if ( $info->{strand} eq "+" ) {
245 6         50 $info->{start}++;
246             }
247             else {
248 5         48 $info->{end}--;
249             }
250             }
251             }
252             }
253             }
254             }
255              
256             { # tail indel(s) to be trimmed
257 1         15 my $tail_set = AlignDB::IntSpan->new;
  1         3  
  1         6  
258 1         15 $tail_set->add_range( $align_length - $chop_length + 1, $align_length );
259 1         70 my $tail_indel_set = $indel_set->find_islands($tail_set);
260              
261             # tail indels
262 1 50       3198 if ( $tail_indel_set->is_not_empty ) {
263 1         22 for ( $tail_indel_set->min .. $align_length ) {
264 2         31 for my $info ( @{$info_ary} ) {
  2         8  
265 8         103 my $base = substr( $info->{seq}, -1, 1, '' );
266 8 100       122 if ( $base ne '-' ) {
267 6 100       18 if ( $info->{strand} eq "+" ) {
268 4         31 $info->{end}--;
269             }
270             else {
271 2         14 $info->{start}++;
272             }
273             }
274             }
275             }
276             }
277             }
278              
279             }
280              
281             1;