in progress...
[jalview.git] / forester / ruby / evoruby / lib / evo / tool / phylogenies_decorator.rb
1 #!/usr/local/bin/ruby -w
2 #
3 # = lib/evo/apps/phylogenies_decorator
4 #
5 # Copyright::    Copyright (C) 2017 Christian M. Zmasek
6 # License::      GNU Lesser General Public License (LGPL)
7 #
8 # decoration of phylogenies with sequence/species names and domain architectures
9 #
10 # Environment variable FORESTER_HOME needs to point to the appropriate
11 # directory (e.g. setenv FORESTER_HOME $HOME/SOFTWARE_DEV/ECLIPSE_WORKSPACE/forester/)
12
13 require 'lib/evo/util/constants'
14 require 'lib/evo/util/util'
15 require 'lib/evo/util/command_line_arguments'
16 require 'date'
17 require 'fileutils'
18
19 module Evoruby
20   class PhylogeniesDecorator
21
22     DECORATOR_OPTIONS_SEQ_NAMES = '-p -t -mp -or'
23     DECORATOR_OPTIONS_DOMAINS   = '-p -t'
24     SLEEP_TIME                  = 0.01
25     REMOVE_NI                   = true
26     TMP_FILE_1                  = '___PD1___'
27     TMP_FILE_2                  = '___PD2___'
28     LOG_FILE                    = '00_phylogenies_decorator.log'
29     FORESTER_HOME               = ENV[Constants::FORESTER_HOME_ENV_VARIABLE]
30     JAVA_HOME                   = ENV[Constants::JAVA_HOME_ENV_VARIABLE]
31
32     PRG_NAME       = "phylogenies_decorator"
33     PRG_DATE       = "170428"
34     PRG_DESC       = "decoration of phylogenies with sequence/species names and domain architectures"
35     PRG_VERSION    = "1.05"
36     WWW            = "https://sites.google.com/site/cmzmasek/home/software/forester"
37
38     HELP_OPTION_1                           = "help"
39     HELP_OPTION_2                           = "h"
40     NO_DOMAINS_OPTION                       = 'nd'
41     NO_SEQS_OPTION                          = 'ns'
42     VERBOSE_OPTION                          = 'v'
43     EXTRACT_BRACKETED_TAXONOMIC_CODE_OPTION = 'tc'
44
45     NL = Constants::LINE_DELIMITER
46     def run
47
48       Util.print_program_information( PRG_NAME,
49       PRG_VERSION,
50       PRG_DESC,
51       PRG_DATE,
52       WWW,
53       STDOUT )
54
55       if ( ARGV == nil || ARGV.length < 2  )
56         print_help
57         exit( -1 )
58       end
59
60       if FORESTER_HOME == nil || FORESTER_HOME.length < 1
61         Util.fatal_error( PRG_NAME, "apparently environment variable #{Constants::FORESTER_HOME_ENV_VARIABLE} has not been set" )
62       end
63       if JAVA_HOME == nil ||  JAVA_HOME.length < 1
64         Util.fatal_error( PRG_NAME, "apparently environment variable #{Constants::JAVA_HOME_ENV_VARIABLE} has not been set" )
65       end
66
67       if !File.exist?( FORESTER_HOME )
68         Util.fatal_error( PRG_NAME, '[' + FORESTER_HOME + '] does not exist' )
69       end
70       if !File.exist?( JAVA_HOME )
71         Util.fatal_error( PRG_NAME, '[' + JAVA_HOME + '] does not exist' )
72       end
73
74       decorator = JAVA_HOME + '/bin/java -cp ' + FORESTER_HOME + '/java/forester.jar org.forester.application.decorator'
75
76       begin
77         cla = CommandLineArguments.new( ARGV )
78       rescue ArgumentError => e
79         Util.fatal_error( PRG_NAME, "error: " + e.to_s )
80       end
81
82       if ( cla.is_option_set?( HELP_OPTION_1 ) ||
83       cla.is_option_set?( HELP_OPTION_2 ) )
84         print_help
85         exit( 0 )
86       end
87
88       if ( cla.get_number_of_files != 2 && cla.get_number_of_files != 3 )
89         print_help
90         exit( -1 )
91       end
92
93       allowed_opts = Array.new
94       allowed_opts.push(NO_DOMAINS_OPTION)
95       allowed_opts.push(NO_SEQS_OPTION)
96       allowed_opts.push(EXTRACT_BRACKETED_TAXONOMIC_CODE_OPTION)
97       allowed_opts.push(VERBOSE_OPTION)
98
99       disallowed = cla.validate_allowed_options_as_str( allowed_opts )
100       if ( disallowed.length > 0 )
101         Util.fatal_error( PRG_NAME,
102         "unknown option(s): " + disallowed,
103         STDOUT )
104       end
105
106       no_domains = false
107       if cla.is_option_set?(NO_DOMAINS_OPTION)
108         no_domains = true
109       end
110
111       no_seqs_files = false
112       if cla.is_option_set?(NO_SEQS_OPTION)
113         no_seqs_files = true
114       end
115
116       extr_bracketed_tc = false
117       if cla.is_option_set?(EXTRACT_BRACKETED_TAXONOMIC_CODE_OPTION)
118         extr_bracketed_tc = true
119       end
120
121       verbose = false
122       if cla.is_option_set?(VERBOSE_OPTION)
123         verbose = true
124       end
125
126       if File.exist? LOG_FILE
127         Util.fatal_error( PRG_NAME, 'logfile [' + LOG_FILE + '] already exists' )
128       end
129
130       in_suffix = cla.get_file_name( 0 )
131       out_suffix = cla.get_file_name( 1 )
132
133       mapping_files_dir = nil
134
135       if cla.get_number_of_files == 3
136         mapping_files_dir = cla.get_file_name( 2 )
137       else
138         mapping_files_dir = Dir.getwd
139       end
140       unless File.exist? mapping_files_dir
141         Util.fatal_error( PRG_NAME, 'mapping files directory [' + mapping_files_dir + '] does not exist' )
142       end
143       unless File.directory? mapping_files_dir
144         Util.fatal_error( PRG_NAME, '[' + mapping_files_dir + '] is not a directory' )
145       end
146       if Dir.entries(mapping_files_dir).length <= 2
147         Util.fatal_error( PRG_NAME, 'mapping files directory [' + mapping_files_dir + '] is empty' )
148       end
149
150       mapping_files_dir = Util.canonical_path( mapping_files_dir.to_s )
151
152       log = String.new
153
154       now = DateTime.now
155       log << "Program              : " + PRG_NAME + NL
156       log << "Version              : " + PRG_VERSION + NL
157       log << "Program date         : " + PRG_DATE + NL
158       log << "Input/Output dir     : " + Dir.getwd + NL
159       log << "Mappings file dir    : " + mapping_files_dir + NL
160       log << "Input suffix         : " + in_suffix + NL
161       log << "Output suffix        : " + out_suffix + NL
162       log << "No domains data      : " + no_domains.to_s + NL
163       log << "No mol seq data      : " + no_seqs_files.to_s + NL
164       log << "Extract tax codes    : " + extr_bracketed_tc.to_s + NL
165       log << "Date/time: " + now.to_s + NL + NL
166
167       Util.print_message( PRG_NAME, 'Input/Output dir : ' + Dir.getwd )
168       Util.print_message( PRG_NAME, 'Mappings file dir: ' + mapping_files_dir )
169       Util.print_message( PRG_NAME, 'Input suffix     : ' + in_suffix )
170       Util.print_message( PRG_NAME, 'Output suffix    : ' + out_suffix )
171       Util.print_message( PRG_NAME, 'No domains data  : ' + no_domains.to_s )
172       Util.print_message( PRG_NAME, 'No mol seq data  : ' + no_seqs_files.to_s )
173       Util.print_message( PRG_NAME, 'Extract tax codes: ' + extr_bracketed_tc.to_s )
174
175       if ( File.exist?( TMP_FILE_1 ) )
176         File.delete( TMP_FILE_1 )
177       end
178       if ( File.exist?( TMP_FILE_2 ) )
179         File.delete( TMP_FILE_2 )
180       end
181
182       files = Dir.entries( "." )
183
184       counter = 0
185
186       files.each { | phylogeny_file |
187         if ( !File.directory?( phylogeny_file ) &&
188         phylogeny_file !~ /^\./ &&
189         phylogeny_file !~ /^00/ &&
190         phylogeny_file !~ /#{out_suffix}$/ &&
191         phylogeny_file =~ /#{in_suffix}$/ )
192           begin
193             Util.check_file_for_readability( phylogeny_file )
194           rescue ArgumentError
195             Util.fatal_error( PRG_NAME, 'can not read from: ' + phylogeny_file + ': '+ $!.to_s )
196           end
197
198           counter += 1
199
200           outfile = phylogeny_file.sub( /#{in_suffix}$/, out_suffix )
201
202           if REMOVE_NI
203             outfile = outfile.sub( /_ni_/, '_' )
204           end
205
206           if File.exist?( outfile )
207             msg = counter.to_s + ': ' + phylogeny_file + ' -> ' +  outfile +
208             ' : already exists, skipping'
209             Util.print_message( PRG_NAME, msg  )
210             log << msg + NL
211             next
212           end
213
214           if verbose
215             puts
216           end
217           Util.print_message( PRG_NAME, counter.to_s + ': ' + phylogeny_file + ' -> ' +  outfile )
218           log << counter.to_s + ': ' + phylogeny_file + ' -> ' +  outfile + NL
219
220           phylogeny_id = phylogeny_file
221           if  phylogeny_id == nil || phylogeny_id.size < 1
222             Util.fatal_error( PRG_NAME, 'could not get id from ' + phylogeny_file.to_s )
223           end
224           if verbose
225             Util.print_message( PRG_NAME, "Id: " + phylogeny_id )
226           end
227           log << "Id: " + phylogeny_id + NL
228
229           ids_mapfile_path = nil
230           domains_mapfile_path = nil
231           seqs_file_path = nil
232
233           ids_mapfile_name = get_file( mapping_files_dir, phylogeny_id, Constants::ID_MAP_FILE_SUFFIX )
234           ids_mapfile_path = Util.canonical_path(mapping_files_dir, ids_mapfile_name)
235
236           begin
237             Util.check_file_for_readability( ids_mapfile_path)
238           rescue IOError
239             Util.fatal_error( PRG_NAME, "failed to read from [#{ids_mapfile_path}]: " + $!.to_s )
240           end
241           if verbose
242             Util.print_message( PRG_NAME, "Ids mapfile: " + ids_mapfile_path )
243           end
244           log << "Ids mapfile: " + ids_mapfile_path + NL
245
246           unless no_seqs_files
247             seqs_file_name = get_file( mapping_files_dir, phylogeny_id, Constants::ID_NORMALIZED_FASTA_FILE_SUFFIX )
248             seqs_file_path = Util.canonical_path(mapping_files_dir, seqs_file_name)
249             begin
250               Util.check_file_for_readability( seqs_file_path  )
251             rescue IOError
252               Util.fatal_error( PRG_NAME, "failed to read from [#{seqs_file_path}]: " + $!.to_s )
253             end
254             if verbose
255               Util.print_message( PRG_NAME, "Seq file: " + seqs_file_path )
256             end
257             log << "Seq file: " + seqs_file_path + NL
258           end
259
260           unless no_domains
261             domains_mapfile_name = get_file( mapping_files_dir , phylogeny_id, Constants::DOMAINS_TO_FORESTER_OUTFILE_SUFFIX  )
262             domains_mapfile_path = Util.canonical_path(mapping_files_dir, domains_mapfile_name)
263             begin
264               Util.check_file_for_readability( domains_mapfile_path )
265             rescue IOError
266               Util.fatal_error( PRG_NAME, "failed to read from [#{domains_mapfile_path}]: " + $!.to_s )
267             end
268             if verbose
269               Util.print_message( PRG_NAME, "Domains file: " + domains_mapfile_path )
270             end
271             log << "Domains file: " + domains_mapfile_path + NL
272           end
273
274           log <<  NL + NL
275
276           if no_seqs_files
277             FileUtils.cp(phylogeny_file, TMP_FILE_1)
278           else
279             cmd = decorator +
280             ' -t -p -f=m ' + phylogeny_file + ' ' +
281             seqs_file_path  + ' ' + TMP_FILE_1
282             if verbose
283               puts cmd
284             end
285             begin
286               execute_cmd( cmd, log )
287             rescue Exception
288               Util.fatal_error( PRG_NAME, 'error: ' + $!.to_s )
289             end
290           end
291
292           unless no_domains
293             cmd = decorator + ' ' + DECORATOR_OPTIONS_DOMAINS + ' ' +
294             '-f=d ' + TMP_FILE_1 + ' ' +
295             domains_mapfile_path + ' ' + TMP_FILE_2
296             if verbose
297               puts cmd
298             end
299             begin
300               execute_cmd( cmd, log )
301             rescue Exception
302               Util.fatal_error( PRG_NAME, 'error: ' + $!.to_s )
303             end
304           end
305
306           opts = DECORATOR_OPTIONS_SEQ_NAMES
307           if extr_bracketed_tc
308             opts += ' -tc'
309           end
310
311           if no_domains
312             cmd = decorator + ' ' + opts + ' -f=n ' + TMP_FILE_1 + ' ' +
313             ids_mapfile_path + ' ' + outfile
314             if verbose
315               puts cmd
316             end
317             begin
318               execute_cmd( cmd, log )
319             rescue Exception
320               Util.fatal_error( PRG_NAME, 'error: ' + $!.to_s )
321             end
322             File.delete( TMP_FILE_1 )
323           else
324             cmd = decorator + ' ' + opts + ' -f=n ' + TMP_FILE_2 + ' ' +
325             ids_mapfile_path + ' ' + outfile
326             if verbose
327               puts cmd
328             end
329             begin
330               execute_cmd( cmd, log )
331             rescue Exception
332               Util.fatal_error( PRG_NAME, 'error: ' + $!.to_s )
333             end
334             File.delete( TMP_FILE_1 )
335             File.delete( TMP_FILE_2 )
336           end
337         end
338       }
339       open( LOG_FILE, 'w' ) do | f |
340         f.write( log )
341       end
342       if verbose
343         puts
344       end
345       Util.print_message( PRG_NAME, 'OK' )
346       puts
347     end # def run
348
349     def execute_cmd( cmd, log )
350       log << 'executing ' + cmd + NL
351       IO.popen( cmd , 'r+' ) do | pipe |
352         pipe.close_write
353         log << pipe.read + NL + NL
354       end
355       if $?.to_i != 0
356         raise StandardError, "failed to execute " + cmd
357       end
358       sleep( SLEEP_TIME )
359     end
360
361     def get_file( files_in_dir, phylogeny_id, suffix_pattern )
362       begin
363         Util.get_matching_file( files_in_dir, phylogeny_id, suffix_pattern )
364       rescue Exception
365         Util.fatal_error( PRG_NAME, 'error: ' + $!.to_s )
366       end
367     end
368
369     def print_help()
370       puts "Usage:"
371       puts
372       puts "   " + PRG_NAME + ".rb [options] <suffix of in-trees to be decorated> <suffix for decorated out-trees> [mapping files directory, default: current dir]"
373       puts
374       puts "   " + PRG_NAME + ".rb [options] <input directory> <output directory> <mapping files directory>"
375       puts
376       puts "   required file  (in mapping files directory): " + "name mappings       : #{Constants::ID_MAP_FILE_SUFFIX}"
377       puts "   optional files (in mapping files directory): " + "sequences           : #{Constants::ID_NORMALIZED_FASTA_FILE_SUFFIX}"
378       puts "                                                " + "domain architectures: #{Constants::DOMAINS_TO_FORESTER_OUTFILE_SUFFIX}"
379       puts
380       puts "   options: -" + NO_DOMAINS_OPTION  + ": to not add domain architecture information (#{Constants::DOMAINS_TO_FORESTER_OUTFILE_SUFFIX} file)"
381       puts "            -" + NO_SEQS_OPTION   + ": to not add molecular sequence information (#{Constants::ID_NORMALIZED_FASTA_FILE_SUFFIX} file)"
382       puts "            -" + EXTRACT_BRACKETED_TAXONOMIC_CODE_OPTION  + ": to extract bracketed taxonomic codes, e.g. [NEMVE]"
383       puts "            -" + VERBOSE_OPTION  + " : verbose"
384       puts
385       puts "Examples: " + PRG_NAME + ".rb .xml _d.xml"
386       puts "          " + PRG_NAME + ".rb -#{NO_DOMAINS_OPTION} -#{NO_SEQS_OPTION} .xml _d.xml"
387       puts "          " + PRG_NAME + ".rb -#{NO_DOMAINS_OPTION} -#{NO_SEQS_OPTION} .xml _d.xml mappings_dir"
388       puts
389       puts "          " + PRG_NAME + ".rb in_trees_dir out_dir mappings_dir"
390       puts
391     end
392   end # class PhylogenyiesDecorator
393
394 end # module Evoruby