miga-base 0.2.0.9 → 0.2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -0
  3. data/actions/add_result.rb +37 -0
  4. data/actions/add_taxonomy.rb +63 -0
  5. data/actions/create_dataset.rb +49 -0
  6. data/actions/create_project.rb +46 -0
  7. data/actions/daemon.rb +50 -0
  8. data/actions/date.rb +14 -0
  9. data/actions/{download_dataset → download_dataset.rb} +5 -28
  10. data/actions/find_datasets.rb +41 -0
  11. data/actions/import_datasets.rb +47 -0
  12. data/actions/index_taxonomy.rb +46 -0
  13. data/actions/list_datasets.rb +50 -0
  14. data/actions/list_files.rb +43 -0
  15. data/actions/project_info.rb +40 -0
  16. data/actions/unlink_dataset.rb +28 -0
  17. data/bin/miga +129 -33
  18. data/lib/miga/daemon.rb +48 -34
  19. data/lib/miga/dataset.rb +7 -123
  20. data/lib/miga/dataset_result.rb +177 -0
  21. data/lib/miga/project.rb +32 -12
  22. data/lib/miga/version.rb +2 -2
  23. data/scripts/_distances_functions.bash +82 -0
  24. data/scripts/_distances_noref_nomulti.bash +96 -67
  25. data/scripts/_distances_ref_nomulti.bash +54 -85
  26. data/scripts/assembly.bash +16 -3
  27. data/scripts/clade_finding.bash +20 -18
  28. data/scripts/distances.bash +2 -1
  29. data/scripts/init.bash +2 -6
  30. data/scripts/subclades.bash +4 -5
  31. data/test/common_test.rb +2 -2
  32. data/test/daemon_test.rb +73 -1
  33. data/test/project_test.rb +26 -2
  34. data/test/taxonomy_test.rb +10 -0
  35. data/test/test_helper.rb +1 -1
  36. data/utils/subclades-compile.rb +4 -2
  37. data/utils/subclades.R +140 -158
  38. metadata +48 -44
  39. data/actions/add_result +0 -58
  40. data/actions/add_taxonomy +0 -83
  41. data/actions/create_dataset +0 -61
  42. data/actions/create_project +0 -67
  43. data/actions/daemon +0 -66
  44. data/actions/find_datasets +0 -61
  45. data/actions/import_datasets +0 -83
  46. data/actions/index_taxonomy +0 -68
  47. data/actions/list_datasets +0 -81
  48. data/actions/list_files +0 -63
  49. data/actions/unlink_dataset +0 -49
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # @package MiGA
4
+ # @license Artistic-2.0
5
+
6
+ require "miga/tax_index"
7
+
8
+ o = {q:true, format: :json}
9
+ OptionParser.new do |opt|
10
+ opt_banner(opt)
11
+ opt_object(opt, o, [:project])
12
+ opt.on("-i", "--index PATH",
13
+ "(Mandatory) File to create with the index."){ |v| o[:index]=v }
14
+ opt.on("-f", "--format STRING",
15
+ "Format of the index file. By default: #{o[:format]}. Supported: " +
16
+ "json, tab."){ |v| o[:format]=v.to_sym }
17
+ opt_filter_datasets(opt, o)
18
+ opt_common(opt, o)
19
+ end.parse!
20
+
21
+ ##=> Main <=
22
+ opt_require(o, project:"-P", index:"-i")
23
+
24
+ $stderr.puts "Loading project." unless o[:q]
25
+ p = MiGA::Project.load(o[:project])
26
+ raise "Impossible to load project: #{o[:project]}" if p.nil?
27
+
28
+ $stderr.puts "Loading datasets." unless o[:q]
29
+ ds = p.datasets
30
+ ds.select!{|d| not d.metadata[:tax].nil? }
31
+ ds = filter_datasets!(ds, o)
32
+
33
+ $stderr.puts "Indexing taxonomy." unless o[:q]
34
+ tax_index = MiGA::TaxIndex.new
35
+ ds.each { |d| tax_index << d }
36
+
37
+ $stderr.puts "Saving index." unless o[:q]
38
+ fh = File.open(o[:index], "w")
39
+ if o[:format]==:json
40
+ fh.print tax_index.to_json
41
+ elsif o[:format]==:tab
42
+ fh.print tax_index.to_tab
43
+ end
44
+ fh.close
45
+
46
+ $stderr.puts "Done." unless o[:q]
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # @package MiGA
4
+ # @license Artistic-2.0
5
+
6
+ o = {q:true, info:false, processing:false}
7
+ OptionParser.new do |opt|
8
+ opt_banner(opt)
9
+ opt_object(opt, o, [:project, :dataset_opt])
10
+ opt_filter_datasets(opt, o)
11
+ opt.on("-i", "--info",
12
+ "Print additional information on each dataset."){ |v| o[:info]=v }
13
+ opt.on("-p", "--processing",
14
+ "Print information on processing advance."){ |v| o[:processing]=v }
15
+ opt.on("-m", "--metadata STRING",
16
+ "Print name and metadata field only. If set, ignores -i."
17
+ ){ |v| o[:datum]=v }
18
+ opt_common(opt, o)
19
+ end.parse!
20
+
21
+ ##=> Main <=
22
+ opt_require(o, project:"-P")
23
+
24
+ $stderr.puts "Loading project." unless o[:q]
25
+ p = MiGA::Project.load(o[:project])
26
+ raise "Impossible to load project: #{o[:project]}" if p.nil?
27
+
28
+ $stderr.puts "Listing datasets." unless o[:q]
29
+ if o[:dataset].nil?
30
+ ds = p.datasets
31
+ elsif MiGA::Dataset.exist? p, o[:dataset]
32
+ ds = [p.dataset(o[:dataset])]
33
+ else
34
+ ds = []
35
+ end
36
+ ds = filter_datasets!(ds, o)
37
+
38
+ if not o[:datum].nil?
39
+ ds.each{|d| puts "#{d.name}\t#{d.metadata[ o[:datum] ] || "?"}"}
40
+ elsif o[:info]
41
+ puts MiGA::MiGA.tabulate(MiGA::Dataset.INFO_FIELDS, ds.map{ |d| d.info })
42
+ elsif o[:processing]
43
+ comp = ["undef","done","queued"]
44
+ puts MiGA::MiGA.tabulate([:name] + MiGA::Dataset.PREPROCESSING_TASKS,
45
+ ds.map{ |d| [d.name] + d.profile_advance.map{ |i| comp[i] } })
46
+ else
47
+ ds.each{|d| puts d.name}
48
+ end
49
+
50
+ $stderr.puts "Done." unless o[:q]
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # @package MiGA
4
+ # @license Artistic-2.0
5
+
6
+ o = {q:true, details:false, json:true}
7
+ OptionParser.new do |opt|
8
+ opt_banner(opt)
9
+ opt_object(opt, o, [:project, :dataset_opt])
10
+ opt.on("-i", "--info",
11
+ "If set, it prints additional details for each file."
12
+ ){ |v| o[:details]=v }
13
+ opt.on("--[no-]json",
14
+ "If set to no, excludes json files containing results metadata."
15
+ ){ |v| o[:json]=v }
16
+ opt_common(opt, o)
17
+ end.parse!
18
+
19
+ ##=> Main <=
20
+ opt_require(o, project:"-P")
21
+
22
+ $stderr.puts "Loading project." unless o[:q]
23
+ p = MiGA::Project.load(o[:project])
24
+ raise "Impossible to load project: #{o[:project]}" if p.nil?
25
+
26
+ if o[:dataset].nil?
27
+ results = p.results
28
+ else
29
+ $stderr.puts "Loading dataset." unless o[:q]
30
+ ds = p.dataset(o[:dataset])
31
+ raise "Impossible to load dataset: #{o[:dataset]}" if ds.nil?
32
+ results = ds.results
33
+ end
34
+
35
+ $stderr.puts "Listing files." unless o[:q]
36
+ results.each do |result|
37
+ puts "#{ "#{result.path}\t\t" if o[:details] }#{result.path}" if o[:json]
38
+ result.each_file do |k,f|
39
+ puts "#{ "#{result.path}\t#{k}\t" if o[:details] }#{result.dir}/#{f}"
40
+ end
41
+ end
42
+
43
+ $stderr.puts "Done." unless o[:q]
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # @package MiGA
4
+ # @license Artistic-2.0
5
+
6
+ o = {q:true, info:false, processing:false}
7
+ OptionParser.new do |opt|
8
+ opt_banner(opt)
9
+ opt_object(opt, o, [:project])
10
+ opt.on("-p", "--processing",
11
+ "Print information on processing advance."){ |v| o[:processing]=v }
12
+ opt.on("-m", "--metadata STRING",
13
+ "Print name and metadata field only. If set, ignores -i."
14
+ ){ |v| o[:datum]=v }
15
+ opt_common(opt, o)
16
+ end.parse!
17
+
18
+
19
+ ##=> Main <=
20
+ opt_require(o, project:"-P")
21
+
22
+ $stderr.puts "Loading project." unless o[:q]
23
+ p = MiGA::Project.load(o[:project])
24
+ raise "Impossible to load project: #{o[:project]}" if p.nil?
25
+
26
+ if not o[:datum].nil?
27
+ puts (p.metadata[ o[:datum] ] || "?")
28
+ elsif o[:processing]
29
+ keys = MiGA::Project.DISTANCE_TASKS + MiGA::Project.INCLADE_TASKS
30
+ puts MiGA::MiGA.tabulate([:task, :status], keys.map do |k|
31
+ [k, p.add_result(k, false).nil? ? "queued" : "done"]
32
+ end)
33
+ else
34
+ puts MiGA::MiGA.tabulate([:key, :value], p.metadata.data.keys.map do |k|
35
+ v = p.metadata[k]
36
+ [k, k==:datasets ? v.size : v]
37
+ end)
38
+ end
39
+
40
+ $stderr.puts "Done." unless o[:q]
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # @package MiGA
4
+ # @license Artistic-2.0
5
+
6
+ o = {q:true, remove:false}
7
+ OptionParser.new do |opt|
8
+ opt_banner(opt)
9
+ opt_object(opt, o)
10
+ opt.on("-r", "--remove", "Also remove all associated files.",
11
+ "By default, only unlinks from metadata."){ o[:remove]=true }
12
+ opt_common(opt, o)
13
+ end.parse!
14
+
15
+ ##=> Main <=
16
+ opt_require(o)
17
+
18
+ $stderr.puts "Loading project." unless o[:q]
19
+ p = MiGA::Project.load(o[:project])
20
+ raise "Impossible to load project: #{o[:project]}" if p.nil?
21
+
22
+ $stderr.puts "Unlinking dataset." unless o[:q]
23
+ raise "Dataset doesn't exist, aborting." unless
24
+ MiGA::Dataset.exist?(p, o[:dataset])
25
+ d = p.unlink_dataset(o[:dataset])
26
+ d.remove! if o[:remove]
27
+
28
+ $stderr.puts "Done." unless o[:q]
data/bin/miga CHANGED
@@ -1,48 +1,144 @@
1
1
  #!/usr/bin/env ruby
2
- #
2
+
3
3
  # @package MiGA
4
- # @author Luis M. Rodriguez-R <lmrodriguezr at gmail dot com>
5
- # @license artistic license 2.0
6
- # @update Oct-01-2015
7
- #
4
+ # @license Artistic-2.0
8
5
 
9
6
  $:.push File.expand_path("../lib", File.dirname(__FILE__))
10
7
 
11
8
  require "optparse"
12
9
  require "miga"
13
10
 
14
- execs = Dir[File.expand_path("../actions/*",
15
- File.dirname(__FILE__))].map{ |b| File.basename b }
16
-
17
- if %w{-v --version}.include? ARGV[0]
18
- puts MiGA::MiGA.VERSION
19
- elsif %w{-V --long-version}.include? ARGV[0]
20
- puts MiGA::MiGA.LONG_VERSION
21
- elsif %w{-C --citation}.include? ARGV[0]
22
- puts MiGA::MiGA.CITATION
23
- elsif execs.include? ARGV[0]
24
- task = ARGV.shift
25
- ARGV << "-h" if ARGV.empty?
26
- begin
27
- load File.expand_path("../actions/" + task, File.dirname(__FILE__))
28
- rescue => err
29
- $stderr.puts "Exception: #{err}\n\n"
30
- err.backtrace.each { |l| $stderr.puts l + "\n" }
31
- err
32
- end
11
+ ##=> Global variables <=
12
+
13
+ $task_desc = {
14
+ add_result: "Registers a result.",
15
+ add_taxonomy: "Registers taxonomic information for datasets.",
16
+ create_dataset: "Creates an empty dataset in a pre-existing MiGA project.",
17
+ create_project: "Creates an empty MiGA project.",
18
+ daemon: "Controls the daemon of a MiGA project.",
19
+ date: "Returns the current date in standard MiGA format.",
20
+ download_dataset: "Creates an empty dataset in a pre-existing MiGA project.",
21
+ find_datasets: "Finds unregistered datasets based on result files.",
22
+ import_datasets: "Link datasets (including results) from one project to "+
23
+ "another.",
24
+ index_taxonomy: "Creates a taxonomy-indexed list of the datasets.",
25
+ list_datasets: "Lists all registered datasets in an MiGA project.",
26
+ list_files: "Lists all registered files from the results of a dataset or a "+
27
+ "project.",
28
+ project_info: "Displays information about a MiGA project.",
29
+ unlink_dataset: "Removes a dataset from an MiGA project."
30
+ }
31
+
32
+ ##=> Functions <=
33
+
34
+ # OptParse banner
35
+ def opt_banner(opt)
36
+ opt.banner = <<BAN
37
+ #{$task_desc[$task]}
38
+
39
+ Usage: #{$0} #{$task} [options]
40
+ BAN
41
+ opt.separator ""
42
+ end
43
+
44
+ # OptParse flags that determine the object to load
45
+ def opt_object(opt, o, what=[:project, :dataset])
46
+ opt.on("-P", "--project PATH", "(Mandatory) Path to the project."
47
+ ){ |v| o[:project]=v } if what.include? :project
48
+ opt.on("-D", "--dataset PATH", "(Mandatory) Name of the dataset."
49
+ ){ |v| o[:dataset]=v } if what.include? :dataset
50
+ opt.on("-D", "--dataset PATH", "Name of the dataset."
51
+ ){ |v| o[:dataset]=v } if what.include? :dataset_opt
52
+ opt.on("-t", "--type STRING",
53
+ "Type of dataset. Recognized types include:",
54
+ *MiGA::Dataset.KNOWN_TYPES.map{ |k,v| "~ #{k}: #{v[:description]}" }
55
+ ){ |v| o[:type]=v.to_sym } if what.include? :dataset_type
56
+ opt.on("-t", "--type STRING",
57
+ "Type of dataset. Recognized types include:",
58
+ *MiGA::Project.KNOWN_TYPES.map{ |k,v| "~ #{k}: #{v[:description]}"}
59
+ ){ |v| o[:type]=v.to_sym } if what.include? :project_type
60
+ end
61
+
62
+ # OptParse flags common to all actions.
63
+ def opt_common(opt, o)
64
+ opt.on("-v", "--verbose",
65
+ "Print additional information to STDERR."){ o[:q]=false }
66
+ opt.on("-d", "--debug INT", "Print debugging information to STDERR.") do |v|
67
+ v.to_i>1 ? MiGA::MiGA.DEBUG_TRACE_ON : MiGA::MiGA.DEBUG_ON
68
+ end
69
+ opt.on("-h", "--help", "Display this screen.") do
70
+ puts opt
71
+ exit
72
+ end
73
+ opt.separator ""
74
+ end
75
+
76
+ # OptParse flags to filter lists of datasets.
77
+ def opt_filter_datasets(opt, o, what=[:ref, :multi, :taxonomy])
78
+ opt.on("--[no-]ref",
79
+ "If set, uses only reference (or only non-reference) datasets."
80
+ ){ |v| o[:ref]=v } if what.include? :ref
81
+ opt.on("--[no-]multi",
82
+ "If set, uses only multi-species (or only single-species) datasets."
83
+ ){ |v| o[:multi]=v } if what.include? :multi
84
+ opt.on("-t", "--taxonomy RANK:TAXON", "Filter by taxonomy."
85
+ ){ |v| o[:taxonomy]=MiGA::Taxonomy.new v } if what.include? :taxonomy
86
+ end
87
+
88
+ def opt_require(o, req={project:"-P", dataset:"-D"})
89
+ req.each do |k,v|
90
+ raise "#{v} is mandatory: please provide #{k}." if o[k].nil?
91
+ end
92
+ end
93
+
94
+ # Filters datasets by keys set in +opt_filter_datasets+.
95
+ def filter_datasets!(ds, o)
96
+ ds.select!{|d| d.is_ref? == o[:ref] } unless o[:ref].nil?
97
+ ds.select! do |d|
98
+ o[:multi] ? d.is_multi? : d.is_nonmulti?
99
+ end unless o[:multi].nil?
100
+ ds.select! do |d|
101
+ (not d.metadata[:tax].nil?) and d.metadata[:tax].is_in?(o[:taxonomy])
102
+ end unless o[:taxonomy].nil?
103
+ ds
104
+ end
105
+
106
+ ##=> Main <=
107
+
108
+ execs = $task_desc.keys.map{ |k| k.to_s }
109
+
110
+ case ARGV[0]
111
+ when "-v", "--version"
112
+ puts MiGA::MiGA.VERSION
113
+ when "-V", "--long-version"
114
+ puts MiGA::MiGA.LONG_VERSION
115
+ when "-C", "--citation"
116
+ puts MiGA::MiGA.CITATION
117
+ when *execs
118
+ $task = ARGV.shift.to_sym
119
+ ARGV << "-h" if ARGV.empty? and not [:date].include? $task
120
+ begin
121
+ load File.expand_path("../actions/#{$task}.rb", File.dirname(__FILE__))
122
+ rescue => err
123
+ $stderr.puts "Exception: #{err}\n\n"
124
+ err.backtrace.each { |l| $stderr.puts l + "\n" }
125
+ err
126
+ end
33
127
  else
34
- print <<HELP.gsub(/^ /,"")
35
- Microbial Genomes Atlas.
128
+ print <<HELP
129
+
130
+ Microbial Genomes Atlas.
131
+
132
+ Usage: #{$0} {action} [options]
36
133
 
37
- Usage: #{$0} {action} [options]
134
+ #{ MiGA::MiGA.tabulate([:action, :description], $task_desc.to_a).join("\n")}
38
135
 
39
- actions:#{ execs.map{ |e| "\n #{e}"}.join }
136
+ generic options:
137
+ -h, --help Display this screen.
138
+ -v, --version Show MiGA version.
139
+ -V, --long-version Show complete MiGA version.
140
+ -C, --citation How to cite MiGA.
40
141
 
41
- generic options:
42
- -h, --help Display this screen.
43
- -v, --version Show MiGA version.
44
- -V, --long-version Show complete MiGA version.
45
- -C, --citation How to cite MiGA.
46
142
  HELP
47
143
  end
48
144
 
data/lib/miga/daemon.rb CHANGED
@@ -26,6 +26,8 @@ class MiGA::Daemon < MiGA::MiGA
26
26
  attr_reader :jobs_to_run
27
27
  # Array of jobs currently running.
28
28
  attr_reader :jobs_running
29
+ # Integer indicating the current iteration.
30
+ attr_reader :loop_i
29
31
 
30
32
  ##
31
33
  # Initialize an unactive daemon for the MiGA::Project +project+. See #daemon
@@ -37,6 +39,7 @@ class MiGA::Daemon < MiGA::MiGA
37
39
  {:symbolize_names=>true})
38
40
  @jobs_to_run = []
39
41
  @jobs_running = []
42
+ @loop_i = -1
40
43
  end
41
44
 
42
45
  ##
@@ -55,13 +58,14 @@ class MiGA::Daemon < MiGA::MiGA
55
58
 
56
59
  ##
57
60
  # Set/get #options, where +k+ is the Symbol of the option and +v+ is the value
58
- # (or nil to use as getter). Returns new value.
59
- def runopts(k, v=nil)
61
+ # (or nil to use as getter). Skips consistency tests if +force+. Returns new
62
+ # value.
63
+ def runopts(k, v=nil, force=false)
60
64
  k = k.to_sym
61
65
  unless v.nil?
62
66
  v = v.to_i if [:latency, :maxjobs, :ppn].include? k
63
67
  raise "Daemon's #{k} cannot be set to zero." if
64
- v.is_a? Integer and v==0
68
+ !force and v.is_a? Integer and v==0
65
69
  @runopts[k] = v
66
70
  end
67
71
  @runopts[k]
@@ -80,20 +84,20 @@ class MiGA::Daemon < MiGA::MiGA
80
84
  def ppn() runopts(:ppn) ; end
81
85
 
82
86
  ##
83
- # Initializes the daemon.
84
- def start() daemon("start") ; end
87
+ # Initializes the daemon with +opts+.
88
+ def start(opts=[]) daemon("start", opts) ; end
85
89
 
86
90
  ##
87
- # Stops the daemon.
88
- def stop() daemon("stop") ; end
91
+ # Stops the daemon with +opts+.
92
+ def stop(opts=[]) daemon("stop", opts) ; end
89
93
 
90
94
  ##
91
- # Restarts the daemon.
92
- def restart() daemon("restart") ; end
95
+ # Restarts the daemon with +opts+.
96
+ def restart(opts=[]) daemon("restart", opts) ; end
93
97
 
94
98
  ##
95
- # Returns the status of the daemon.
96
- def status() daemon("status") ; end
99
+ # Returns the status of the daemon with +opts+.
100
+ def status(opts=[]) daemon("status", opts) ; end
97
101
 
98
102
  ##
99
103
  # Launches the +task+ with options +opts+ (as command-line arguments).
@@ -102,26 +106,7 @@ class MiGA::Daemon < MiGA::MiGA
102
106
  options = default_options
103
107
  opts.unshift(task)
104
108
  options[:ARGV] = opts
105
- Daemons.run_proc("MiGA:#{project.name}", options) do
106
- say "-----------------------------------"
107
- say "MiGA:#{project.name} launched."
108
- say "-----------------------------------"
109
- loop_i = 0
110
- loop do
111
- loop_i += 1
112
- declare_alive
113
- check_datasets
114
- check_project
115
- flush!
116
- if loop_i==12
117
- say "Housekeeping for sanity"
118
- loop_i = 0
119
- purge!
120
- project.load
121
- end
122
- sleep(latency)
123
- end
124
- end
109
+ Daemons.run_proc("MiGA:#{project.name}", options) { loop { in_loop } }
125
110
  end
126
111
 
127
112
  ##
@@ -135,9 +120,14 @@ class MiGA::Daemon < MiGA::MiGA
135
120
  ##
136
121
  # Traverse datasets
137
122
  def check_datasets
138
- project.each_dataset do |ds|
139
- to_run = ds.next_preprocessing(true)
140
- queue_job(to_run, ds) unless to_run.nil?
123
+ project.each_dataset do |n, ds|
124
+ if ds.nil?
125
+ say "Warning: Dataset #{n} listed but not loaded, reloading project."
126
+ project.load
127
+ else
128
+ to_run = ds.next_preprocessing(true)
129
+ queue_job(to_run, ds) unless to_run.nil?
130
+ end
141
131
  end
142
132
  end
143
133
 
@@ -145,6 +135,7 @@ class MiGA::Daemon < MiGA::MiGA
145
135
  # Check if all reference datasets are pre-processed. If yes, check the
146
136
  # project-level tasks
147
137
  def check_project
138
+ return if project.dataset_names.empty?
148
139
  if project.done_preprocessing?(false)
149
140
  to_run = project.next_distances(true)
150
141
  to_run = project.next_inclade(true) if to_run.nil?
@@ -230,6 +221,29 @@ class MiGA::Daemon < MiGA::MiGA
230
221
  end
231
222
  end
232
223
 
224
+ ##
225
+ # Run one loop step.
226
+ def in_loop
227
+ if loop_i == -1
228
+ say "-----------------------------------"
229
+ say "MiGA:#{project.name} launched."
230
+ say "-----------------------------------"
231
+ @loop_i = 0
232
+ end
233
+ @loop_i += 1
234
+ declare_alive
235
+ check_datasets
236
+ check_project
237
+ flush!
238
+ if loop_i==12
239
+ say "Housekeeping for sanity"
240
+ @loop_i = 0
241
+ purge!
242
+ project.load
243
+ end
244
+ sleep(latency)
245
+ end
246
+
233
247
  ##
234
248
  # Send a datestamped message to the log.
235
249
  def say(*opts)