flex-cartesian 0.1.9 → 1.0

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +173 -2
  4. data/lib/flex-cartesian.rb +131 -30
  5. metadata +20 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b26211b52fac8e2a26ff5408d422e6ae328aefbd67d0a18ac07ef0404006c05
4
- data.tar.gz: cf406e24632a26605b5002e91ddfdf63bddaeb0a361d2dbbab76f5514bfdec88
3
+ metadata.gz: 4b8d43af751cdba0a5c141bab0606576291395a5cc3e3a8cb673bacc86c2920b
4
+ data.tar.gz: 96bfd43d1328e48d38665c2c4aa3b951cf24b932c510ab73ed704d7b7bf98c10
5
5
  SHA512:
6
- metadata.gz: 2cb31e4b65dba494aeac390ad1baee384c5eca90aeb67363cb374db4ba0c1e4b4d4f4dc6acac088642a751e9d7d4ee996b98c99abf49fdc98c615981c5c12663
7
- data.tar.gz: 2b9c27d532a24430cbd21508d91d02d2f29172d9069eab75a8af5c60a2fb25936ff014de5ffc6c5a7df29e08d07de1b7272aefb6edbc907f60a96ae1e1520236
6
+ metadata.gz: 6ff1dbb6deb67e9c9a997d8fb4cea7bb75f8fb4aa9ef8ade0a2452c413e19f97c8687f7816c68d47bef8711fe9cdec98159aae2d80ab468ca55512675f4fd5b3
7
+ data.tar.gz: 16764e02f4010d4c6431779f8cb2f9b7e11f6f04b97ac0c7546f5b1431e287936b92207a527a9838ab614aeed635f290e268961c235fbc83553eef4e448c2358
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0 - 2025-07-14
4
+ ### Added
5
+ - Optional flad for hiding function from output
6
+
7
+ ### Fixed
8
+ - Simplified default parameters
9
+ - Unified umbrella method for handling functions
10
+ - Initialization directly from file of dimensions
11
+
12
+ ## 0.2 - 2025-07-12
13
+ ### Added
14
+ - Logical conditions on Cartesian space
15
+
3
16
  ## 0.1.9 - 2025-07-08
4
17
  ### Fixed
5
18
  - Documentation
data/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  ✅ Functions over Cartesian vectors are decoupled from dimensionality
14
14
 
15
+ ✅ Define conditions on Cartesian combinations using `s.cond(:set) { |v| v.dim1 > v.dim2 } }` syntax
16
+
15
17
  ✅ Calculate over named dimensions using `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }` syntax
16
18
 
17
19
  ✅ Add functions over dimensions using `s.add_function { |v| v.dim1 + v.dim2 }` syntax
@@ -42,7 +44,7 @@ gem install flex-cartesian-*.gem
42
44
 
43
45
  ## Usage
44
46
 
45
- ```
47
+ ```ruby
46
48
  #!/usr/bin/ruby
47
49
 
48
50
  require 'flex-cartesian'
@@ -51,6 +53,9 @@ require 'flex-cartesian'
51
53
 
52
54
  # BASIC CONCEPTS
53
55
 
56
+ # 1. Cartesian object is a set of combinations of values of dimansions.
57
+ # 2. Dimensions always have names.
58
+
54
59
  puts "\nDefine named dimensions"
55
60
  example = {
56
61
  dim1: [1, 2],
@@ -69,6 +74,12 @@ end
69
74
 
70
75
  # ITERATION OVER CARTESIAN SPACE
71
76
 
77
+ # 3. Iterator is dimensionality-agnostic, that is, has a vector syntax that hides dimensions under the hood.
78
+ # This keeps foundational code intact, and isolates modifications in the iterator body 'do_something'.
79
+ # 4. For efficiency on VERY largse Cartesian spaces, there are
80
+ # a). lazy evaluation of each combination
81
+ # b). progress bar to track time-consuming calculations.
82
+
72
83
  puts "\nIterate over all Cartesian combinations and execute action (dimensionality-agnostic style)"
73
84
  s.cartesian { |v| do_something(v) }
74
85
 
@@ -85,6 +96,12 @@ s.cartesian(lazy: true).take(2).each { |v| do_something(v) }
85
96
 
86
97
  # FUNCTIONS ON CARTESIAN SPACE
87
98
 
99
+ # 5. A function is a virtual dimension that is calculated based on a vector of base dimensions.
100
+ # You can think of a function as a scalar field defined on Cartesian space.
101
+ # 6. Functions are printed as virtual dimensions in .output method.
102
+ # 7. However, functions remains virtual construct, and their values can't be referenced by name
103
+ # (unlike regular dimensions). Also, functions do not add to .size of Cartesian space.
104
+
88
105
  puts "\nAdd function 'triple'"
89
106
  puts "Note: function is visualized in .output as a new dimension"
90
107
  s.add_function(:triple) { |v| v.dim1 * 3 + (v.dim3 ? 1: 0) }
@@ -97,6 +114,28 @@ s.remove_function(:test)
97
114
 
98
115
 
99
116
 
117
+ # CONDITIONS ON CARTESIAN SPACE
118
+
119
+ # 8. A condition is a logical restriction of allowed combitnations for Cartesian space.
120
+ # 9. Using conditions, you can take a slice of Cartesian space.
121
+ # In particular, you can reflect semantical dependency of dimensional values.
122
+
123
+ puts "Build Cartesian space that includes only odd values of 'dim1' dimension"
124
+ s.cond(:set) { |v| v.dim1.odd? }
125
+ puts "print all the conditions in format 'index | condition '"
126
+ s.cond
127
+ puts "Test the condition: print the updated Cartesian space"
128
+ s.output
129
+ puts "Test the condition: check the updated size of Cartesian space"
130
+ puts "New size: #{s.size}"
131
+ puts "Clear condition #0"
132
+ s.cond(:unset, index: 0)
133
+ puts "Clear all conditions"
134
+ s.cond(:clear)
135
+ puts "Restored size without conditions: #{s.size}"
136
+
137
+
138
+
100
139
  # PRINT
101
140
 
102
141
  puts "\nPrint Cartesian space as plain table, all functions included"
@@ -125,7 +164,7 @@ s.export('example.yaml', format: :yaml)
125
164
  # UTILITIES
126
165
 
127
166
  puts "\nGet number of Cartesian combinations"
128
- puts "Note: .size counts only dimenstions, it ignores functions"
167
+ puts "Note: .size counts only dimensions, it ignores virtual constructs (functions, conditions, etc.)"
129
168
  puts "Total size of Cartesian space: #{s.size}"
130
169
 
131
170
  puts "\nPartially converting Cartesian space to array:"
@@ -133,7 +172,120 @@ array = s.to_a(limit: 3)
133
172
  puts array.inspect
134
173
  ```
135
174
 
175
+ ## Example
176
+
177
+ The most common use case for FlexCartesian is sweep analysis, that is, analysis of target value on all possible combinations of its parameters.
178
+ FlexCartesian has been designed to provide a concise form for sweep analysis:
179
+
180
+ ```ruby
181
+ require 'flex-cartesian'
182
+
183
+ # create Cartesian space from JSON describing input parameters
184
+ s = FlexCartesian.new(path: './config.json')
185
+
186
+ # Define the values we want to calculate on all possible combinations of parameters
187
+ s.func(:add, :cmd) { |v| v.threads * v.batch }
188
+ s.func(:add, :performance) { |v| v.cmd / 3 }
189
+
190
+ # Calculate
191
+ s.func(:run)
192
+
193
+ # Save result as CSV, to easily open it in any business analytics tool
194
+ s.output(format: :csv, file: './benchmark.csv')
195
+ # For convenience, print result to the terminal
196
+ s.output
197
+ ```
198
+
199
+ As this code is a little artificial, let us build real-world example.
200
+ Perhaps, we want to analyze PING perfomance from our machine to several DNS providers: Google DNS, CloudFlare DNS, and Cisco DNS.
201
+ For each of those services, we would like to know:
202
+
203
+ - What is our ping time?
204
+ - How does ping scale by packet size?
205
+ - How does ping statistics vary based on count of pings?
206
+
207
+ These input parameters form the following dimensions.
208
+
209
+ ```json
210
+ {
211
+ "count": [2, 4],
212
+ "size": [32, 64],
213
+ "target": [
214
+ "8.8.8.8", // Google DNS
215
+ "1.1.1.1", // Cloudflare DNS
216
+ "208.67.222.222" // Cisco OpenDNS
217
+ ]
218
+ }
219
+ ```
220
+
221
+ Note that '//' isn't officially supported by JSON, and you may want to remove the comments if you experience parser errors.
222
+ Let us build the code to run over these parameters.
223
+
224
+ ```ruby
225
+ require 'flex-cartesian'
226
+
227
+ s = FlexCartesian.new(path: './ping_config.json') # file with the parameters as given above
228
+
229
+ result = {} # here we will store raw result of each ping and fetch target metrics from it
230
+
231
+ # this function shows actual ping command
232
+ s.func(:add, :command) do |v|
233
+ "ping -c #{v.count} -s #{v.size} #{v.target}"
234
+ end
235
+
236
+ # this function gets raw result of actual ping command
237
+ s.func(:add, :raw_ping, hide: true) do |v|
238
+ result[v.command] ||= `#{v.command} 2>&1`
239
+ end
240
+
241
+ # this function extracts ping time
242
+ s.func(:add, :time) do |v|
243
+ if v.raw_ping =~ /min\/avg\/max\/(?:mdev|stddev) = [^\/]+\/([^\/]+)/
244
+ $1.to_f
245
+ end
246
+ end
247
+
248
+ # this function extracts minimum ping time
249
+ s.func(:add, :min) do |v|
250
+ if v.raw_ping =~ /min\/avg\/max\/(?:mdev|stddev) = ([^\/]+)/
251
+ $1.to_f
252
+ end
253
+ end
254
+
255
+ # funally, this function extracts losses of ping
256
+ s.func(:add, :loss) do |v|
257
+ if v.raw_ping =~ /(\d+(?:\.\d+)?)% packet loss/
258
+ $1.to_f
259
+ end
260
+ end
261
+
262
+ # this is the spinal axis of FlexCartesian:
263
+ # calculate all functions on the entire Cartesian space of parameters aka dimensions
264
+ s.func(:run)
265
+
266
+ # save benchmark results to CSV for convenient analysis in BI tools
267
+ s.output(format: :csv, file: './benchmark.csv')
268
+
269
+ # for convenience, show tabular result on screen as well
270
+ s.output(colorize: true)
271
+ ```
272
+
273
+ This code is 100% practical and illustrative. You can benchmark:
274
+
275
+ - Local block devices using 'dd'
276
+ - GPU-to-Storage connection using 'gdsio'
277
+ - Local file systems using FS-based utilities
278
+ - Local CPU RAM using RAM disk or specialized benchmarks for CPU RAM
279
+ - Database performance using SQL client or non-SQL client utilities
280
+ - Performance of object storage of cloud providers, be it AWS S3, OCI Object Storage, or anything else
281
+ - Performance of any AI model, from simplistic YOLO to heavy-weight LLM such as LLAMA, Cohere, or DeepSeek
282
+ - ... Any other target application or service
283
+
284
+ In any use case, FlexCartesian will unfold complete landscape of the target performance over all configurable parameters.
285
+ As result, you will be able to spot optimal configurations, correlations, bottlenecks, and sweet spots.
286
+ Moreover, you will make your conclusions in a justifiable way.
136
287
 
288
+ Here is an example of how I used FlexCartesian to [analyze optimal performance/cost of YOLO](https://www.linkedin.com/pulse/comparing-gpu-a10-ampere-a1-shapes-object-oci-yuri-rassokhin-rseqf).
137
289
 
138
290
  ## API Overview
139
291
 
@@ -278,6 +430,25 @@ Example:
278
430
  s.cartesian { |v| v.output(colorize: true, align: false) }
279
431
  ```
280
432
 
433
+ ---
434
+
435
+ ### Conditions on Cartesian Space
436
+ ```ruby
437
+ cond(command = :print, # or :set, :unset, :clear
438
+ index: nil, # index of a conditions to unset
439
+ &block # defintiion of the condition to set
440
+ )
441
+ ```
442
+ Example:
443
+ ```ruby
444
+ s.cond(:set) { |v| v.dim1 > v.dim3 }
445
+ s.cond # defaults to s.cond(:print) and shows all the conditions in the form 'index | definition'
446
+ s.cond(:unset, 0) # remove previously set condition
447
+ s.cond(:clear) # remove all conditions, if any
448
+ ```
449
+
450
+
451
+
281
452
  ## License
282
453
 
283
454
  This project is licensed under the terms of the GNU General Public License v3.0.
@@ -3,6 +3,8 @@ require 'progressbar'
3
3
  require 'colorize'
4
4
  require 'json'
5
5
  require 'yaml'
6
+ require 'method_source'
7
+ require 'set'
6
8
 
7
9
  module FlexOutput
8
10
  def output(separator: " | ", colorize: false, align: true)
@@ -25,11 +27,77 @@ end
25
27
  class FlexCartesian
26
28
  attr :dimensions
27
29
 
28
- def initialize(dimensions = nil)
30
+ def initialize(dimensions = nil, path: nil, format: :json)
31
+ if dimensions && path
32
+ $logger.msg "Please specify either dimensions or path to dimensions", :error
33
+ end
29
34
  @dimensions = dimensions
35
+ @conditions = []
30
36
  @derived = {}
37
+ @function_results = {} # key: Struct instance.object_id => { fname => value }
38
+ @function_hidden = Set.new
39
+ import(path, format: format) if path
40
+ end
41
+
42
+ def cond(command = :print, index: nil, &block)
43
+ case command
44
+ when :set
45
+ raise ArgumentError, "Block required" unless block_given?
46
+ @conditions << block
47
+ self
48
+ when :unset
49
+ raise ArgumentError, "Index of the condition required" unless index
50
+ @conditions.delete_at(index)
51
+ when :clear
52
+ @conditions.clear
53
+ self
54
+ when :print
55
+ return if @conditions.empty?
56
+ @conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
57
+ else
58
+ raise ArgumentError, "unknown condition command: #{command}"
59
+ end
31
60
  end
32
61
 
62
+ def func(command = :print, name = nil, hide: false, &block)
63
+ case command
64
+ when :add
65
+ raise ArgumentError, "Function name and block required for :add" unless name && block_given?
66
+ add_function(name, &block)
67
+ @function_hidden.delete(name.to_sym)
68
+ @function_hidden << name.to_sym if hide
69
+
70
+ when :del
71
+ raise ArgumentError, "Function name required for :del" unless name
72
+ remove_function(name)
73
+
74
+ when :print
75
+ if @derived.empty?
76
+ puts "(no functions defined)"
77
+ else
78
+ @derived.each do |fname, fblock|
79
+ source = fblock.source rescue '(source unavailable)'
80
+
81
+ body = source.sub(/^.*?\s(?=(\{|\bdo\b))/, '').strip
82
+
83
+ puts " #{fname.inspect.ljust(12)}| #{body}#{@function_hidden.include?(fname) ? ' [HIDDEN]' : ''}"
84
+ end
85
+ end
86
+
87
+ when :run
88
+ @function_results = {}
89
+ cartesian do |v|
90
+ @function_results[v] ||= {}
91
+ @derived.each do |fname, block|
92
+ @function_results[v][fname] = block.call(v)
93
+ end
94
+ end
95
+
96
+ else
97
+ raise ArgumentError, "Unknown command for function: #{command.inspect}"
98
+ end
99
+ end
100
+
33
101
  def add_function(name, &block)
34
102
  raise ArgumentError, "Block required" unless block_given?
35
103
  @derived[name.to_sym] = block
@@ -61,18 +129,26 @@ class FlexCartesian
61
129
  struct_instance.define_singleton_method(name) { block.call(struct_instance) }
62
130
  end
63
131
 
132
+ next if @conditions.any? { |cond| !cond.call(struct_instance) }
133
+
64
134
  yield struct_instance
65
135
  end
66
136
  end
67
137
 
68
- def size(dims = nil)
69
- dimensions = dims || @dimensions
70
- return 0 unless dimensions.is_a?(Hash)
71
-
72
- values = dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
73
- return 0 if values.any?(&:empty?)
74
-
75
- values.map(&:size).inject(1, :*)
138
+ def size
139
+ return 0 unless @dimensions.is_a?(Hash)
140
+ if @conditions.empty?
141
+ values = @dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
142
+ return 0 if values.any?(&:empty?)
143
+ values.map(&:size).inject(1, :*)
144
+ else
145
+ size = 0
146
+ cartesian do |v|
147
+ next if @conditions.any? { |cond| !cond.call(v) }
148
+ size += 1
149
+ end
150
+ size
151
+ end
76
152
  end
77
153
 
78
154
  def to_a(limit: nil)
@@ -84,46 +160,71 @@ end
84
160
  result
85
161
  end
86
162
 
87
- def progress_each(dims = nil, lazy: false, title: "Processing")
88
- total = size(dims)
89
- bar = ProgressBar.create(title: title, total: total, format: '%t [%B] %p%% %e')
163
+ def progress_each(lazy: false, title: "Processing")
164
+ bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')
90
165
 
91
- cartesian(dims, lazy: lazy) do |v|
166
+ cartesian(@dimensions, lazy: lazy) do |v|
92
167
  yield v
93
168
  bar.increment
94
169
  end
95
170
  end
96
171
 
97
- def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil)
98
- rows = []
99
- cartesian do |v|
100
- rows << v
101
- break if limit && rows.size >= limit
102
- end
172
+ def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil)
173
+ rows = if @function_results && !@function_results.empty?
174
+ @function_results.keys
175
+ else
176
+ result = []
177
+ cartesian do |v|
178
+ result << v
179
+ break if limit && result.size >= limit
180
+ end
181
+ result
182
+ end
183
+
103
184
  return if rows.empty?
104
185
 
105
- headers = (
106
- rows.first.members +
107
- rows.first.singleton_methods(false).reject { |m| m.to_s.start_with?('__') }
108
- ).map(&:to_s)
186
+ visible_func_names = @derived.keys - (@function_hidden || Set.new).to_a
187
+ headers = rows.first.members.map(&:to_s) + visible_func_names.map(&:to_s)
109
188
 
110
189
  widths = align ? headers.to_h { |h|
111
- [h, [h.size, *rows.map { |r| fmt_cell(r.send(h), false).size }].max]
190
+ values = rows.map do |r|
191
+ val = if r.members.map(&:to_s).include?(h)
192
+ r.send(h)
193
+ else
194
+ @function_results&.dig(r, h.to_sym)
195
+ end
196
+ fmt_cell(val, false).size
197
+ end
198
+ [h, [h.size, *values].max]
112
199
  } : {}
113
200
 
201
+ lines = []
202
+
203
+ # Header
114
204
  case format
115
205
  when :markdown
116
- puts "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
117
- puts "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
206
+ lines << "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
207
+ lines << "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
118
208
  when :csv
119
- puts headers.join(",")
209
+ lines << headers.join(",")
120
210
  else
121
- puts headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(separator)
211
+ lines << headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(separator)
122
212
  end
123
213
 
214
+ # Rows
124
215
  rows.each do |row|
125
- line = headers.map { |h| fmt_cell(row.send(h), colorize, widths[h]) }
126
- puts format == :csv ? line.join(",") : line.join(separator)
216
+ values = row.members.map { |m| row.send(m) } +
217
+ visible_func_names.map { |fname| @function_results&.dig(row, fname) }
218
+
219
+ line = headers.zip(values).map { |(_, val)| fmt_cell(val, colorize, widths[_]) }
220
+ lines << (format == :csv ? line.join(",") : line.join(separator))
221
+ end
222
+
223
+ # Output to console or file
224
+ if file
225
+ File.write(file, lines.join("\n") + "\n")
226
+ else
227
+ lines.each { |line| puts line }
127
228
  end
128
229
  end
129
230
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flex-cartesian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: '1.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yury Rassokhin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
11
+ date: 2025-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -52,10 +52,24 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: method_source
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
55
69
  description: 'Flexible and human-friendly Cartesian product enumerator for Ruby. Supports
56
- functions on cartesian, dimensionality-agnostic/dimensionality-aware iterators,
57
- named dimensions, tabular output, lazy/eager evaluation, progress bar, import from
58
- JSON/YAML, and export to Markdown/CSV. Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#usage'
70
+ functions and conditions on cartesian, dimensionality-agnostic/dimensionality-aware
71
+ iterators, named dimensions, tabular output, lazy/eager evaluation, progress bar,
72
+ import from JSON/YAML, and export to Markdown/CSV. Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#example'
59
73
  email:
60
74
  - yuri.rassokhin@gmail.com
61
75
  executables: []
@@ -80,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
80
94
  requirements:
81
95
  - - ">="
82
96
  - !ruby/object:Gem::Version
83
- version: '0'
97
+ version: '3.0'
84
98
  required_rubygems_version: !ruby/object:Gem::Requirement
85
99
  requirements:
86
100
  - - ">="