flex-cartesian 0.2 → 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 +115 -0
  4. data/lib/flex-cartesian.rb +90 -18
  5. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b34c40aea1692a248f2f4ee506b8b7a5a66a8257f90b1f74a7613897f8047c4
4
- data.tar.gz: 3fb4c691a65ebcda1f9606368f436ba1ebee56749dee9e27d04296884cfef26e
3
+ metadata.gz: 4b8d43af751cdba0a5c141bab0606576291395a5cc3e3a8cb673bacc86c2920b
4
+ data.tar.gz: 96bfd43d1328e48d38665c2c4aa3b951cf24b932c510ab73ed704d7b7bf98c10
5
5
  SHA512:
6
- metadata.gz: 455053108bada48ae9b224879069cfe6e43fca419391e194f5b90dbaece0523ab7810eb471e1768814c17bc61f4952171493fb5c38283473d7798039f6b8b8dd
7
- data.tar.gz: 63ebb02a0fb22162b67d17caf483ab2abc3499e9d072ddec3749124e82856d4bfcf3106fbe53b84fd927fe3b36279cb1b2103d75064d4a1cf393ab2f72912f38
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
@@ -172,7 +172,120 @@ array = s.to_a(limit: 3)
172
172
  puts array.inspect
173
173
  ```
174
174
 
175
+ ## Example
175
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.
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).
176
289
 
177
290
  ## API Overview
178
291
 
@@ -320,10 +433,12 @@ s.cartesian { |v| v.output(colorize: true, align: false) }
320
433
  ---
321
434
 
322
435
  ### Conditions on Cartesian Space
436
+ ```ruby
323
437
  cond(command = :print, # or :set, :unset, :clear
324
438
  index: nil, # index of a conditions to unset
325
439
  &block # defintiion of the condition to set
326
440
  )
441
+ ```
327
442
  Example:
328
443
  ```ruby
329
444
  s.cond(:set) { |v| v.dim1 > v.dim3 }
@@ -4,6 +4,7 @@ require 'colorize'
4
4
  require 'json'
5
5
  require 'yaml'
6
6
  require 'method_source'
7
+ require 'set'
7
8
 
8
9
  module FlexOutput
9
10
  def output(separator: " | ", colorize: false, align: true)
@@ -26,10 +27,16 @@ end
26
27
  class FlexCartesian
27
28
  attr :dimensions
28
29
 
29
- 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
30
34
  @dimensions = dimensions
31
35
  @conditions = []
32
36
  @derived = {}
37
+ @function_results = {} # key: Struct instance.object_id => { fname => value }
38
+ @function_hidden = Set.new
39
+ import(path, format: format) if path
33
40
  end
34
41
 
35
42
  def cond(command = :print, index: nil, &block)
@@ -52,6 +59,45 @@ class FlexCartesian
52
59
  end
53
60
  end
54
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
+
55
101
  def add_function(name, &block)
56
102
  raise ArgumentError, "Block required" unless block_given?
57
103
  @derived[name.to_sym] = block
@@ -123,36 +169,62 @@ end
123
169
  end
124
170
  end
125
171
 
126
- def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil)
127
- rows = []
128
- cartesian do |v|
129
- rows << v
130
- break if limit && rows.size >= limit
131
- 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
+
132
184
  return if rows.empty?
133
185
 
134
- headers = (
135
- rows.first.members +
136
- rows.first.singleton_methods(false).reject { |m| m.to_s.start_with?('__') }
137
- ).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)
138
188
 
139
189
  widths = align ? headers.to_h { |h|
140
- [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]
141
199
  } : {}
142
200
 
201
+ lines = []
202
+
203
+ # Header
143
204
  case format
144
205
  when :markdown
145
- puts "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
146
- 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("-|-") + "-|"
147
208
  when :csv
148
- puts headers.join(",")
209
+ lines << headers.join(",")
149
210
  else
150
- 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)
151
212
  end
152
213
 
214
+ # Rows
153
215
  rows.each do |row|
154
- line = headers.map { |h| fmt_cell(row.send(h), colorize, widths[h]) }
155
- 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 }
156
228
  end
157
229
  end
158
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.2'
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-12 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
@@ -69,7 +69,7 @@ dependencies:
69
69
  description: 'Flexible and human-friendly Cartesian product enumerator for Ruby. Supports
70
70
  functions and conditions on cartesian, dimensionality-agnostic/dimensionality-aware
71
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#usage'
72
+ import from JSON/YAML, and export to Markdown/CSV. Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#example'
73
73
  email:
74
74
  - yuri.rassokhin@gmail.com
75
75
  executables: []
@@ -94,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: '0'
97
+ version: '3.0'
98
98
  required_rubygems_version: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="