biopsy 0.1.0.alpha → 0.1.1.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +24 -1
- data/lib/biopsy/base_extensions.rb +2 -5
- data/lib/biopsy/experiment.rb +58 -21
- data/lib/biopsy/objective_handler.rb +21 -50
- data/lib/biopsy/optimisers/parameter_sweeper.rb +14 -1
- data/lib/biopsy/optimisers/tabu_search.rb +32 -4
- data/lib/biopsy/settings.rb +4 -23
- data/lib/biopsy/target.rb +71 -56
- data/lib/biopsy/version.rb +1 -1
- data/lib/biopsy.rb +0 -1
- data/test/helper.rb +35 -58
- data/test/test_experiment.rb +14 -27
- data/test/test_objective_handler.rb +10 -13
- data/test/test_settings.rb +2 -11
- data/test/test_target.rb +29 -18
- metadata +32 -52
- data/lib/biopsy/domain.rb +0 -156
- data/test/test_domain.rb +0 -61
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e4b7eed6bdea005473b5bab0e14b2d05eadb7dbf
|
4
|
+
data.tar.gz: 788a2e6a6bd4f67d5d35c39e5b710634d72c45cb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ddfe6c7044e51f9f59774b2750670cc5e44f88ea61cacdcdf42f5cb8e8000979da68f9c7326d8c3fcf05aea5125c5d12e27c23a9b9be73de55306fe5b71b1590
|
7
|
+
data.tar.gz: 1e8ad4d20403f7c6abf300663ad2dd670518c67d93626573011cdf9220ebc38ee330952af24e175b16e12d3ab99562f72ab9ecef286a707333af388f0c331be5
|
data/README.md
CHANGED
@@ -7,6 +7,28 @@ Biopsy is a framework for optimising any program or pipeline which produces a me
|
|
7
7
|
|
8
8
|
A simple example of the power of this approach is *de-novo* transcriptome assembly. Typically, the assembly process takes many GB of data as input, uses many GB of RAM and takes many hours to complete. This prevents researchers from performing full parameter sweeps, and they are therefore forced to use word-of-mouth and very basic optimisation to choose assembler settings. Assemblotron, which uses the Biopsy framework, can fully optimise any *de-novo* assembler to produce the optimal assembly possible given a particular input. This typically takes little more time than running a single assembly.
|
9
9
|
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Make sure you have Ruby installed, then:
|
13
|
+
|
14
|
+
`gem install biopsy --pre`
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
[]
|
19
|
+
|
20
|
+
Detailed usage instructions are on the wiki. Here's a quick overview:
|
21
|
+
|
22
|
+
1. Define your optimisation target. This is a program or pipeline you want to optimise, and you define it by filling in a template [YAML file](http://en.wikipedia.org/wiki/YAML). Easy!
|
23
|
+
2. Define your objective function. This is a program that analyses the output of your program and gives it a score. You define it by writing a small amount of Ruby code. Don't worry - there's a template and detailed instructions on the wiki.
|
24
|
+
3. Run Biopsy, and wait while the experiment runs.
|
25
|
+
|
26
|
+
### Command line examples
|
27
|
+
|
28
|
+
`biopsy list targets`
|
29
|
+
`biopsy list objectives`
|
30
|
+
`biposy run --target test_target --objective test_objective --input test_file.txt --time-limit 24h`
|
31
|
+
|
10
32
|
## Development status
|
11
33
|
|
12
34
|
[![Gem Version](https://badge.fury.io/rb/biopsy.png)][gem]
|
@@ -46,4 +68,5 @@ Documentation is in development and will be released with the beta.
|
|
46
68
|
|
47
69
|
### Citation
|
48
70
|
|
49
|
-
This is *pre-release*, *pre-publication* academic software. In lieu of a paper to cite, please cite this Github repo
|
71
|
+
This is *pre-release*, *pre-publication* academic software. In lieu of a paper to cite, please cite this Github repo and/or the [Figshare DOI (http://dx.doi.org/10.6084/m9.figshare.790660
|
72
|
+
)](http://dx.doi.org/10.6084/m9.figshare.790660) if your use of the software leads to a publication.
|
@@ -32,6 +32,7 @@ class Hash
|
|
32
32
|
target = dup
|
33
33
|
target.inject({}) do |memo, (key, value)|
|
34
34
|
value = value.deep_symbolize if value.is_a?(Hash)
|
35
|
+
value = value.map{ |x| x.is_a?(Hash) ? x.deep_symbolize : x } if value.is_a?(Array)
|
35
36
|
memo[(key.to_sym rescue key) || key] = value
|
36
37
|
memo
|
37
38
|
end
|
@@ -54,11 +55,7 @@ class Array
|
|
54
55
|
# Requires the array to contain only objects of class Fixnum.
|
55
56
|
# If any other class is encountered, an error will be raised.
|
56
57
|
def mean
|
57
|
-
self.
|
58
|
-
end
|
59
|
-
|
60
|
-
def sum
|
61
|
-
self.inject(0, :+)
|
58
|
+
self.inject(0, :+) / self.size.to_f
|
62
59
|
end
|
63
60
|
|
64
61
|
end # Array
|
data/lib/biopsy/experiment.rb
CHANGED
@@ -19,13 +19,12 @@ module Biopsy
|
|
19
19
|
attr_reader :inputs, :outputs, :retain_intermediates, :target, :start, :algorithm
|
20
20
|
|
21
21
|
# Returns a new Experiment
|
22
|
-
def initialize(target_name,
|
23
|
-
@domain = Domain.new domain_name
|
22
|
+
def initialize(target_name, start=nil, algorithm=nil)
|
24
23
|
@start = start
|
25
24
|
@algorithm = algorithm
|
26
25
|
|
27
26
|
self.load_target target_name
|
28
|
-
@objective = ObjectiveHandler.new
|
27
|
+
@objective = ObjectiveHandler.new @target
|
29
28
|
self.select_algorithm
|
30
29
|
self.select_starting_point
|
31
30
|
@scores = {}
|
@@ -44,19 +43,23 @@ module Biopsy
|
|
44
43
|
|
45
44
|
# Return a random set of parameters from the parameter space.
|
46
45
|
def random_start_point
|
47
|
-
Hash[@target.
|
46
|
+
Hash[@target.parameters.map { |p, r| [p, r.sample] }]
|
48
47
|
end
|
49
48
|
|
50
49
|
# select the optimisation algorithm to use
|
51
50
|
def select_algorithm
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
max = Settings.instance.sweep_cutoff
|
52
|
+
n = @target.count_parameter_permutations
|
53
|
+
if n < max
|
54
|
+
@algorithm = ParameterSweeper.new(@target.parameters)
|
55
|
+
else
|
56
|
+
@algorithm = TabuSearch.new(@target.parameters)
|
57
|
+
end
|
55
58
|
end
|
56
59
|
|
57
60
|
# load the target named +:target_name+
|
58
61
|
def load_target target_name
|
59
|
-
@target = Target.new
|
62
|
+
@target = Target.new
|
60
63
|
@target.load_by_name target_name
|
61
64
|
end
|
62
65
|
|
@@ -82,20 +85,54 @@ module Biopsy
|
|
82
85
|
# encompassing the program, objective(s) and optimiser.
|
83
86
|
# Returns the output of the optimiser.
|
84
87
|
def run_iteration
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
result =
|
92
|
-
|
93
|
-
|
94
|
-
|
88
|
+
# create temp dir
|
89
|
+
Dir.chdir(self.create_tempdir) do
|
90
|
+
# run the target
|
91
|
+
raw_output = @target.run @current_params
|
92
|
+
# evaluate with objectives
|
93
|
+
param_key = @current_params.to_s
|
94
|
+
result = nil
|
95
|
+
if @scores.has_key? param_key
|
96
|
+
result = @scores[param_key]
|
97
|
+
else
|
98
|
+
result = @objective.run_for_output raw_output
|
99
|
+
@iteration_count += 1
|
100
|
+
end
|
101
|
+
@scores[@current_params.to_s] = result
|
102
|
+
# get next steps from optimiser
|
103
|
+
@current_params = @algorithm.run_one_iteration(@current_params, result)
|
104
|
+
end
|
105
|
+
self.cleanup
|
106
|
+
end
|
107
|
+
|
108
|
+
def cleanup
|
109
|
+
# TODO: make this work
|
110
|
+
# remove all but essential files
|
111
|
+
if Settings.instance.keep_intermediates
|
112
|
+
@objectives.values.each{ |objective| essential_files += objective.essential_files }
|
113
|
+
end
|
114
|
+
Dir["*"].each do |file|
|
115
|
+
next if File.directory? file
|
116
|
+
if essential_files && essential_files.include?(file)
|
117
|
+
`gzip #{file}` if Settings.instance.gzip_intermediates
|
118
|
+
FileUtils.mv("#{file}.gz", '../output')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
FileUtils.rm_rf @last_tempdir
|
122
|
+
end
|
123
|
+
|
124
|
+
# create a guaranteed random temporary directory for storing outputs
|
125
|
+
# return the dirname
|
126
|
+
def create_tempdir
|
127
|
+
token = loop do
|
128
|
+
# generate random dirnames until we find one that
|
129
|
+
# doesn't exist
|
130
|
+
test_token = SecureRandom.hex
|
131
|
+
break test_token unless File.exists? test_token
|
95
132
|
end
|
96
|
-
|
97
|
-
|
98
|
-
|
133
|
+
Dir.mkdir(token)
|
134
|
+
@last_tempdir = token
|
135
|
+
return token
|
99
136
|
end
|
100
137
|
|
101
138
|
end # end of class RunHandler
|
@@ -39,10 +39,8 @@ module Biopsy
|
|
39
39
|
attr_reader :last_tempdir
|
40
40
|
attr_accessor :objectives
|
41
41
|
|
42
|
-
def initialize
|
43
|
-
@domain = domain
|
42
|
+
def initialize target
|
44
43
|
@target = target
|
45
|
-
base_dir = Settings.instance.base_dir
|
46
44
|
@objectives_dir = Settings.instance.objectives_dir.first
|
47
45
|
@objectives = {}
|
48
46
|
$LOAD_PATH.unshift(@objectives_dir)
|
@@ -76,12 +74,14 @@ module Biopsy
|
|
76
74
|
|
77
75
|
# Run a specific +:objective+ on the +:output+ of a target
|
78
76
|
# with max +:threads+.
|
79
|
-
def run_objective(objective, name,
|
77
|
+
def run_objective(objective, name, raw_output, output_files, threads)
|
80
78
|
begin
|
81
|
-
# output is a
|
82
|
-
#
|
83
|
-
#
|
84
|
-
|
79
|
+
# output is a, array: [raw_output, output_files].
|
80
|
+
# output_files is a hash containing the absolute paths
|
81
|
+
# to file(s) output by the target in the format expected by the
|
82
|
+
# objective function(s), with keys as the keys expected by the
|
83
|
+
# objective function
|
84
|
+
return objective.run(raw_output, output_files, threads)
|
85
85
|
rescue NotImplementedError => e
|
86
86
|
puts "Error: objective function #{objective.class} does not implement the run() method"
|
87
87
|
puts "Please refer to the documentation for instructions on adding objective functions"
|
@@ -90,7 +90,6 @@ module Biopsy
|
|
90
90
|
end
|
91
91
|
|
92
92
|
# Perform a euclidean distance dimension reduction of multiple objectives
|
93
|
-
# using weighting specified in the domain definition.
|
94
93
|
def dimension_reduce(results)
|
95
94
|
# calculate the weighted Euclidean distance from optimal
|
96
95
|
# d(p, q) = \sqrt{(p_1 - q_1)^2 + (p_2 - q_2)^2+...+(p_i - q_i)^2+...+(p_n - q_n)^2}
|
@@ -107,40 +106,26 @@ module Biopsy
|
|
107
106
|
end
|
108
107
|
|
109
108
|
# Run all objectives functions for +:output+.
|
110
|
-
def run_for_output(
|
109
|
+
def run_for_output(raw_output, threads=6, allresults=false)
|
111
110
|
# check output files exist
|
112
|
-
|
113
|
-
|
114
|
-
|
111
|
+
output_files = {}
|
112
|
+
@target.output.each_pair do |key, glob|
|
113
|
+
files = Dir[glob]
|
114
|
+
zerosize = files.reduce(false) { |empty, f| File.size(f) == 0 }
|
115
|
+
if files.empty? || zerosize
|
116
|
+
puts Dir.pwd
|
117
|
+
raise ObjectiveHandlerError.new "output files for #{key} matching #{glob} do not exist or are empty"
|
115
118
|
return nil
|
116
119
|
end
|
120
|
+
output_files[key] = files.map { |f| File.expand_path(f) }
|
117
121
|
end
|
122
|
+
|
118
123
|
# run all objectives for output
|
119
124
|
results = {}
|
120
|
-
|
121
|
-
|
122
|
-
@objectives.each_pair do |name, objective|
|
123
|
-
results[name] = self.run_objective(objective, name, output, threads)
|
124
|
-
end
|
125
|
-
if cleanup == 1
|
126
|
-
# remove all but essential files
|
127
|
-
essential_files = @domain.keep_intermediates
|
128
|
-
if essential_files
|
129
|
-
@objectives.values.each{ |objective| essential_files += objective.essential_files }
|
130
|
-
end
|
131
|
-
Dir["*"].each do |file|
|
132
|
-
next if File.directory? file
|
133
|
-
if essential_files && essential_files.include?(file)
|
134
|
-
`gzip #{file}` if @domain.gzip_intermediates
|
135
|
-
FileUtils.mv("#{file}.gz", '..')
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
if cleanup
|
141
|
-
# clean up temp dir
|
142
|
-
FileUtils.rm_rf @last_tempdir
|
125
|
+
@objectives.each_pair do |name, objective|
|
126
|
+
results[name] = self.run_objective(objective, name, raw_output, output_files, threads)
|
143
127
|
end
|
128
|
+
|
144
129
|
if allresults
|
145
130
|
return {:results => results,
|
146
131
|
:reduced => self.dimension_reduce(results)}
|
@@ -151,20 +136,6 @@ module Biopsy
|
|
151
136
|
end
|
152
137
|
end
|
153
138
|
|
154
|
-
# create a guaranteed random temporary directory for storing outputs
|
155
|
-
# return the dirname
|
156
|
-
def create_tempdir
|
157
|
-
token = loop do
|
158
|
-
# generate random dirnames until we find one that
|
159
|
-
# doesn't exist
|
160
|
-
test_token = SecureRandom.hex
|
161
|
-
break test_token unless File.exists? test_token
|
162
|
-
end
|
163
|
-
Dir.mkdir(token)
|
164
|
-
@last_tempdir = token
|
165
|
-
return token
|
166
|
-
end
|
167
|
-
|
168
139
|
end
|
169
140
|
|
170
141
|
end
|
@@ -42,8 +42,12 @@ module Biopsy
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
+
def setup *args
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
45
49
|
# return the next parameter set to evaluate
|
46
|
-
def run_one_iteration
|
50
|
+
def run_one_iteration *args
|
47
51
|
@combinations.pop
|
48
52
|
rescue
|
49
53
|
nil
|
@@ -62,5 +66,14 @@ module Biopsy
|
|
62
66
|
generate_combinations(index + 1, opts)
|
63
67
|
end
|
64
68
|
end
|
69
|
+
|
70
|
+
def knows_starting_point?
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def select_starting_point
|
75
|
+
self.run_one_iteration
|
76
|
+
end
|
77
|
+
|
65
78
|
end
|
66
79
|
end
|
@@ -201,8 +201,10 @@ module Biopsy
|
|
201
201
|
@backtracks = 1.0
|
202
202
|
|
203
203
|
# convergence
|
204
|
-
@num_threads =
|
204
|
+
@num_threads = 5
|
205
205
|
@threads = []
|
206
|
+
@convergence_alpha = 0.05
|
207
|
+
@global_best = {:parameters => nil, :score => nil}
|
206
208
|
|
207
209
|
end # initialize
|
208
210
|
|
@@ -249,6 +251,8 @@ module Biopsy
|
|
249
251
|
thread.loaded = false
|
250
252
|
end
|
251
253
|
@current_thread = @num_threads - 2
|
254
|
+
# adjust the alpha for multiple testing in convergence
|
255
|
+
@adjusted_alpha = @convergence_alpha / @num_threads
|
252
256
|
end
|
253
257
|
|
254
258
|
def load_next_thread
|
@@ -276,6 +280,13 @@ module Biopsy
|
|
276
280
|
else
|
277
281
|
@iterations_since_best += 1
|
278
282
|
end
|
283
|
+
if @global_best[:score].nil? || @best[:score] > @global_best[:score]
|
284
|
+
@global_best = @best.clone
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def best
|
289
|
+
@global_best
|
279
290
|
end
|
280
291
|
|
281
292
|
# use probability distributions to define the
|
@@ -385,9 +396,26 @@ module Biopsy
|
|
385
396
|
# check termination conditions
|
386
397
|
# and return true if met
|
387
398
|
def finished?
|
388
|
-
return false
|
389
|
-
|
390
|
-
|
399
|
+
return false unless @threads.all? { |t| t.recent_scores.size == @jump_cutoff }
|
400
|
+
probabilities = self.recent_scores_combination_test
|
401
|
+
n_significant = 0
|
402
|
+
probabilities.each do |mann_u, levene|
|
403
|
+
if mann_u <= @adjusted_alpha && levene <= @convergence_alpha
|
404
|
+
n_significant += 1
|
405
|
+
end
|
406
|
+
end
|
407
|
+
finish = n_significant >= probabilities.size * 0.5
|
408
|
+
end
|
409
|
+
|
410
|
+
# returns a matrix of correlation probabilities for recent
|
411
|
+
# scores between all threads
|
412
|
+
def recent_scores_combination_test
|
413
|
+
combinations =
|
414
|
+
@threads.map{ |t| t.recent_scores.to_scale }.combination(2).to_a
|
415
|
+
combinations.map do |a, b|
|
416
|
+
[Statsample::Test.u_mannwhitney(a, b).probability_exact,
|
417
|
+
Statsample::Test::Levene.new([a,b]).probability]
|
418
|
+
end
|
391
419
|
end
|
392
420
|
|
393
421
|
# True if this algorithm chooses its own starting point
|
data/lib/biopsy/settings.rb
CHANGED
@@ -25,11 +25,11 @@ module Biopsy
|
|
25
25
|
|
26
26
|
attr_accessor :base_dir
|
27
27
|
attr_accessor :target_dir
|
28
|
-
attr_accessor :domain_dir
|
29
|
-
attr_accessor :domain
|
30
28
|
attr_accessor :objectives_dir
|
31
29
|
attr_accessor :objectives_subset
|
32
30
|
attr_accessor :sweep_cutoff
|
31
|
+
attr_accessor :keep_intermediates
|
32
|
+
attr_accessor :gzip_intermediates
|
33
33
|
|
34
34
|
def initialize
|
35
35
|
self.set_defaults
|
@@ -40,11 +40,11 @@ module Biopsy
|
|
40
40
|
@config_file = '~/.biopsyrc'
|
41
41
|
@base_dir = ['.']
|
42
42
|
@target_dir = ['targets']
|
43
|
-
@domain_dir = ['domains']
|
44
|
-
@domain = 'test_domain'
|
45
43
|
@objectives_dir = ['objectives']
|
46
44
|
@objectives_subset = nil
|
47
45
|
@sweep_cutoff = 100
|
46
|
+
@keep_intermediates = false
|
47
|
+
@gzip_intermediates = false
|
48
48
|
end
|
49
49
|
|
50
50
|
# Loads settings from a YAML config file. If no file is
|
@@ -86,25 +86,6 @@ module Biopsy
|
|
86
86
|
all_settings.to_yaml
|
87
87
|
end
|
88
88
|
|
89
|
-
# Locate the first YAML config file whose name
|
90
|
-
# excluding extension matches +:name+ (case insensitive)
|
91
|
-
# in dirs listed by the +:dir_key+ setting.
|
92
|
-
def locate_config(dir_key, name)
|
93
|
-
dir_key = "@#{dir_key.to_s}".to_sym
|
94
|
-
unless self.instance_variables.include? dir_key
|
95
|
-
raise SettingsError.new "no setting found for compulsory key #{dir_key}"
|
96
|
-
end
|
97
|
-
self.instance_variable_get(dir_key).each do |dir|
|
98
|
-
Dir.chdir ::File.expand_path(dir) do
|
99
|
-
Dir[name + '.yml'].each do |file|
|
100
|
-
return ::File.expand_path(file) if ::File.basename(file, '.yml').downcase == name.downcase
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
nil
|
106
|
-
end
|
107
|
-
|
108
89
|
end # Settings
|
109
90
|
|
110
91
|
end # Biopsy
|
data/lib/biopsy/target.rb
CHANGED
@@ -5,25 +5,13 @@ module Biopsy
|
|
5
5
|
|
6
6
|
class Target
|
7
7
|
require 'yaml'
|
8
|
-
require '
|
9
|
-
|
10
|
-
# array of input files expected by the target constructor
|
11
|
-
attr_accessor :input_files
|
12
|
-
# array of output files to keep for submission to objective
|
13
|
-
# functions during optimisation
|
14
|
-
attr_accessor :output_files
|
15
|
-
# hash mapping parameters to the ranges of values they can take
|
16
|
-
attr_reader :parameter_ranges
|
17
|
-
# path to the constructor code
|
18
|
-
attr_reader :constructor_path
|
19
|
-
attr_reader :domain
|
8
|
+
require 'set'
|
20
9
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
10
|
+
attr_accessor :parameters
|
11
|
+
attr_accessor :options
|
12
|
+
attr_accessor :output
|
13
|
+
attr_accessor :name
|
14
|
+
attr_reader :constructor_path
|
27
15
|
|
28
16
|
# load target with +name+.
|
29
17
|
def load_by_name name
|
@@ -31,17 +19,9 @@ module Biopsy
|
|
31
19
|
raise TargetLoadError.new("Target definition file does not exist for #{name}") if path.nil?
|
32
20
|
config = YAML::load_file(path)
|
33
21
|
raise TargetLoadError.new("Target definition file #{path} is not valid YAML") if config.nil?
|
34
|
-
|
35
|
-
if missing
|
36
|
-
msg = "Target definition file #{path} is missing required fields: #{missing}"
|
37
|
-
raise TargetLoadError.new(msg)
|
38
|
-
end
|
39
|
-
errors = self.validate_config config
|
40
|
-
unless errors.empty?
|
41
|
-
raise TargetLoadError.new("Target definition file #{path} contains the following errors:\n - #{errors.join("\n - ")}")
|
42
|
-
end
|
22
|
+
config = config.deep_symbolize
|
43
23
|
self.store_config config
|
44
|
-
self.check_constructor
|
24
|
+
self.check_constructor name
|
45
25
|
self.load_constructor
|
46
26
|
end
|
47
27
|
|
@@ -49,48 +29,52 @@ module Biopsy
|
|
49
29
|
# to the definition YAML file. All +:target_dir+s defined in Settings are
|
50
30
|
# searched and the first matching YAML file is loaded.
|
51
31
|
def locate_definition name
|
52
|
-
|
32
|
+
self.locate_file(name + '.yml')
|
53
33
|
end
|
54
34
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
35
|
+
# store the values in +:config+, checking they are valid
|
36
|
+
def store_config config
|
37
|
+
required = Set.new([:name, :parameters, :output])
|
38
|
+
missing = required - config.keys
|
39
|
+
raise TargetLoadError.new("Required keys are missing from target definition: #{missing.to_a.join(",")}") unless missing.empty?
|
40
|
+
config.each_pair do |param, data|
|
41
|
+
case param
|
42
|
+
when :name
|
43
|
+
raise TargetLoadError.new('Target name must be a string') unless data.is_a? String
|
44
|
+
@name = data
|
45
|
+
when :shortname
|
46
|
+
raise TargetLoadError.new('Target shortname must be a string') unless data.is_a? String
|
47
|
+
@shortname = data
|
48
|
+
when :parameters
|
49
|
+
self.generate_parameters data
|
50
|
+
when :output
|
51
|
+
raise TargetLoadError.new('Target output must be a hash') unless data.is_a?(Hash)
|
52
|
+
@output = data
|
65
53
|
end
|
66
54
|
end
|
67
|
-
missing
|
68
|
-
end
|
69
|
-
|
70
|
-
# validate the config against the domain definition. Return an array
|
71
|
-
# whose length will be the number of errors found. Thus an array of
|
72
|
-
# length 0 indicates that the config is valid according to the domain
|
73
|
-
# specification.
|
74
|
-
def validate_config config
|
75
|
-
@domain.target_valid? config
|
76
55
|
end
|
77
56
|
|
78
|
-
#
|
79
|
-
def
|
80
|
-
|
81
|
-
|
57
|
+
# Locate a file with name in one of the target_dirs
|
58
|
+
def locate_file name
|
59
|
+
Settings.instance.target_dir.each do |dir|
|
60
|
+
Dir.chdir File.expand_path(dir) do
|
61
|
+
return File.expand_path(name) if File.exists? name
|
62
|
+
end
|
82
63
|
end
|
64
|
+
raise TargetLoadError.new("Couldn't find file #{name}")
|
65
|
+
nil
|
83
66
|
end
|
84
67
|
|
85
68
|
# Validate the constructor. True if valid, false otherwise.
|
86
|
-
def check_constructor
|
87
|
-
|
69
|
+
def check_constructor name
|
70
|
+
@constructor_path = self.locate_file name + '.rb'
|
71
|
+
raise TargetLoadError.new("constructor path is not defined for this target") if @constructor_path.nil?
|
88
72
|
self.valid_ruby? @constructor_path
|
89
73
|
end
|
90
74
|
|
91
75
|
# Load constructor
|
92
76
|
def load_constructor
|
93
|
-
require
|
77
|
+
require @constructor_path
|
94
78
|
file_name = File.basename(@constructor_path, '.rb')
|
95
79
|
constructor_name = file_name.camelize
|
96
80
|
@constructor = Module.const_get(constructor_name).new
|
@@ -104,10 +88,41 @@ module Biopsy
|
|
104
88
|
# true if file is valid ruby
|
105
89
|
def valid_ruby? file
|
106
90
|
return false unless ::File.exists? file
|
107
|
-
result = `ruby -c #{file}
|
91
|
+
result = `ruby -c #{file} > /dev/null 2>&1`
|
108
92
|
!result.size.zero?
|
109
93
|
end
|
110
94
|
|
95
|
+
# convert parameter specification to a hash of arrays and ranges
|
96
|
+
def generate_parameters params
|
97
|
+
@parameters = {}
|
98
|
+
@options = {}
|
99
|
+
params.each_pair do |param, data|
|
100
|
+
if data[:opt]
|
101
|
+
# optimise this parameter
|
102
|
+
if data[:values]
|
103
|
+
# definition has provided an array of values
|
104
|
+
raise TargetLoadError.new("'values' for parameter #{param} is not an array") unless data[:values].is_a? Array
|
105
|
+
@parameters[param] = data[:values]
|
106
|
+
else
|
107
|
+
# definition has specified a range
|
108
|
+
min, max, step = data[:min], data[:max], data[:step]
|
109
|
+
raise TargetLoadError.new("min and max must be set for parameter #{param}") unless min && max
|
110
|
+
range = (min..max)
|
111
|
+
range = range.step(step) if step
|
112
|
+
@parameters[param] = range.to_a
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# present option to user
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# return the total number of possible permutations of
|
122
|
+
def count_parameter_permutations
|
123
|
+
@parameters.each_pair.map{ |k, v| v }.reduce(1) { |n, arr| n * arr.size }
|
124
|
+
end
|
125
|
+
|
111
126
|
end # end of class Domain
|
112
127
|
|
113
128
|
end # end of module Biopsy
|
data/lib/biopsy/version.rb
CHANGED