5186bf56bf6a04158567ed37dd850eb976f223bc
[jalview.git] / forester / ruby / evoruby / lib / evo / io / parser / hmmscan_domain_extractor.rb
1 #
2 # = lib/evo/io/parser/hmmscan_domain_extractor.rb - HmmscanDomainExtractor class
3 #
4 # Copyright::    Copyright (C) 2017 Christian M. Zmasek
5 # License::      GNU Lesser General Public License (LGPL)
6
7 require 'lib/evo/util/constants'
8 require 'lib/evo/msa/msa_factory'
9 require 'lib/evo/io/msa_io'
10 require 'lib/evo/io/writer/fasta_writer'
11 require 'lib/evo/io/parser/fasta_parser'
12 require 'lib/evo/io/parser/hmmscan_parser'
13
14 module Evoruby
15   class HmmscanDomainExtractor
16
17     ADD_TO_CLOSE_PAIRS = 0
18     def initialize
19     end
20
21     # raises ArgumentError, IOError, StandardError
22     def parse( domain_id,
23       hmmscan_output,
24       fasta_sequence_file,
25       outfile,
26       passed_seqs_outfile,
27       failed_seqs_outfile,
28       e_value_threshold,
29       length_threshold,
30       add_position,
31       add_domain_number,
32       add_species,
33       min_linker,
34       log )
35
36       Util.check_file_for_readability( hmmscan_output )
37       Util.check_file_for_readability( fasta_sequence_file )
38       Util.check_file_for_writability( outfile + ".fasta" )
39       Util.check_file_for_writability( passed_seqs_outfile )
40       Util.check_file_for_writability( failed_seqs_outfile )
41
42       in_msa = nil
43       factory = MsaFactory.new()
44       in_msa = factory.create_msa_from_file( fasta_sequence_file, FastaParser.new() )
45
46       if ( in_msa == nil || in_msa.get_number_of_seqs() < 1 )
47         error_msg = "could not find fasta sequences in " + fasta_sequence_file
48         raise IOError, error_msg
49       end
50
51       out_msa = Msa.new
52
53       failed_seqs = Msa.new
54       passed_seqs = Msa.new
55       out_msa_pairs = nil
56       out_msa_isolated = nil
57       out_msa_singles = nil
58       out_msa_single_domains_protein_seqs = nil
59       out_msa_close_pairs_protein_seqs = nil
60       out_msa_close_pairs_only_protein_seqs = nil
61       out_msa_isolated_protein_seqs = nil
62       out_msa_isolated_only_protein_seqs = nil
63       out_msa_isolated_and_close_pair_protein_seqs = nil
64       if min_linker
65         out_msa_pairs = Msa.new
66         out_msa_isolated = Msa.new
67         out_msa_singles = Msa.new
68         out_msa_single_domains_protein_seqs = Msa.new
69         out_msa_close_pairs_protein_seqs = Msa.new
70         out_msa_close_pairs_only_protein_seqs = Msa.new
71         out_msa_isolated_protein_seqs = Msa.new
72         out_msa_isolated_only_protein_seqs = Msa.new
73         out_msa_isolated_and_close_pair_protein_seqs  = Msa.new
74       end
75
76       ld = Constants::LINE_DELIMITER
77
78       domain_pass_counter     = 0
79       domain_fail_counter     = 0
80       proteins_with_failing_domains = 0
81       max_domain_copy_number_per_protein = -1
82       max_domain_copy_number_sequence    = ""
83
84       hmmscan_datas = []
85
86       hmmscan_parser = HmmscanParser.new( hmmscan_output )
87       results = hmmscan_parser.parse
88
89       results.each do | r |
90         if domain_id != r.model
91           next
92         end
93
94         sequence  = r.query
95         number    = r.number
96         out_of    = r.out_of
97         env_from  = r.env_from
98         env_to    = r.env_to
99         i_e_value = r.i_e_value
100
101         if ( ( ( e_value_threshold < 0.0 ) || ( i_e_value <= e_value_threshold ) ) &&
102         ( ( length_threshold <= 0 )   || ( env_to - env_from + 1 ) >= length_threshold.to_f ) )
103           hmmscan_datas << HmmsearchData.new( sequence, number, out_of, env_from, env_to, i_e_value )
104           if ( number > max_domain_copy_number_per_protein )
105             max_domain_copy_number_sequence    = sequence
106             max_domain_copy_number_per_protein = number
107           end
108         else # failed
109           print( domain_fail_counter.to_s + ": " + sequence.to_s + " did not meet threshold(s)" )
110           log << domain_fail_counter.to_s + ": " + sequence.to_s + " did not meet threshold(s)"
111           if ( ( e_value_threshold.to_f >= 0.0 ) && ( i_e_value > e_value_threshold ) )
112             print( " iE=" + i_e_value.to_s )
113             log << " iE=" + i_e_value.to_s
114           end
115           if ( ( length_threshold.to_f > 0 ) && ( env_to - env_from + 1 ) < length_threshold.to_f )
116             le = env_to - env_from + 1
117             print( " l=" + le.to_s )
118             log << " l=" + le.to_s
119           end
120           print( ld )
121           log << ld
122           domain_fail_counter  += 1
123         end
124
125         if number > out_of
126           error_msg = "number > out_of ! (this should not have happened)"
127           raise StandardError, error_msg
128         end
129
130         if number == out_of
131           if !hmmscan_datas.empty?
132             process_hmmscan_datas( hmmscan_datas,
133             in_msa,
134             add_position,
135             add_domain_number,
136             add_species,
137             out_msa,
138             out_msa_singles,
139             out_msa_pairs,
140             out_msa_isolated,
141             min_linker,
142             out_msa_single_domains_protein_seqs,
143             out_msa_close_pairs_protein_seqs,
144             out_msa_close_pairs_only_protein_seqs,
145             out_msa_isolated_protein_seqs,
146             out_msa_isolated_only_protein_seqs,
147             out_msa_isolated_and_close_pair_protein_seqs )
148             domain_pass_counter += hmmscan_datas.length
149             if passed_seqs.find_by_name_start( sequence, true ).length < 1
150               add_sequence( sequence, in_msa, passed_seqs )
151             else
152               error_msg = "this should not have happened"
153               raise StandardError, error_msg
154             end
155           else
156             if failed_seqs.find_by_name_start( sequence, true ).length < 1
157               add_sequence( sequence, in_msa, failed_seqs )
158               proteins_with_failing_domains += 1
159             else
160               error_msg = "this should not have happened"
161               raise StandardError, error_msg
162             end
163           end
164           hmmscan_datas.clear
165         end
166       end
167
168       if domain_pass_counter < 1
169         error_msg = "no domain sequences were extracted"
170         raise IOError, error_msg
171       end
172
173       log << ld
174       puts( "Max domain copy number per protein : " + max_domain_copy_number_per_protein.to_s )
175       log << "Max domain copy number per protein : " + max_domain_copy_number_per_protein.to_s
176       log << ld
177
178       if ( max_domain_copy_number_per_protein > 1 )
179         puts( "First protein with this copy number: " + max_domain_copy_number_sequence )
180         log << "First protein with this copy number: " + max_domain_copy_number_sequence
181         log << ld
182       end
183
184       write_msa( out_msa, outfile + ".fasta"  )
185       write_msa( passed_seqs, passed_seqs_outfile )
186       write_msa( failed_seqs, failed_seqs_outfile )
187
188       if out_msa_pairs
189         write_msa( out_msa_pairs, outfile + "_" + min_linker.to_s + ".fasta")
190       end
191
192       if out_msa_singles
193         write_msa( out_msa_singles, outfile + "_singles.fasta")
194       end
195
196       if out_msa_isolated
197         write_msa( out_msa_isolated, outfile + "_" + min_linker.to_s + "_isolated.fasta");
198       end
199
200       if out_msa_single_domains_protein_seqs
201         write_msa( out_msa_single_domains_protein_seqs, outfile + "_proteins_with_singles.fasta" )
202       end
203
204       if out_msa_close_pairs_protein_seqs
205         write_msa( out_msa_close_pairs_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_close_pairs.fasta" )
206       end
207
208       if out_msa_close_pairs_only_protein_seqs
209         write_msa( out_msa_close_pairs_only_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_close_pairs_only.fasta" )
210       end
211
212       if  out_msa_isolated_protein_seqs
213         write_msa(  out_msa_isolated_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_isolated_domains.fasta" )
214       end
215
216       if out_msa_isolated_only_protein_seqs
217         write_msa( out_msa_isolated_only_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_isolated_domains_only.fasta" )
218       end
219
220       if out_msa_isolated_and_close_pair_protein_seqs
221         write_msa( out_msa_isolated_and_close_pair_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_isolated_and_close_pairs.fasta" )
222       end
223
224       if min_linker
225         if ( out_msa_single_domains_protein_seqs.get_number_of_seqs +
226         out_msa_close_pairs_only_protein_seqs.get_number_of_seqs +
227         out_msa_isolated_only_protein_seqs.get_number_of_seqs +
228         out_msa_isolated_and_close_pair_protein_seqs.get_number_of_seqs ) != passed_seqs.get_number_of_seqs
229           error_msg = "this should not have happened"
230           raise StandardError, error_msg
231         end
232       end
233
234       log << ld
235       log << "passing domains                             : " + domain_pass_counter.to_s + ld
236       log << "failing domains                             : " + domain_fail_counter.to_s + ld
237       log << "input proteins                              : " + in_msa.get_number_of_seqs.to_s + ld
238       log << "proteins with passing domains               : " + passed_seqs.get_number_of_seqs.to_s + ld
239       log << "proteins with no passing domains            : " + proteins_with_failing_domains.to_s + ld
240       if min_linker
241         log << "min linker length                           : " + min_linker.to_s + ld
242         log << "single domains                              : " + out_msa_singles.get_number_of_seqs.to_s + ld
243         log << "domains in close pairs                      : " + (2 * out_msa_pairs.get_number_of_seqs).to_s + ld
244         log << "isolated domains                            : " + out_msa_isolated.get_number_of_seqs.to_s + ld
245         log << "proteins wih single domains                 : " + out_msa_single_domains_protein_seqs.get_number_of_seqs.to_s + ld
246         log << "proteins wih close pair domains             : " + out_msa_close_pairs_protein_seqs.get_number_of_seqs.to_s + ld
247         log << "proteins wih close pair domains only        : " + out_msa_close_pairs_only_protein_seqs.get_number_of_seqs.to_s + ld
248         log << "proteins wih isolated domains               : " + out_msa_isolated_protein_seqs.get_number_of_seqs.to_s + ld
249         log << "proteins wih isolated domains only          : " + out_msa_isolated_only_protein_seqs.get_number_of_seqs.to_s + ld
250         log << "proteins wih close pair and isolated domains: " + out_msa_isolated_and_close_pair_protein_seqs.get_number_of_seqs.to_s + ld
251       end
252
253       log << ld
254
255       return domain_pass_counter
256
257     end # parse
258
259     private
260
261     def write_msa( msa, filename )
262       io = MsaIO.new()
263       w = FastaWriter.new()
264       w.set_line_width( 60 )
265       w.clean( true )
266       begin
267         io.write_to_file( msa, filename, w )
268       rescue Exception
269         error_msg = "could not write to \"" + filename + "\""
270         raise IOError, error_msg
271       end
272     end
273
274     def add_sequence( sequence_name, in_msa, add_to_msa )
275       seqs = in_msa.find_by_name_start( sequence_name, true )
276       if ( seqs.length < 1 )
277         error_msg = "sequence \"" + sequence_name + "\" not found in sequence file"
278         raise StandardError, error_msg
279       end
280       if ( seqs.length > 1 )
281         error_msg = "sequence \"" + sequence_name + "\" not unique in sequence file"
282         raise StandardError, error_msg
283       end
284       seq = in_msa.get_sequence( seqs[ 0 ] )
285       add_to_msa.add_sequence( seq )
286     end
287
288     def process_hmmscan_datas( hmmscan_datas,
289       in_msa,
290       add_position,
291       add_domain_number,
292       add_species,
293       out_msa,
294       out_msa_singles,
295       out_msa_pairs,
296       out_msa_isolated,
297       min_linker,
298       out_msa_single_domains_protein_seqs,
299       out_msa_close_pairs_protein_seqs,
300       out_msa_close_pairs_only_protein_seqs,
301       out_msa_isolated_protein_seqs,
302       out_msa_isolated_only_protein_seqs,
303       out_msa_isolated_and_close_pair_protein_seqs )
304
305       actual_out_of = hmmscan_datas.size
306       saw_close_pair = false
307       saw_isolated = false
308
309       seq_name = ""
310       prev_seq_name = nil
311
312       hmmscan_datas.each_with_index do |hmmscan_data, index|
313         if hmmscan_data.number < ( index + 1 )
314           error_msg = "hmmscan_data.number < ( index + 1 ) (this should not have happened)"
315           raise StandardError, error_msg
316         end
317
318         seq_name =  hmmscan_data.seq_name
319
320         extract_domain( seq_name,
321         index + 1,
322         actual_out_of,
323         hmmscan_data.env_from,
324         hmmscan_data.env_to,
325         in_msa,
326         out_msa,
327         add_position,
328         add_domain_number,
329         add_species )
330
331         if min_linker
332           if actual_out_of == 1
333             extract_domain( seq_name,
334             1,
335             1,
336             hmmscan_data.env_from,
337             hmmscan_data.env_to,
338             in_msa,
339             out_msa_singles,
340             add_position,
341             add_domain_number,
342             add_species )
343             if out_msa_single_domains_protein_seqs.find_by_name_start( seq_name, true ).length < 1
344               add_sequence( seq_name, in_msa, out_msa_single_domains_protein_seqs )
345             else
346               error_msg = "this should not have happened"
347               raise StandardError, error_msg
348             end
349
350           else
351             first = index == 0
352             last = index == hmmscan_datas.length - 1
353
354             if ( ( first && ( ( hmmscan_datas[ index + 1 ].env_from - hmmscan_data.env_to ) > min_linker) )  ||
355             ( last && ( ( hmmscan_data.env_from - hmmscan_datas[ index - 1 ].env_to ) > min_linker ) ) ||
356             ( !first && !last &&  ( ( hmmscan_datas[ index + 1 ].env_from - hmmscan_data.env_to ) > min_linker ) &&
357             ( ( hmmscan_data.env_from - hmmscan_datas[ index - 1 ].env_to ) > min_linker ) ) )
358
359               extract_domain( seq_name,
360               index + 1,
361               actual_out_of,
362               hmmscan_data.env_from,
363               hmmscan_data.env_to,
364               in_msa,
365               out_msa_isolated,
366               add_position,
367               add_domain_number,
368               add_species )
369               saw_isolated = true
370
371             elsif !first
372
373               from = hmmscan_datas[ index - 1 ].env_from
374               to = hmmscan_data.env_to
375
376               if ADD_TO_CLOSE_PAIRS > 0
377                 from = from - ADD_TO_CLOSE_PAIRS
378                 if from < 1
379                   from = 1
380                 end
381                 to = to + ADD_TO_CLOSE_PAIRS
382                 temp_seqs = in_msa.find_by_name_start( seq_name, true )
383                 temp_seq = in_msa.get_sequence( temp_seqs[ 0 ] )
384                 if to >  temp_seq.get_length
385                   to =  temp_seq.get_length
386                 end
387               end
388
389               extract_domain( seq_name,
390               index.to_s  + "+" + ( index + 1 ).to_s,
391               actual_out_of,
392               from,
393               to,
394               in_msa,
395               out_msa_pairs,
396               add_position,
397               add_domain_number,
398               add_species )
399               saw_close_pair = true
400             end
401           end
402         end
403         if prev_seq_name && prev_seq_name != seq_name
404           error_msg = "this should not have happened"
405           raise StandardError, error_msg
406         end
407         prev_seq_name = seq_name
408       end # each
409       if saw_isolated
410         if out_msa_isolated_protein_seqs.find_by_name_start( seq_name, true ).length < 1
411           add_sequence( seq_name, in_msa, out_msa_isolated_protein_seqs )
412         else
413           error_msg = "this should not have happened"
414           raise StandardError, error_msg
415         end
416       end
417       if saw_close_pair
418         if out_msa_close_pairs_protein_seqs.find_by_name_start( seq_name, true ).length < 1
419           add_sequence( seq_name, in_msa, out_msa_close_pairs_protein_seqs )
420         else
421           error_msg = "this should not have happened"
422           raise StandardError, error_msg
423         end
424       end
425       if saw_close_pair && saw_isolated
426         if out_msa_isolated_and_close_pair_protein_seqs.find_by_name_start( seq_name, true ).length < 1
427           add_sequence( seq_name, in_msa, out_msa_isolated_and_close_pair_protein_seqs )
428         else
429           error_msg = "this should not have happened"
430           raise StandardError, error_msg
431         end
432       elsif saw_close_pair
433         if out_msa_close_pairs_only_protein_seqs.find_by_name_start( seq_name, true ).length < 1
434           add_sequence( seq_name, in_msa, out_msa_close_pairs_only_protein_seqs )
435         else
436           error_msg = "this should not have happened"
437           raise StandardError, error_msg
438         end
439       elsif saw_isolated
440         if out_msa_isolated_only_protein_seqs.find_by_name_start( seq_name, true ).length < 1
441           add_sequence( seq_name, in_msa, out_msa_isolated_only_protein_seqs )
442         else
443           error_msg = "this should not have happened"
444           raise StandardError, error_msg
445         end
446       end
447     end # def process_hmmscan_data
448
449     def extract_domain( sequence,
450       number,
451       out_of,
452       seq_from,
453       seq_to,
454       in_msa,
455       out_msa,
456       add_position,
457       add_domain_number,
458       add_species )
459       if number.is_a?( Fixnum ) && ( number < 1 || out_of < 1 || number > out_of )
460         error_msg = "number=" + number.to_s + ", out of=" + out_of.to_s
461         raise StandardError, error_msg
462       end
463       if seq_from < 1 || seq_to < 1 || seq_from >= seq_to
464         error_msg = "impossible: seq-from=" + seq_from.to_s + ", seq-to=" + seq_to.to_s
465         raise StandardError, error_msg
466       end
467       seqs = in_msa.find_by_name_start( sequence, true )
468       if seqs.length < 1
469         error_msg = "sequence \"" + sequence + "\" not found in sequence file"
470         raise IOError, error_msg
471       end
472       if seqs.length > 1
473         error_msg = "sequence \"" + sequence + "\" not unique in sequence file"
474         raise IOError, error_msg
475       end
476       # hmmsearch is 1 based, wheres sequences are 0 bases in this package.
477       seq = in_msa.get_sequence( seqs[ 0 ] ).get_subsequence( seq_from - 1, seq_to - 1 )
478
479       orig_name = seq.get_name
480
481       seq.set_name( orig_name.split[ 0 ] )
482
483       if add_position
484         seq.set_name( seq.get_name + "_" + seq_from.to_s + "-" + seq_to.to_s )
485       end
486
487       if out_of != 1 && add_domain_number
488         seq.set_name( seq.get_name + "~" + number.to_s + "-" + out_of.to_s )
489       end
490
491       if add_species
492         a = orig_name.rindex "["
493         b = orig_name.rindex "]"
494         unless a && b
495           error_msg = "species not found in " + orig_name
496           raise StandardError, error_msg
497         end
498         species = orig_name[ a .. b ]
499         seq.set_name( seq.get_name + " " + species )
500       end
501       out_msa.add_sequence( seq )
502     end
503
504     def is_ignorable?( line )
505       return ( line !~ /[A-Za-z0-9-]/ || line =~/^#/ )
506     end
507
508   end # class HmmscanDomainExtractor
509
510   class HmmsearchData
511     def initialize( seq_name, number, out_of, env_from, env_to, i_e_value )
512       @seq_name = seq_name
513       @number = number
514       @out_of = out_of
515       @env_from = env_from
516       @env_to = env_to
517       @i_e_value = i_e_value
518     end
519     attr_reader :seq_name, :number, :out_of, :env_from, :env_to, :i_e_value
520   end
521
522 end # module Evoruby
523