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   14196 use strict;
  20         57  
  20         622  
3 20     20   109 use warnings;
  20         51  
  20         491  
4 20     20   103 use autodie;
  20         49  
  20         113  
5              
6 20     20   104905 use MCE;
  20         50  
  20         173  
7 20     20   930 use MCE::Flow Sereal => 1;
  20         44  
  20         207  
8 20     20   2691 use MCE::Candy;
  20         41  
  20         172  
9              
10 20     20   675 use App::Fasops -command;
  20         43  
  20         193  
11 20     20   6809 use App::RL::Common;
  20         53  
  20         539  
12 20     20   115 use App::Fasops::Common;
  20         51  
  20         36952  
13              
14             sub abstract {
15 2     2 1 45 return 'realign blocked fasta file with external programs';
16             }
17              
18             sub opt_spec {
19             return (
20 7     7 1 79 [ "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 61948 return "fasops refine [options] ";
36             }
37              
38             sub description {
39 1     1 1 1920 my $desc;
40 1         4 $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 12778 my ( $self, $opt, $args ) = @_;
58              
59 6 100       12 if ( @{$args} != 1 ) {
  6         23  
60 1         2 my $message = "This command need one input file.\n\tIt found";
61 1         3 $message .= sprintf " [%s]", $_ for @{$args};
  1         3  
62 1         3 $message .= ".\n";
63 1         19 $self->usage_error($message);
64             }
65 5         12 for ( @{$args} ) {
  5         12  
66 5 50       23 next if lc $_ eq "stdin";
67 5 100       39 if ( !Path::Tiny::path($_)->is_file ) {
68 1         109 $self->usage_error("The input file [$_] doesn't exist.");
69             }
70             }
71              
72 4 50       339 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 31 my ( $self, $opt, $args ) = @_;
79              
80 4         9 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         44 $in_fh = IO::Zlib->new( $args->[0], "rb" );
86             }
87              
88 4         7089 my $out_fh;
89 4 50       16 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         9 my $content = ''; # content of one block
98 4         9 while (1) {
99 92 100 100     612 last if $in_fh->eof and $content eq '';
100 88         3982 my $line = '';
101 88 100       301 if ( !$in_fh->eof ) {
102 87         3387 $line = $in_fh->getline;
103             }
104 88 100 100     10569 if ( ( $line eq '' or $line =~ /^\s+$/ ) and $content ne '' ) {
      66        
105 10         57 my $info_ary = App::Fasops::Common::parse_block_array( $content );
106 10         20 $content = '';
107              
108 10 100       27 if ( $opt->{parallel} >= 2 ) {
109 3         14 push @infos, $info_ary;
110             }
111             else {
112 7         18 my $out_string = proc_block( $info_ary, $opt );
113 7         16 print {$out_fh} $out_string;
  7         43  
114             }
115             }
116             else {
117 78         162 $content .= $line;
118             }
119             }
120 4         688 $in_fh->close;
121              
122 4 100       620 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         46 mce_flow $worker, \@infos;
139 1         27348 MCE::Flow::finish;
140             }
141              
142 4         183 close $out_fh;
143             }
144              
145             sub proc_block {
146 7     7 0 12 my $info_ary = shift;
147 7         11 my $opt = shift;
148              
149             #----------------------------#
150             # processing seqs, leave headers untouched
151             #----------------------------#
152             {
153 7         8 my $seq_refs = [];
  7         14  
154 7         10 for my $info ( @{$info_ary} ) {
  7         20  
155 27         131 push @{$seq_refs}, $info->{seq};
  27         79  
156             }
157              
158             #----------------------------#
159             # realigning
160             #----------------------------#
161 7 50       57 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         39 App::Fasops::Common::trim_pure_dash($seq_refs);
176 7 50       24 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         10 for my $i ( 0 .. scalar @{$seq_refs} - 1 ) {
  7         15  
182 27         306 $info_ary->[$i]{seq} = uc $seq_refs->[$i];
183             }
184             }
185              
186             #----------------------------#
187             # change headers
188             #----------------------------#
189 7 100       81 if ( $opt->{chop} ) {
190 3         32 trim_head_tail( $info_ary, $opt->{chop} );
191             }
192              
193 7         50 my $out_string;
194 7         12 for my $info ( @{$info_ary} ) {
  7         21  
195 27         177 $out_string .= sprintf ">%s\n", App::RL::Common::encode_header($info);
196 27         4032 $out_string .= sprintf "%s\n", $info->{seq};
197             }
198 7         53 $out_string .= "\n";
199              
200 7         18 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         7 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       9 $chop_length = defined $chop_length ? $chop_length : 1;
217              
218 3         21 my $align_length = length $info_ary->[0]{seq};
219              
220             # chop region covers all
221 3 50       31 return if $chop_length * 2 >= $align_length;
222              
223 3         9 my $indel_set = AlignDB::IntSpan->new;
224 3         35 for my $info ( @{$info_ary} ) {
  3         14  
225 12         568 my $seq_indel_set = App::Fasops::Common::indel_intspan( $info->{seq} );
226 12         47 $indel_set->merge($seq_indel_set);
227             }
228              
229             # There're no indels at all
230             # Leave $info_ary untouched
231 3 100       525 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         21 $head_set->add_pair( 1, $chop_length );
236 1         67 my $head_indel_set = $indel_set->find_islands($head_set);
237              
238             # head indels
239 1 50       3368 if ( $head_indel_set->is_not_empty ) {
240 1         32 for ( 1 .. $head_indel_set->max ) {
241 3         54 for my $info ( @{$info_ary} ) {
  3         6  
242 12         147 my $base = substr( $info->{seq}, 0, 1, '' );
243 12 100       215 if ( $base ne '-' ) {
244 11 100       37 if ( $info->{strand} eq "+" ) {
245 6         45 $info->{start}++;
246             }
247             else {
248 5         32 $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         10  
258 1         22 $tail_set->add_range( $align_length - $chop_length + 1, $align_length );
259 1         72 my $tail_indel_set = $indel_set->find_islands($tail_set);
260              
261             # tail indels
262 1 50       3162 if ( $tail_indel_set->is_not_empty ) {
263 1         32 for ( $tail_indel_set->min .. $align_length ) {
264 2         24 for my $info ( @{$info_ary} ) {
  2         5  
265 8         108 my $base = substr( $info->{seq}, -1, 1, '' );
266 8 100       160 if ( $base ne '-' ) {
267 6 100       17 if ( $info->{strand} eq "+" ) {
268 4         32 $info->{end}--;
269             }
270             else {
271 2         22 $info->{start}++;
272             }
273             }
274             }
275             }
276             }
277             }
278              
279             }
280              
281             1;