flex-cartesian 1.3.1 → 2.0.1.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -1
- data/LICENSE +6 -0
- data/README.md +133 -439
- data/lib/analyzer.rb +48 -0
- data/lib/analyzers/morris.rb +268 -0
- data/lib/flex-cartesian/flex-cartesian-analyzer.rb +16 -0
- data/lib/flex-cartesian/flex-cartesian-core.rb +624 -0
- data/lib/flex-cartesian/flex-cartesian-deprecations.rb +13 -0
- data/lib/flex-cartesian/flex-cartesian-io.rb +192 -0
- data/lib/flex-cartesian/flex-cartesian-utilities.rb +126 -0
- data/lib/flex-cartesian.rb +13 -336
- data/lib/version.rb +3 -0
- data/lib/visualization/html.rb +217 -0
- metadata +49 -26
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
module FlexCartesianCore
|
|
2
|
+
|
|
3
|
+
attr_reader :function_results, :derived, :names, :dimensiality, :dimensions, :struct, :levels, :index_show, :log
|
|
4
|
+
|
|
5
|
+
def index_show
|
|
6
|
+
@index
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(dims = nil, path: nil, format: :json, logger: nil, log_level: Logger::WARN, source: nil, uri: nil, dimensions: nil, separator: ',')
|
|
10
|
+
init_logger(logger: logger, log_level: log_level)
|
|
11
|
+
|
|
12
|
+
# guarantee existence of the cache from the data gets loaded from index/import
|
|
13
|
+
@dimensions_hash = Hash.new { |h, k| h[k] = {} }
|
|
14
|
+
|
|
15
|
+
init_dimensions(dims, path: path, format: format, source: source, uri: uri, dimensions: dimensions, separator: separator)
|
|
16
|
+
|
|
17
|
+
update_space_structures
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cond(command = :print, index: nil, &block)
|
|
21
|
+
case command
|
|
22
|
+
when :set
|
|
23
|
+
raise ArgumentError, "Block required" unless block_given?
|
|
24
|
+
@conditions << block
|
|
25
|
+
self
|
|
26
|
+
when :unset
|
|
27
|
+
raise ArgumentError, "Index of the condition required" unless index
|
|
28
|
+
@conditions.delete_at(index)
|
|
29
|
+
when :clear
|
|
30
|
+
@conditions.clear
|
|
31
|
+
self
|
|
32
|
+
when :print
|
|
33
|
+
return if @conditions.empty?
|
|
34
|
+
@conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError, "unknown condition command: #{command}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def func(command = :print, *names, hide: false, progress: false, title: "Computing function(s)", order: nil, mode: :lazy, &block)
|
|
41
|
+
case command
|
|
42
|
+
|
|
43
|
+
when :add
|
|
44
|
+
raise ArgumentError, "Function name required for :add" if names.empty?
|
|
45
|
+
raise ArgumentError, "Block required for :add" unless block_given?
|
|
46
|
+
@logger.warn "You are adding #{names.size} identical functions" if names.size > 1
|
|
47
|
+
|
|
48
|
+
names.each do |name|
|
|
49
|
+
add_function(name, order: order, &block)
|
|
50
|
+
@function_hidden.delete(name.to_sym)
|
|
51
|
+
@function_hidden << name.to_sym if hide
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
when :del
|
|
55
|
+
raise ArgumentError, "Function name(s) required for :del" if names.empty?
|
|
56
|
+
names.each do |name|
|
|
57
|
+
remove_function(name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
when :print
|
|
61
|
+
if @derived.empty?
|
|
62
|
+
puts "(no functions defined)"
|
|
63
|
+
else
|
|
64
|
+
functions_found = names.empty? ? @derived : @derived.slice(*names)
|
|
65
|
+
functions_missing = names.empty? ? [] : names - @derived.keys
|
|
66
|
+
|
|
67
|
+
functions_found.each do |fname, fblock|
|
|
68
|
+
source = fblock.source rescue '(source unavailable)'
|
|
69
|
+
body = source.sub(/^.*?\s(?=(\{|\bdo\b))/, '').strip
|
|
70
|
+
order = ""
|
|
71
|
+
if @order.value?(fname.to_sym)
|
|
72
|
+
case @order.key(fname.to_sym)
|
|
73
|
+
when :first
|
|
74
|
+
order = " [FIRST]"
|
|
75
|
+
when :last
|
|
76
|
+
order = " [LAST]"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
puts " #{fname.inspect.ljust(12)}| #{body}#{@function_hidden.include?(fname) ? ' [HIDDEN]' : ''}#{order}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
functions_missing.each { |fname| puts "#{fname.inspect.ljust(12)}| (no function defined)" }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
when :run
|
|
86
|
+
functions_missing = names.empty? ? [] : names - @derived.keys
|
|
87
|
+
raise "No function(s) defined: #{functions_missing.join(', ')}" unless functions_missing.empty?
|
|
88
|
+
|
|
89
|
+
functions_found = names.empty? ? @derived : @derived.slice(*names)
|
|
90
|
+
# @function_results ||= {} # probably exccessive
|
|
91
|
+
cartesian(progress: progress, title: title) { |v| functions_update_value(vector: v, functions: functions_found, mode: mode) }
|
|
92
|
+
else
|
|
93
|
+
raise ArgumentError, "Unknown command for function: #{command.inspect}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def cartesian(dims = nil, lazy: false, progress: false, title: "Iterating over parameter space")
|
|
98
|
+
|
|
99
|
+
# process edge cases and initialize data structures
|
|
100
|
+
return to_enum(:cartesian, dims, lazy: lazy) unless block_given?
|
|
101
|
+
dimensions = dims || @dimensions
|
|
102
|
+
return nil unless dimensions.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
# create actual cartesian product as iterator of all combinations
|
|
105
|
+
values = dimension_values(dimensions)
|
|
106
|
+
enum = Enumerator.product(*values)
|
|
107
|
+
space = lazy ? enum.lazy : enum
|
|
108
|
+
|
|
109
|
+
# visualize progress bar, if requested
|
|
110
|
+
bar = progress ? ProgressBar.create(title: title, total: self.size, format: '%t [%B] %p%% %e') : nil
|
|
111
|
+
|
|
112
|
+
space.each do |combination|
|
|
113
|
+
# create current vector as Struct
|
|
114
|
+
vector = @struct.new(*combination)
|
|
115
|
+
# skip current vector if it doesn't respect space conditions
|
|
116
|
+
next unless fit?(vector)
|
|
117
|
+
|
|
118
|
+
# guarantee that functions can refer to one another within cartesian block
|
|
119
|
+
@derived&.each do |name, block|
|
|
120
|
+
vector.define_singleton_method(name) { block.call(vector) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# process current vector as Struct
|
|
124
|
+
yield vector
|
|
125
|
+
|
|
126
|
+
# update progress bar, if it's enabled
|
|
127
|
+
bar&.increment
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# check if `v` is a valid vector in parameter space, with respect to space conditions
|
|
132
|
+
# vector can be Struct, Hash, or Array. If it's Array, then order of dimensions is assumed from parameter space
|
|
133
|
+
# this check is computationally aggressive and only makes sense for cmanually constructed vector
|
|
134
|
+
def valid?(v)
|
|
135
|
+
# DEBUG HERE
|
|
136
|
+
# check if vector class is recognizable, and names & number of dimensions are aligned with parameter space
|
|
137
|
+
return false unless vector_consistent?(v)
|
|
138
|
+
# check if vector elements present among their respective dimensional values
|
|
139
|
+
return false unless vector_to(v, :hash).each_pair.all? { |dim, v| @dimensions[dim].include?(v) }
|
|
140
|
+
# check if vector respects conditions
|
|
141
|
+
fit?(v)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def fit?(v)
|
|
145
|
+
@conditions.none? { |cond| !cond.call(vector_to(v, :struct)) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def function(vector, function, substitute: 0)
|
|
149
|
+
v_hash = vector_to(vector, :hash)
|
|
150
|
+
|
|
151
|
+
unless @function_results.key?(v_hash) and @function_results[v_hash].key?(function)
|
|
152
|
+
return substitute
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@function_results[v_hash][function]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# reads from target column using data source created by `data` method
|
|
159
|
+
def lookup(vector, target)
|
|
160
|
+
vec = vector_to(vector, :hash)
|
|
161
|
+
return nil unless @index[vec]
|
|
162
|
+
@index[vec][target.to_sym] ? @index[vec][target.to_sym] : @index[vec][target.to_s]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# creates cartesian space and index from URI
|
|
166
|
+
# index is a hash that maps { dim1: value1, ..., dimN: valueN} to a row of the data source
|
|
167
|
+
def index(source:, uri:, dimensions:, separator: ',')
|
|
168
|
+
@index = {}
|
|
169
|
+
@dimensions ||= {}
|
|
170
|
+
# initialize empty dimensions
|
|
171
|
+
dimensions.each do |dim|
|
|
172
|
+
@dimensions[dim.to_sym] ||= []
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
case source
|
|
176
|
+
when :csv
|
|
177
|
+
require 'csv'
|
|
178
|
+
table = CSV.read(uri, headers: true, header_converters: :symbol, col_sep: separator, strip: true)
|
|
179
|
+
|
|
180
|
+
table.each do |row|
|
|
181
|
+
key = dimensions.each_with_object({}) do |dim, hash|
|
|
182
|
+
value = row[dim].to_s.strip
|
|
183
|
+
unless @dimensions_hash[dim][value]
|
|
184
|
+
@dimensions[dim] << value
|
|
185
|
+
@dimensions_hash[dim][value] = true
|
|
186
|
+
end
|
|
187
|
+
hash[dim.to_sym] = value
|
|
188
|
+
end
|
|
189
|
+
@index[key] = row
|
|
190
|
+
end
|
|
191
|
+
when :xlsx
|
|
192
|
+
require 'roo'
|
|
193
|
+
|
|
194
|
+
xlsx = Roo::Excelx.new(uri)
|
|
195
|
+
sheet = xlsx.sheet(0)
|
|
196
|
+
|
|
197
|
+
# each row is a hash of ALL columns from the XSLX
|
|
198
|
+
data = sheet.parse(headers: true)
|
|
199
|
+
# skip headers in the first row of XLSX sheet
|
|
200
|
+
data.shift
|
|
201
|
+
data.each do |row|
|
|
202
|
+
next if row.values.all?(&:nil?)
|
|
203
|
+
|
|
204
|
+
# index key is an array of dimensional values from the specified dimensions only
|
|
205
|
+
# this key points to FULL row which is assumed to have values of the future functions
|
|
206
|
+
key = dimensions.each_with_object({}) do |dim, hash|
|
|
207
|
+
# TODO: XSLX values are converted to string for uniformity with CSV, even though XSLX returns proper types
|
|
208
|
+
value = row[dim.to_s].to_s.strip
|
|
209
|
+
dim_sym = dim.to_sym
|
|
210
|
+
unless @dimensions_hash[dim_sym][value]
|
|
211
|
+
@dimensions[dim_sym] << value
|
|
212
|
+
@dimensions_hash[dim_sym][value] = true
|
|
213
|
+
end
|
|
214
|
+
hash[dim.to_sym]= value
|
|
215
|
+
end
|
|
216
|
+
@index[key] = row
|
|
217
|
+
end
|
|
218
|
+
else
|
|
219
|
+
raise "Unknown source type #{source}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
self
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# TODO: Dimensions can be omitted - in this case, automatically fetch all dimensions from CSV header
|
|
226
|
+
def source(command, vector: nil, target: nil )
|
|
227
|
+
case command
|
|
228
|
+
when :read
|
|
229
|
+
return nil if (vector.size == 0 or target.nil?)
|
|
230
|
+
lookup(vector, target)
|
|
231
|
+
else
|
|
232
|
+
@logger.error "Unknown data command `#{command}`"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# convert Struct or Array vector to Hash with all checks
|
|
237
|
+
def vector_to(v, type)
|
|
238
|
+
return nil unless vector_consistent?(v)
|
|
239
|
+
|
|
240
|
+
case type
|
|
241
|
+
when :hash
|
|
242
|
+
vector_to_hash!(v)
|
|
243
|
+
when :struct
|
|
244
|
+
vector_to_struct!(v)
|
|
245
|
+
else
|
|
246
|
+
raise "Incorrect target type for vector conversion: #{type}"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def dim(command, *dims)
|
|
251
|
+
case command
|
|
252
|
+
when :add
|
|
253
|
+
dims.each do |d|
|
|
254
|
+
raise ArgumentError, "Incorrect description of the dimensions #{dims.inspect}, must be Hash" unless d.is_a?(Hash)
|
|
255
|
+
@dimensions.update(d)
|
|
256
|
+
end
|
|
257
|
+
update_space_structures
|
|
258
|
+
when :del
|
|
259
|
+
dims.each do |dim|
|
|
260
|
+
raise ArgumentError, "Incorrect dimension name #{dim.inspect}, must be Symbol" unless dim.is_a?(Symbol)
|
|
261
|
+
@dimensions.delete(dim)
|
|
262
|
+
end
|
|
263
|
+
update_space_structures
|
|
264
|
+
else
|
|
265
|
+
raise "Incorrect dimension command: #{command}"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
# For a given array of dimension names, return those that are used in `block` as fields of its iterator
|
|
274
|
+
# This method provides a correctness check in the case we're removing dimensions
|
|
275
|
+
# It allows to determine the functions broken by the dimension removal
|
|
276
|
+
# NOTE: this check will miss any dependencies if executed from IRB
|
|
277
|
+
|
|
278
|
+
def check_dimension_deps(dimension_names)
|
|
279
|
+
@derived.each do |func, body|
|
|
280
|
+
deps = dimension_deps(body, dimension_names)
|
|
281
|
+
@logger.error "Function `#{func}` depends on removed dimension(s): #{deps.join(', ')}" unless deps.empty?
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
@conditions.each_with_index do |body, index|
|
|
285
|
+
deps = dimension_deps(body, dimension_names)
|
|
286
|
+
@logger.error "Condition ##{index} depends on removed dimension(s): #{deps.join(', ')}" unless deps.empty?
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def dimension_deps(block, dimension_names)
|
|
293
|
+
require 'ast'
|
|
294
|
+
|
|
295
|
+
# Get AST of the function body
|
|
296
|
+
ast = RubyVM::AbstractSyntaxTree.of(block)
|
|
297
|
+
return [] unless ast
|
|
298
|
+
|
|
299
|
+
# Get the name of iterator based on parameters of the block
|
|
300
|
+
# block.parameters returns something like [[:opt, :v]] or [[:req, :vector]]
|
|
301
|
+
# We simply take the first argument
|
|
302
|
+
iterator_var_name = block.parameters.first&.last
|
|
303
|
+
return [] unless iterator_var_name
|
|
304
|
+
|
|
305
|
+
found_dimensions = []
|
|
306
|
+
|
|
307
|
+
# Traverse the AST recursively
|
|
308
|
+
search = ->(node) do
|
|
309
|
+
return unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
|
310
|
+
|
|
311
|
+
# We are interested in :call only
|
|
312
|
+
if node.type == :CALL
|
|
313
|
+
receiver = node.children[0] # Object called
|
|
314
|
+
method_name = node.children[1] # What method is called , such as :size
|
|
315
|
+
|
|
316
|
+
# Main criteria:
|
|
317
|
+
# - Object of the call exists
|
|
318
|
+
# - It is a local variable :LVAR
|
|
319
|
+
# - Its name is identical to the iterator's name, such as :v
|
|
320
|
+
is_iterator_receiver = receiver &&
|
|
321
|
+
receiver.is_a?(RubyVM::AbstractSyntaxTree::Node) &&
|
|
322
|
+
[:LVAR, :DVAR].include?(receiver.type) &&
|
|
323
|
+
receiver.children[0] == iterator_var_name
|
|
324
|
+
|
|
325
|
+
if is_iterator_receiver && dimension_names.include?(method_name)
|
|
326
|
+
found_dimensions << method_name
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Recursively traverse nested nodes
|
|
331
|
+
node.children.each { |child| search.call(child) }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Search and return unique findings
|
|
335
|
+
search.call(ast)
|
|
336
|
+
|
|
337
|
+
found_dimensions.uniq
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# if dimensions were changed, update hash of function results, accordingly
|
|
341
|
+
def function_results_immerse
|
|
342
|
+
return if @function_results.empty?
|
|
343
|
+
|
|
344
|
+
# check if dimensions were added or removed
|
|
345
|
+
change = @function_results.first.first.size - @dimensiality
|
|
346
|
+
|
|
347
|
+
return if change == 0
|
|
348
|
+
|
|
349
|
+
if change > 0
|
|
350
|
+
# dimensions were removed
|
|
351
|
+
removed_dimensions = @function_results.first.first.keys - @names
|
|
352
|
+
check_dimension_deps(removed_dimensions)
|
|
353
|
+
# NOTE: When we reduce dimensiality, then vectors as keys of function_results cease to be unique!
|
|
354
|
+
# NOTE: As a new-unique vector key appear, Ruby just silently rewrite the same hash entry
|
|
355
|
+
# NOTE: Having said this, only the _last_ function value will survive!
|
|
356
|
+
@function_results.transform_keys! { |vector| vector.except(*removed_dimensions) }
|
|
357
|
+
else
|
|
358
|
+
# dimensions were added
|
|
359
|
+
# as hash elements are added in order, to the end of hash, we take the `change` of last elements in @dimensiality
|
|
360
|
+
# and - by agreement - we take the first dimensional values for each added dimension
|
|
361
|
+
new_dimensions = @names - @function_results.first.first.keys
|
|
362
|
+
# this is a hash of added dimensions with only first dimensional value for each dimension
|
|
363
|
+
new_first_values = @dimensions.slice(*new_dimensions).transform_values!(&:first)
|
|
364
|
+
# Immerse existing vectors to higher-dimensiality space by adding new dimensions with their first values
|
|
365
|
+
# Note: this implies that existing functions will be defined in the immerse sub-space, and nil in the rest of the new space
|
|
366
|
+
@function_results.transform_keys! { |vector| vector.merge(new_first_values) }
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# For a given subset of space functions, update their values in a given vector
|
|
371
|
+
def functions_update_value(vector: , functions: , mode: )
|
|
372
|
+
v = vector_to(vector, :hash)
|
|
373
|
+
@function_results[v] ||= {}
|
|
374
|
+
|
|
375
|
+
functions.each do |fname, block|
|
|
376
|
+
@function_results[v] ||= {}
|
|
377
|
+
results = @function_results[v]
|
|
378
|
+
|
|
379
|
+
case mode
|
|
380
|
+
when :enforce
|
|
381
|
+
results[fname] = block.call(vector)
|
|
382
|
+
|
|
383
|
+
when :reuse
|
|
384
|
+
unless results.key?(fname)
|
|
385
|
+
raise ArgumentError, "Compute mode #{mode} requires function #{fname} to have value in #{vector_to(vector, :array).inspect}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
when :lazy
|
|
389
|
+
unless results.key?(fname)
|
|
390
|
+
results[fname] = block.call(vector)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
else
|
|
394
|
+
raise ArgumentError, "Incorrect computing mode #{mode.inspect}"
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
ensure_dimension_width(fname, results[fname])
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def update_space_structures
|
|
402
|
+
update_dimensional_structures
|
|
403
|
+
update_conditional_structures
|
|
404
|
+
update_functional_structures
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def init_logger(logger:, log_level:)
|
|
408
|
+
@logger = logger || Logger.new($stdout)
|
|
409
|
+
@logger.level = log_level
|
|
410
|
+
|
|
411
|
+
@logger.formatter = proc do |severity, _datetime, _progname, msg|
|
|
412
|
+
"#{severity}: #{msg}\n"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# make this logger instance kill the program if severity is error or worse
|
|
416
|
+
def @logger.add(severity, message = nil, progname = nil, &block)
|
|
417
|
+
super
|
|
418
|
+
raise SystemExit.new(1, message || progname) if severity >= Logger::ERROR
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def init_dimensions(dims, path:, format:, source:, uri:, dimensions:, separator:)
|
|
424
|
+
# get hash of dimensions: name => array of dimensional values
|
|
425
|
+
if dims && path
|
|
426
|
+
raise "Cannot specify both dimensions and path to dimensions"
|
|
427
|
+
elsif dims
|
|
428
|
+
@dimensions = dims
|
|
429
|
+
elsif path
|
|
430
|
+
import(path, format: format)
|
|
431
|
+
else
|
|
432
|
+
# finally, we read entire space from URI
|
|
433
|
+
raise "Missing data source type" if source.empty?
|
|
434
|
+
raise "Missing data URI" if uri.empty?
|
|
435
|
+
raise "Missing data dimensions" if dimensions.empty?
|
|
436
|
+
index(source: source, uri: uri, dimensions: dimensions, separator: separator)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def update_dimensional_structures
|
|
441
|
+
@dimensions = normalize_dimensions(@dimensions)
|
|
442
|
+
|
|
443
|
+
# internal structure that allows us to quickly check if we're adding new or existing dimensional value
|
|
444
|
+
# such hash is O(1) to the contrast with straightforward .include? which is O(n) and VERY slow on huge tables
|
|
445
|
+
@dimensions_hash ||= Hash.new { |h, k| h[k] = {} }
|
|
446
|
+
|
|
447
|
+
# array of arrays of dimension values (not a Cartesian product yet)
|
|
448
|
+
@levels = dimension_values(@dimensions)
|
|
449
|
+
# total size of Cartesian space (number of vectors, that is, ALL combinations, ignoring conditions)
|
|
450
|
+
@raw_size = @levels.map(&:size).inject(:*)
|
|
451
|
+
# array of dimension names
|
|
452
|
+
@names = @dimensions.keys
|
|
453
|
+
# internal structure: for each dimension, minimal textual width that fits all values in this dimension - required for table output
|
|
454
|
+
# the width is determined for each actual dimension and for each function
|
|
455
|
+
if @dimension_widths.nil?
|
|
456
|
+
@dimension_widths = @names.zip(dimension_widths).to_h
|
|
457
|
+
else
|
|
458
|
+
@dimension_widths.update(@names.zip(dimension_widths).to_h)
|
|
459
|
+
end
|
|
460
|
+
# however, we must remove widths of removed dimensions or functions
|
|
461
|
+
# @dimension_widths.keep_if { |k,_| @dimensions.key?(k) || @derived.key?(k) }
|
|
462
|
+
@default_width = 10
|
|
463
|
+
|
|
464
|
+
# define class for a vector represented as Struct, to be able to access its elements using `.<dimension_name>`
|
|
465
|
+
# NOTE: this class must be unique - otherwise, Struct objects as Hash keys won't coincide for different Struct classes
|
|
466
|
+
# even if fields of such structs are identical
|
|
467
|
+
@struct = Struct.new(*@names).tap { |sc| sc.include(FlexOutput) }
|
|
468
|
+
|
|
469
|
+
# number of dimensions of parameter space
|
|
470
|
+
@dimensiality = @names.size
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def update_conditional_structures
|
|
474
|
+
# array of conditions for valid vectors in parameter space
|
|
475
|
+
@conditions ||= []
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def update_functional_structures
|
|
479
|
+
# functions in parameter space
|
|
480
|
+
@derived ||= {}
|
|
481
|
+
# ordering of the functions
|
|
482
|
+
@order ||= { first: nil, last: nil }
|
|
483
|
+
# Hash: instance of @struct vector => { fname => value }
|
|
484
|
+
@function_results ||= {}
|
|
485
|
+
function_results_immerse
|
|
486
|
+
@function_hidden ||= Set.new
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# create tabular widths for basic dimensions (that is, excluding functions)
|
|
490
|
+
def dimension_widths
|
|
491
|
+
@dimensions.map do |dim, values|
|
|
492
|
+
max_width = ([dim.to_s] + values).inject(0) do |max, e|
|
|
493
|
+
len = e.to_s.length
|
|
494
|
+
len > max ? len : max
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# update tabular width of a dynamic dimension (i.e., function)
|
|
500
|
+
# to be called wherever you expect new dimension or a new dimensional value to appear
|
|
501
|
+
def ensure_dimension_width(name, value = nil)
|
|
502
|
+
raise "Dimension name is empty" unless name
|
|
503
|
+
|
|
504
|
+
if value == nil # adding new dynamic dimension with default width, if not added before
|
|
505
|
+
@dimension_widths[name] = @default_width unless @dimension_widths[name]
|
|
506
|
+
else
|
|
507
|
+
value.to_s.size > @dimension_widths[name] # adding new value of a dynamic dimension
|
|
508
|
+
@dimension_widths[name] = value.to_s.size
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# convert dimensional values to array, for conformity
|
|
513
|
+
def normalize_dimensions(dimensions)
|
|
514
|
+
dimensions.transform_values do |values|
|
|
515
|
+
values.is_a?(Enumerable) && !values.is_a?(String) ? values.to_a : [values]
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def dimension_values(dimensions)
|
|
520
|
+
# array of arrays of dimensional values, not a cartesian product yet
|
|
521
|
+
res = dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
|
|
522
|
+
# check if any dimension has no values
|
|
523
|
+
res.each { |dim| raise "dimension cannot be empty: ``" if dim.empty? }
|
|
524
|
+
res
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def add_function(name, order: nil , &block)
|
|
528
|
+
raise ArgumentError, "Block required" unless block_given?
|
|
529
|
+
if reserved_function_names.include?(name.to_sym)
|
|
530
|
+
raise ArgumentError, "Function name '#{name}' has been already added"
|
|
531
|
+
elsif reserved_struct_names.include?(name.to_sym)
|
|
532
|
+
raise ArgumentError, "Name '#{name}' has been reserved for internal method, you can't use it for a function"
|
|
533
|
+
end
|
|
534
|
+
if order == :last
|
|
535
|
+
@derived[name.to_sym] = block # add to the tail of the hash
|
|
536
|
+
@order[:last] = name.to_sym
|
|
537
|
+
elsif order == :first
|
|
538
|
+
@derived = { name.to_sym => block }.merge(@derived) # add to the head of the hash
|
|
539
|
+
@order[:first] = name.to_sym
|
|
540
|
+
elsif order == nil
|
|
541
|
+
if @order[:last] != nil
|
|
542
|
+
last_name = @order[:last]
|
|
543
|
+
last_body = @derived[last_name]
|
|
544
|
+
@derived.delete(@order[:last]) # remove the tail of the hash
|
|
545
|
+
@derived[name.to_sym] = block # add new function to the tail of the hash
|
|
546
|
+
@derived[last_name] = last_body # restore :last function in the tail of the hash
|
|
547
|
+
else
|
|
548
|
+
@derived[name.to_sym] = block
|
|
549
|
+
end
|
|
550
|
+
else
|
|
551
|
+
raise ArgumentError, "unknown function order '#{order}'"
|
|
552
|
+
end
|
|
553
|
+
ensure_dimension_width(name)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def remove_function(name)
|
|
557
|
+
@derived.delete(name.to_sym)
|
|
558
|
+
@order[:last] = nil if @order[:last] == name.to_sym
|
|
559
|
+
@order[:first] = nil if @order[:first] == name.to_sym
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def reserved_struct_names
|
|
563
|
+
(base_struct_methods = Struct.new(:dummy).methods(false) + Struct.new(:dummy).instance_methods(false)).uniq
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def reserved_function_names
|
|
567
|
+
(self.methods + self.class.instance_methods(false)).uniq
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def decorate_point(v)
|
|
571
|
+
@derived&.each do |name, block|
|
|
572
|
+
v.define_singleton_method(name) { block.call(v) }
|
|
573
|
+
end
|
|
574
|
+
v
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def log
|
|
578
|
+
@logger
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# convert Struct or Array vector to Hash, keeping order of dimension names as it is in parameter space
|
|
582
|
+
# Note: conditions and dimension consistency are NOT respected
|
|
583
|
+
def vector_to_hash!(v)
|
|
584
|
+
return v if v.is_a?(Hash)
|
|
585
|
+
|
|
586
|
+
if v.is_a?(Array)
|
|
587
|
+
@names.zip(v).to_h
|
|
588
|
+
elsif v.is_a?(Struct)
|
|
589
|
+
v.members.zip(v.values).to_h
|
|
590
|
+
else
|
|
591
|
+
raise "Incorrect vector type `#{v.class}`"
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# convert Hash or Array vector to Struct, keeping order of dimension names as it is in parameter space
|
|
596
|
+
# Note: conditions and dimension consistency are NOT respected
|
|
597
|
+
def vector_to_struct!(v)
|
|
598
|
+
return v if v.is_a?(Struct)
|
|
599
|
+
|
|
600
|
+
if v.is_a?(Array)
|
|
601
|
+
@struct.new(*v)
|
|
602
|
+
elsif v.is_a?(Hash)
|
|
603
|
+
@struct.new(*v.values)
|
|
604
|
+
else
|
|
605
|
+
raise "Incorrect vector type `#{v.class}`"
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# check consistency of the vector internal structure relatively to parameter space
|
|
610
|
+
# Note: conditions are NOT checked
|
|
611
|
+
def vector_consistent?(v)
|
|
612
|
+
raise "Incorrect vector type `#{v.class}`" unless v.is_a?(Enumerable)
|
|
613
|
+
raise "Incorrect dimensiality of vector '#{v.inspect}'" unless vector_to_hash!(v).size == @dimensiality
|
|
614
|
+
raise "Incorrect vector dimensions #{v.keys.inspect}" unless @names.to_set == vector_to_hash!(v).keys.to_set
|
|
615
|
+
true
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def add_dimension(dim)
|
|
619
|
+
raise "Incorrect description of the dimension #{dim.inspect}, Hash required" unless dim.is_a(Hash)
|
|
620
|
+
@dimensions << dim
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
end
|
|
624
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module FlexCartesianDeprecations
|
|
2
|
+
|
|
3
|
+
WARNINGS = [
|
|
4
|
+
"`.dimensions` is deprecated and will be renamed to `.elements` in the next version",
|
|
5
|
+
"flag `.dimensions(... raw: ...) is deprecated and will be removed in the next version, please use `.inspect` instead"
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
def deprecations
|
|
9
|
+
WARNINGS.each { |msg| log.warn msg }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
|