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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +115 -0
- data/lib/flex-cartesian.rb +90 -18
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b8d43af751cdba0a5c141bab0606576291395a5cc3e3a8cb673bacc86c2920b
|
4
|
+
data.tar.gz: 96bfd43d1328e48d38665c2c4aa3b951cf24b932c510ab73ed704d7b7bf98c10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 }
|
data/lib/flex-cartesian.rb
CHANGED
@@ -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
|
-
|
127
|
-
rows =
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
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
|
-
|
146
|
-
|
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
|
-
|
209
|
+
lines << headers.join(",")
|
149
210
|
else
|
150
|
-
|
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
|
-
|
155
|
-
|
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
|
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-
|
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#
|
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
|
- - ">="
|