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 20     20   13794 use strict;
  20         49  
  20         680  
3 20     20   117 use warnings;
  20         41  
  20         498  
4 20     20   107 use autodie;
  20         40  
  20         108  
5              
6 20     20   104581 use MCE;
  20         53  
  20         179  
7 20     20   873 use MCE::Flow Sereal => 1;
  20         50  
  20         185  
8 20     20   2785 use MCE::Candy;
  20         43  
  20         173  
9              
10 20     20   687 use App::Fasops -command;
  20         43  
  20         185  
11 20     20   6810 use App::RL::Common;
  20         44  
  20         537  
12 20     20   107 use App::Fasops::Common;
  20         55  
  20         36244  
13              
14             sub abstract {
15 2     2 1 48 return 'realign blocked fasta file with external programs';
16             }
17              
18             sub opt_spec {
19             return (
20 7     7 1 72 [ "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 53035 return "fasops refine [options] ";
36             }
37              
38             sub description {
39 1     1 1 1584 my $desc;
40 1         3 $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         3 return $desc;
54             }
55              
56             sub validate_args {
57 6     6 1 10665 my ( $self, $opt, $args ) = @_;
58              
59 6 100       13 if ( @{$args} != 1 ) {
  6         21  
60 1         3 my $message = "This command need one input file.\n\tIt found";
61 1         2 $message .= sprintf " [%s]", $_ for @{$args};
  1         3  
62 1         2 $message .= ".\n";
63 1         10 $self->usage_error($message);
64             }
65 5         13 for ( @{$args} ) {
  5         16  
66 5 50       16 next if lc $_ eq "stdin";
67 5 100       34 if ( !Path::Tiny::path($_)->is_file ) {
68 1         97 $self->usage_error("The input file [$_] doesn't exist.");
69             }
70             }
71              
72 4 50       349 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 22 my ( $self, $opt, $args ) = @_;
79              
80 4         8 my $in_fh;
81 4 50       16 if ( lc $args->[0] eq "stdin" ) {
82 0         0 $in_fh = *STDIN{IO};
83             }
84             else {
85 4         44 $in_fh = IO::Zlib->new( $args->[0], "rb" );
86             }
87              
88 4         6207 my $out_fh;
89 4 50       17 if ( lc( $opt->{outfile} ) eq "stdout" ) {
90 4         11 $out_fh = *STDOUT{IO};
91             }
92             else {
93 0         0 open $out_fh, ">", $opt->{outfile};
94             }
95              
96 4         9 my @infos; # collect blocks for parallelly refining
97 4         5 my $content = ''; # content of one block
98 4         8 while (1) {
99 92 100 100     515 last if $in_fh->eof and $content eq '';
100 88         3292 my $line = '';
101 88 100       234 if ( !$in_fh->eof ) {
102 87         2822 $line = $in_fh->getline;
103             }
104 88 100 100     8932 if ( ( $line eq '' or $line =~ /^\s+$/ ) and $content ne '' ) {
      66        
105 10         50 my $info_ary = App::Fasops::Common::parse_block_array( $content );
106 10         18 $content = '';
107              
108 10 100       23 if ( $opt->{parallel} >= 2 ) {
109 3         7 push @infos, $info_ary;
110             }
111             else {
112 7         33 my $out_string = proc_block( $info_ary, $opt );
113 7         11 print {$out_fh} $out_string;
  7         56  
114             }
115             }
116             else {
117 78         133 $content .= $line;
118             }
119             }
120 4         604 $in_fh->close;
121              
122 4 100       588 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         7 };
132              
133             MCE::Flow::init {
134             chunk_size => 1,
135             max_workers => $opt->{parallel},
136 1         7 gather => MCE::Candy::out_iter_fh($out_fh),
137             };
138 1         41 mce_flow $worker, \@infos;
139 1         22242 MCE::Flow::finish;
140             }
141              
142 4         162 close $out_fh;
143             }
144              
145             sub proc_block {
146 7     7 0 11 my $info_ary = shift;
147 7         11 my $opt = shift;
148              
149             #----------------------------#
150             # processing seqs, leave headers untouched
151             #----------------------------#
152             {
153 7         9 my $seq_refs = [];
  7         18  
154 7         9 for my $info ( @{$info_ary} ) {
  7         18  
155 27         125 push @{$seq_refs}, $info->{seq};
  27         67  
156             }
157              
158             #----------------------------#
159             # realigning
160             #----------------------------#
161 7 50       46 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         30 App::Fasops::Common::trim_pure_dash($seq_refs);
176 7 50       18 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         14 for my $i ( 0 .. scalar @{$seq_refs} - 1 ) {
  7         14  
182 27         249 $info_ary->[$i]{seq} = uc $seq_refs->[$i];
183             }
184             }
185              
186             #----------------------------#
187             # change headers
188             #----------------------------#
189 7 100       100 if ( $opt->{chop} ) {
190 3         21 trim_head_tail( $info_ary, $opt->{chop} );
191             }
192              
193 7         40 my $out_string;
194 7         18 for my $info ( @{$info_ary} ) {
  7         15  
195 27         155 $out_string .= sprintf ">%s\n", App::RL::Common::encode_header($info);
196 27         3490 $out_string .= sprintf "%s\n", $info->{seq};
197             }
198 7         50 $out_string .= "\n";
199              
200 7         28 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 6 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         14 my $align_length = length $info_ary->[0]{seq};
219              
220             # chop region covers all
221 3 50       30 return if $chop_length * 2 >= $align_length;
222              
223 3         14 my $indel_set = AlignDB::IntSpan->new;
224 3         31 for my $info ( @{$info_ary} ) {
  3         6  
225 12         471 my $seq_indel_set = App::Fasops::Common::indel_intspan( $info->{seq} );
226 12         40 $indel_set->merge($seq_indel_set);
227             }
228              
229             # There're no indels at all
230             # Leave $info_ary untouched
231 3 100       402 return if $indel_set->is_empty;
232              
233             { # head indel(s) to be trimmed
234 1         3 my $head_set = AlignDB::IntSpan->new;
235 1         14 $head_set->add_pair( 1, $chop_length );
236 1         63 my $head_indel_set = $indel_set->find_islands($head_set);
237              
238             # head indels
239 1 50       2823 if ( $head_indel_set->is_not_empty ) {
240 1         27 for ( 1 .. $head_indel_set->max ) {
241 3         44 for my $info ( @{$info_ary} ) {
  3         5  
242 12         127 my $base = substr( $info->{seq}, 0, 1, '' );
243 12 100       151 if ( $base ne '-' ) {
244 11 100       24 if ( $info->{strand} eq "+" ) {
245 6         37 $info->{start}++;
246             }
247             else {
248 5         31 $info->{end}--;
249             }
250             }
251             }
252             }
253             }
254             }
255              
256             { # tail indel(s) to be trimmed
257 1         14 my $tail_set = AlignDB::IntSpan->new;
  1         7  
  1         15  
258 1         14 $tail_set->add_range( $align_length - $chop_length + 1, $align_length );
259 1         57 my $tail_indel_set = $indel_set->find_islands($tail_set);
260              
261             # tail indels
262 1 50       2626 if ( $tail_indel_set->is_not_empty ) {
263 1         18 for ( $tail_indel_set->min .. $align_length ) {
264 2         21 for my $info ( @{$info_ary} ) {
  2         4  
265 8         85 my $base = substr( $info->{seq}, -1, 1, '' );
266 8 100       100 if ( $base ne '-' ) {
267 6 100       13 if ( $info->{strand} eq "+" ) {
268 4         28 $info->{end}--;
269             }
270             else {
271 2         12 $info->{start}++;
272             }
273             }
274             }
275             }
276             }
277             }
278              
279             }
280              
281             1;