zfp 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 74703e564c8c5a5b8f5acae9b29aeb8faaf494769a2be01216060f54f51e91d9
4
+ data.tar.gz: 84c36071e18794d6c8a174c6fe7508a7f5acad282dc7d574a8565cb3cad88082
5
+ SHA512:
6
+ metadata.gz: 4bd205cf6669f1d47c9bdb16328b3d23558626bcdeb8066440ec5ef4ee1195822379bf2e13272aefd5ed9fba286873d0f4b796e260f7b0fa157cd521e3d582bd
7
+ data.tar.gz: 58c293a2b8d1d69740f56b82f6811819d59c50d017d4190d23ad3679e448a0099d2e966c264b5792b2f6dd65dd26470dab0b4c9f3d748148b63c55c9eab828fb
data/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # zfp — ZFP Floating-Point Compression for Ruby
2
+
3
+ > _Because your floats deserve better than Base64._
4
+
5
+ [![Gem Version](https://img.shields.io/gem/v/zfp)](https://rubygems.org/gems/zfp)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ **`zfp`** brings [LLNL's battle-hardened ZFP compression library](https://computing.llnl.gov/projects/zfp) to Ruby. ZFP was built by national-lab scientists to compress petabytes of floating-point simulation data without losing the ability to do science on it. Now it's in a Ruby gem. You're welcome, science.
9
+
10
+ Whether you're cramming ten years of OHLCV market data into Redis, shipping a million embedding vectors over the wire, or just deeply offended by how wasteful `Array#pack("E*")` is, this gem is for you.
11
+
12
+ ---
13
+
14
+ ## What ZFP Actually Does
15
+
16
+ ZFP compresses *n*-dimensional arrays of floats, doubles, int32s, and int64s — up to 4 dimensions — using a floating-point-aware transform that exploits spatial correlation across array elements. Unlike general-purpose compressors, it understands the structure of numeric data.
17
+
18
+ It offers four compression modes:
19
+
20
+ | Mode | What it does | Good for |
21
+ |---|---|---|
22
+ | `:reversible` | Bit-exact lossless | Audit trails, exact P&L, storing anything you'll diff |
23
+ | `:fixed_rate` | Guaranteed bits-per-value | Streaming, fixed-size storage slots |
24
+ | `:fixed_precision` | Guaranteed significant bits | Scientific reproducibility |
25
+ | `:fixed_accuracy` | Guaranteed absolute error bound | Financial data, ML embeddings, anything with a tolerance |
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ You need `libzfp` installed first. Install it with [upkg](https://github.com/seuros/upkg):
32
+
33
+ ```bash
34
+ upkg install zfp
35
+ ```
36
+
37
+ Then add the gem:
38
+
39
+ ```bash
40
+ bundle add zfp
41
+ ```
42
+
43
+ Or install directly:
44
+
45
+ ```bash
46
+ gem install zfp
47
+ ```
48
+
49
+ No native compilation. No waiting. The binding uses [ruby-ffi](https://github.com/ffi/ffi), so `gem install` is instant and the library loads at runtime.
50
+
51
+ ---
52
+
53
+ ## Five-Minute Quickstart
54
+
55
+ ```ruby
56
+ require "zfp"
57
+
58
+ prices = [174.21, 174.85, 173.40, 175.10, 176.33] # ... 10,000 more of these
59
+
60
+ # Lossless round-trip — bit-exact, no questions asked
61
+ compressed = Zfp.compress(prices, type: :double, shape: [prices.size], mode: :reversible)
62
+ restored = Zfp.decompress(compressed, type: :double, shape: [prices.size], mode: :reversible)
63
+
64
+ prices == restored # => true
65
+
66
+ # Self-describing pack — no metadata bookkeeping required
67
+ packed = Zfp.pack(prices, type: :double, shape: [prices.size], mode: :reversible)
68
+ restored = Zfp.unpack(packed) # type, shape, mode all embedded — no args needed
69
+
70
+ prices == restored # => true
71
+ ```
72
+
73
+ That's it. The rest is just choosing how hard you want to squeeze.
74
+
75
+ ---
76
+
77
+ ## API Reference
78
+
79
+ There are three ways to use the gem. Pick whichever fits your architecture.
80
+
81
+ ### Module methods — the easy path
82
+
83
+ **Raw bytes** (`compress` / `decompress`)
84
+
85
+ The caller manages metadata (type, shape, mode). Returns a plain `String` of compressed bytes.
86
+
87
+ ```ruby
88
+ bytes = Zfp.compress(data, type: :double, shape: [1000], mode: :reversible)
89
+ data = Zfp.decompress(bytes, type: :double, shape: [1000], mode: :reversible)
90
+
91
+ # Lossy — absolute error bounded to $0.001 per element
92
+ bytes = Zfp.compress(prices, type: :double, shape: [1000], mode: :fixed_accuracy, tolerance: 0.001)
93
+ data = Zfp.decompress(bytes, type: :double, shape: [1000], mode: :fixed_accuracy, tolerance: 0.001)
94
+ ```
95
+
96
+ **Self-describing bytes** (`pack` / `unpack`)
97
+
98
+ Metadata is embedded in a 32-byte header. Pass bytes anywhere — Redis, S3, a message queue — and unpack without needing a schema.
99
+
100
+ ```ruby
101
+ # Pack: type/shape/mode/params stored in the bytes themselves
102
+ packed = Zfp.pack(data, type: :double, shape: [1000], mode: :fixed_accuracy, tolerance: 0.001)
103
+
104
+ # Unpack: zero arguments needed
105
+ restored = Zfp.unpack(packed)
106
+ ```
107
+
108
+ ### `Zfp::Codec` — when you compress many arrays with the same config
109
+
110
+ Build a codec once. Compress forever.
111
+
112
+ ```ruby
113
+ codec = Zfp::Codec.new(type: :double, shape: [252], mode: :fixed_accuracy, tolerance: 0.001)
114
+
115
+ # Same codec compresses each security's yearly price history
116
+ securities.each do |ticker|
117
+ store[ticker] = codec.compress(daily_closes[ticker])
118
+ end
119
+
120
+ # Retrieve and decompress
121
+ prices = codec.decompress(store["AAPL"])
122
+
123
+ # Or pack (self-describing) via codec
124
+ packed = codec.pack(daily_closes["AAPL"])
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Multi-Dimensional Arrays
130
+
131
+ ZFP natively understands 1-D through 4-D structure. Providing the actual shape (not just total element count) lets it exploit correlation across all axes simultaneously — the more structure you describe, the better it compresses.
132
+
133
+ ```ruby
134
+ # 1-D time series
135
+ Zfp.compress(prices, type: :double, shape: [252], mode: :reversible)
136
+
137
+ # 2-D matrix — e.g. 50 securities × 252 days
138
+ matrix = securities.flat_map { |s| daily_closes[s] }
139
+ Zfp.compress(matrix, type: :double, shape: [50, 252], mode: :fixed_accuracy, tolerance: 0.001)
140
+
141
+ # 3-D tensor — e.g. 10 portfolios × 30 securities × 252 days
142
+ Zfp.compress(tensor, type: :double, shape: [10, 30, 252], mode: :reversible)
143
+
144
+ # 4-D — ZFP supports up to 4 dimensions
145
+ Zfp.compress(data4d, type: :double, shape: [4, 8, 16, 16], mode: :reversible)
146
+ ```
147
+
148
+ ---
149
+
150
+ ## All Four Scalar Types
151
+
152
+ ```ruby
153
+ # Floating-point — all modes available
154
+ Zfp.compress(floats, type: :float, shape: [n], mode: :reversible)
155
+ Zfp.compress(doubles, type: :double, shape: [n], mode: :fixed_accuracy, tolerance: 1e-6)
156
+
157
+ # Integer — reversible mode only (already lossless by nature)
158
+ Zfp.compress(counts, type: :int32, shape: [n], mode: :reversible)
159
+ Zfp.compress(ids, type: :int64, shape: [n], mode: :reversible)
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Numo::NArray Support
165
+
166
+ If [numo-narray](https://github.com/ruby-numo/numo-narray) is loaded, the gem auto-detects type and shape from Numo arrays. No `type:` or `shape:` arguments needed on compress or pack.
167
+
168
+ ```ruby
169
+ require "numo/narray"
170
+
171
+ # Input: Numo::DFloat — type (:double) and shape inferred automatically
172
+ closes = Numo::DFloat.cast(daily_prices).reshape(50, 252)
173
+ bytes = Zfp.compress(closes, mode: :fixed_accuracy, tolerance: 0.001)
174
+ packed = Zfp.pack(closes, mode: :reversible)
175
+
176
+ # Output: get a Numo array back with numo: true
177
+ result = Zfp.decompress(bytes, type: :double, shape: [50, 252], mode: :fixed_accuracy,
178
+ tolerance: 0.001, numo: true)
179
+ # => Numo::DFloat[50, 252]
180
+
181
+ # pack/unpack preserves Numo in → Numo out automatically
182
+ result = Zfp.unpack(packed)
183
+ # => Numo::DFloat[50, 252]
184
+
185
+ # All four Numo types supported:
186
+ # Numo::SFloat → :float
187
+ # Numo::DFloat → :double
188
+ # Numo::Int32 → :int32
189
+ # Numo::Int64 → :int64
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Compression Mode Guide
195
+
196
+ ### `:reversible` — lossless, bit-exact
197
+
198
+ Use this when correctness is non-negotiable. Compression ratios depend on data entropy:
199
+ sequential integers and smooth time series compress well; high-entropy noise barely shrinks.
200
+
201
+ ```ruby
202
+ bytes = Zfp.compress(data, type: :double, shape: [n], mode: :reversible)
203
+ # Typical ratios: 2x–6x on financial/scientific data
204
+ ```
205
+
206
+ ### `:fixed_accuracy` — absolute error bound
207
+
208
+ The workhorse for financial and ML workloads. You set a maximum per-element error;
209
+ ZFP uses as few bits as needed to honor it.
210
+
211
+ ```ruby
212
+ # Error on any element guaranteed ≤ tolerance
213
+ bytes = Zfp.compress(prices, type: :double, shape: [n], mode: :fixed_accuracy, tolerance: 0.001)
214
+ # $0.001 per price point — indistinguishable for P&L calculations
215
+ # Typical ratios: 3x–8x
216
+ ```
217
+
218
+ ### `:fixed_precision` — significant bits
219
+
220
+ Useful when you want to preserve a specific number of significant bits rather than
221
+ an absolute error bound. Handy for scientific data where relative precision matters more than absolute.
222
+
223
+ ```ruby
224
+ bytes = Zfp.compress(data, type: :double, shape: [n], mode: :fixed_precision, precision: 20)
225
+ # 20 significant bits preserved (of 52 available in a double)
226
+ ```
227
+
228
+ ### `:fixed_rate` — guaranteed bytes per value
229
+
230
+ Use when you need fixed-size storage slots — e.g., each block in a columnar store
231
+ must be exactly the same size. The `rate` is bits per scalar value.
232
+
233
+ ```ruby
234
+ bytes = Zfp.compress(data, type: :double, shape: [n], mode: :fixed_rate, rate: 16.0)
235
+ # 16 bits per value → 4x smaller than raw double
236
+ # WARNING: aggressive rates (< 8) can produce large errors on high-dynamic-range data.
237
+ # Always validate max_err against your tolerance before committing to a rate.
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Real-World Performance
243
+
244
+ Benchmarked on a portfolio of 5 × ~20 securities × 252 trading days of close prices
245
+ (synthetic GBM data, see `examples/portfolio_performance.rb`):
246
+
247
+ | Mode | Ratio | Max Error |
248
+ |---|---|---|
249
+ | `:reversible` | 1.1×–1.2× | 0 (exact) |
250
+ | `:fixed_accuracy, tolerance: 0.001` | 3.0×–3.4× | < $0.001 |
251
+ | `:fixed_accuracy, tolerance: 0.01` | 3.4× | < $0.01 |
252
+
253
+ ZFP shines brightest on correlated data — real market prices (correlated sectors, macro
254
+ moves, mean reversion) compress significantly better than the GBM baseline above.
255
+
256
+ For ML embedding vectors (1536-dim float32, high spatial correlation):
257
+ expect 4×–10× with `:fixed_accuracy` and a tolerance tuned to preserve cosine similarity.
258
+
259
+ ---
260
+
261
+ ## Error Handling
262
+
263
+ ```ruby
264
+ Zfp::LibraryNotFound # libzfp not installed or not found
265
+ Zfp::InvalidType # unrecognized :type symbol
266
+ Zfp::InvalidMode # unrecognized :mode symbol
267
+ Zfp::InvalidShape # empty shape, > 4 dimensions, or non-positive dimension
268
+ Zfp::InvalidParams # missing or invalid mode-specific param (rate/precision/tolerance)
269
+ Zfp::CompressionFailed # libzfp returned an error during compress
270
+ Zfp::DecompressionFailed # libzfp returned an error during decompress
271
+ Zfp::PackerError # corrupt or truncated pack header
272
+ ```
273
+
274
+ All inherit from `Zfp::Error < StandardError`.
275
+
276
+ ---
277
+
278
+ ## When to Use This Gem
279
+
280
+ - **In-memory caches of numerical time series** — compress to a byte string, store in Redis,
281
+ decompress on demand. Transparent to the rest of your code.
282
+ - **Columnar financial data** — years of OHLCV for thousands of tickers at a fraction of raw size.
283
+ - **ML embedding storage** — vectors stay geometrically faithful under `:fixed_accuracy` at
284
+ tolerances that preserve cosine similarity.
285
+ - **Message queues** — pack/unpack means the consumer needs no schema; bytes are self-describing.
286
+ - **Checkpointing numerical computation** — save intermediate Numo arrays between steps.
287
+
288
+ ## When Not to Use This Gem
289
+
290
+ - **Random or already-compressed data** — ZFP assumes spatial correlation. Noise, encrypted data,
291
+ and random floats will barely shrink and may grow.
292
+ - **Text, blobs, or non-numeric data** — use MessagePack, zstd, or zlib.
293
+ - **Tiny arrays (< ~50 elements)** — the 32-byte pack header plus ZFP block overhead dominates.
294
+ - **Integer data needing lossy compression** — integer types support `:reversible` only.
295
+
296
+ ---
297
+
298
+ ## Examples
299
+
300
+ ```bash
301
+ bundle exec ruby examples/01_basic_usage.rb
302
+ bundle exec ruby examples/portfolio_performance.rb
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Requirements
308
+
309
+ - Ruby ≥ 3.0
310
+ - libzfp 1.0+ — install with [upkg](https://github.com/seuros/upkg): `upkg install zfp`
311
+ - [ffi](https://github.com/ffi/ffi) ~> 1.0 (installed automatically)
312
+ - [numo-narray](https://github.com/ruby-numo/numo-narray) _(optional — loaded automatically if present)_
313
+
314
+ ---
315
+
316
+ ## Development
317
+
318
+ ```bash
319
+ git clone https://github.com/madbomber/zfp
320
+ cd zfp
321
+ bundle install
322
+ bundle exec rake test
323
+ ```
324
+
325
+ Tests that require libzfp are automatically skipped if the library isn't found,
326
+ so the suite is safe to run anywhere.
327
+
328
+ ---
329
+
330
+ ## Contributing
331
+
332
+ Bug reports and pull requests are welcome at https://github.com/madbomber/zfp.
333
+ Please include a failing test for bugs and a passing test for features.
334
+
335
+ ---
336
+
337
+ ## References
338
+
339
+ - [ZFP — Compressed Floating-Point and Integer Arrays](https://computing.llnl.gov/projects/zfp) — official project page at Lawrence Livermore National Laboratory
340
+ - [ZFP on GitHub](https://github.com/LLNL/zfp) — source, documentation, and compression algorithm details
341
+
342
+ ---
343
+
344
+ ## License
345
+
346
+ MIT. Go nuts.
data/lib/zfp/codec.rb ADDED
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ class Codec
5
+ VALID_TYPES = %i[float double int32 int64].freeze
6
+ VALID_MODES = %i[fixed_rate fixed_precision fixed_accuracy reversible].freeze
7
+
8
+ def initialize(type:, shape:, mode:, numo: false, **params)
9
+ validate_type!(type)
10
+ validate_mode!(mode)
11
+ validate_shape!(shape)
12
+ validate_params!(mode, params)
13
+ @type = type
14
+ @shape = shape
15
+ @mode = mode
16
+ @params = params
17
+ @numo = numo
18
+ end
19
+
20
+ def compress(data)
21
+ buf = TypeCoercion.to_buffer(data, @type)
22
+ field = Field.new(@type, @shape, buf)
23
+ Stream.new(@mode, @params).compress(field)
24
+ ensure
25
+ field&.free
26
+ end
27
+
28
+ def decompress(bytes)
29
+ count = @shape.reduce(:*)
30
+ elem_size = TypeCoercion::ELEMENT_SIZE[@type]
31
+ out_buf = ::FFI::MemoryPointer.new(:uint8, count * elem_size)
32
+ field = Field.new(@type, @shape, out_buf)
33
+ Stream.new(@mode, @params).decompress(field, bytes)
34
+ TypeCoercion.from_buffer(out_buf, @type, @shape, @numo)
35
+ ensure
36
+ field&.free
37
+ end
38
+
39
+ def pack(data)
40
+ as_numo = @numo || TypeCoercion.numo?(data)
41
+ compressed = compress(data)
42
+ Packer.encode(compressed, type: @type, shape: @shape, mode: @mode,
43
+ params: @params, numo: as_numo)
44
+ end
45
+
46
+ private
47
+
48
+ def validate_type!(type)
49
+ raise Zfp::InvalidType, "Unknown type: #{type.inspect}. Valid: #{VALID_TYPES}" \
50
+ unless VALID_TYPES.include?(type)
51
+ end
52
+
53
+ def validate_mode!(mode)
54
+ raise Zfp::InvalidMode, "Unknown mode: #{mode.inspect}. Valid: #{VALID_MODES}" \
55
+ unless VALID_MODES.include?(mode)
56
+ end
57
+
58
+ def validate_shape!(shape)
59
+ unless shape.is_a?(Array) && shape.length.between?(1, 4) &&
60
+ shape.all? { |d| d.is_a?(Integer) && d > 0 }
61
+ raise Zfp::InvalidShape,
62
+ "shape must be Array of 1–4 positive integers, got #{shape.inspect}"
63
+ end
64
+ end
65
+
66
+ def validate_params!(mode, params)
67
+ case mode
68
+ when :fixed_rate
69
+ raise Zfp::InvalidParams, "fixed_rate requires rate: (Float > 0)" \
70
+ unless params[:rate]&.to_f&.positive?
71
+ when :fixed_precision
72
+ raise Zfp::InvalidParams, "fixed_precision requires precision: (Integer > 0)" \
73
+ unless params[:precision].is_a?(Integer) && params[:precision] > 0
74
+ when :fixed_accuracy
75
+ raise Zfp::InvalidParams, "fixed_accuracy requires tolerance: (Float > 0)" \
76
+ unless params[:tolerance]&.to_f&.positive?
77
+ end
78
+ end
79
+ end
80
+ end
data/lib/zfp/errors.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ class Error < StandardError; end
5
+ class LibraryNotFound < Error; end
6
+ class InvalidType < Error; end
7
+ class InvalidMode < Error; end
8
+ class InvalidShape < Error; end
9
+ class InvalidParams < Error; end
10
+ class CompressionFailed < Error; end
11
+ class DecompressionFailed < Error; end
12
+ class PackerError < Error; end
13
+ end
data/lib/zfp/ffi.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Zfp
6
+ module FFI
7
+ extend ::FFI::Library
8
+
9
+ begin
10
+ ffi_lib "zfp"
11
+ rescue LoadError
12
+ raise Zfp::LibraryNotFound,
13
+ "ZFP library not found. Install with: brew install zfp"
14
+ end
15
+
16
+ ZFP_TYPE_INT32 = 1
17
+ ZFP_TYPE_INT64 = 2
18
+ ZFP_TYPE_FLOAT = 3
19
+ ZFP_TYPE_DOUBLE = 4
20
+
21
+ attach_function :zfp_stream_open, [:pointer], :pointer
22
+ attach_function :zfp_stream_close, [:pointer], :void
23
+ attach_function :zfp_stream_set_bit_stream, [:pointer, :pointer], :void
24
+ attach_function :zfp_stream_rewind, [:pointer], :void
25
+ attach_function :zfp_stream_set_rate, [:pointer, :double, :int, :uint, :int], :double
26
+ attach_function :zfp_stream_set_precision, [:pointer, :uint], :uint
27
+ attach_function :zfp_stream_set_accuracy, [:pointer, :double], :double
28
+ attach_function :zfp_stream_set_reversible, [:pointer], :void
29
+ attach_function :zfp_stream_maximum_size, [:pointer, :pointer], :size_t
30
+ attach_function :zfp_compress, [:pointer, :pointer], :size_t
31
+ attach_function :zfp_decompress, [:pointer, :pointer], :size_t
32
+
33
+ attach_function :zfp_field_alloc, [], :pointer
34
+ attach_function :zfp_field_free, [:pointer], :void
35
+ attach_function :zfp_field_set_pointer, [:pointer, :pointer], :void
36
+ attach_function :zfp_field_set_type, [:pointer, :int], :void
37
+ attach_function :zfp_field_set_size_1d, [:pointer, :size_t], :void
38
+ attach_function :zfp_field_set_size_2d, [:pointer, :size_t, :size_t], :void
39
+ attach_function :zfp_field_set_size_3d, [:pointer, :size_t, :size_t, :size_t], :void
40
+ attach_function :zfp_field_set_size_4d, [:pointer, :size_t, :size_t, :size_t, :size_t], :void
41
+
42
+ attach_function :stream_open, [:pointer, :size_t], :pointer
43
+ attach_function :stream_close, [:pointer], :void
44
+ end
45
+ end
data/lib/zfp/field.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ class Field
5
+ attr_reader :zfp_type_int, :shape
6
+
7
+ def initialize(type, shape, buffer)
8
+ validate_shape!(shape)
9
+ @type = type
10
+ @shape = shape
11
+ @buffer = buffer # hold reference — prevents GC while field is alive
12
+ @zfp_type_int = Zfp::TypeCoercion::RUBY_TO_ZFP_TYPE[type]
13
+ @ptr = Zfp::FFI.zfp_field_alloc
14
+ raise Zfp::Error, "Failed to allocate zfp_field" if @ptr.null?
15
+
16
+ Zfp::FFI.zfp_field_set_pointer(@ptr, buffer)
17
+ Zfp::FFI.zfp_field_set_type(@ptr, @zfp_type_int)
18
+ set_shape(shape)
19
+ end
20
+
21
+ def pointer
22
+ @ptr
23
+ end
24
+
25
+ def dims
26
+ @shape.length
27
+ end
28
+
29
+ def free
30
+ return if @ptr.nil? || @ptr.null?
31
+ Zfp::FFI.zfp_field_free(@ptr)
32
+ @ptr = ::FFI::Pointer::NULL
33
+ end
34
+
35
+ private
36
+
37
+ def validate_shape!(shape)
38
+ unless shape.is_a?(Array) && shape.length.between?(1, 4) &&
39
+ shape.all? { |d| d.is_a?(Integer) && d > 0 }
40
+ raise Zfp::InvalidShape,
41
+ "shape must be an Array of 1–4 positive integers, got #{shape.inspect}"
42
+ end
43
+ end
44
+
45
+ def set_shape(shape)
46
+ case shape.length
47
+ when 1 then Zfp::FFI.zfp_field_set_size_1d(@ptr, shape[0])
48
+ when 2 then Zfp::FFI.zfp_field_set_size_2d(@ptr, shape[0], shape[1])
49
+ when 3 then Zfp::FFI.zfp_field_set_size_3d(@ptr, shape[0], shape[1], shape[2])
50
+ when 4 then Zfp::FFI.zfp_field_set_size_4d(@ptr, shape[0], shape[1], shape[2], shape[3])
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/zfp/packer.rb ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ module Packer
5
+ MAGIC = "ZFP\x01"
6
+ HEADER_LEN = 32
7
+ PACK_FMT = "a4CCCCVVVVE"
8
+
9
+ TYPE_TO_BYTE = { float: 0, double: 1, int32: 2, int64: 3 }.freeze
10
+ BYTE_TO_TYPE = TYPE_TO_BYTE.invert.freeze
11
+
12
+ MODE_TO_BYTE = { fixed_rate: 0, fixed_precision: 1, fixed_accuracy: 2, reversible: 3 }.freeze
13
+ BYTE_TO_MODE = MODE_TO_BYTE.invert.freeze
14
+
15
+ def self.encode(compressed_bytes, type:, shape:, mode:, params: {}, numo: false)
16
+ flags = numo ? 1 : 0
17
+ dims = shape.length
18
+ dim0, dim1, dim2, dim3 = Array.new(4) { |i| shape[i].to_i }
19
+ param = extract_param(mode, params)
20
+ header = [MAGIC, TYPE_TO_BYTE[type], MODE_TO_BYTE[mode], dims, flags,
21
+ dim0, dim1, dim2, dim3, param].pack(PACK_FMT)
22
+ header + compressed_bytes
23
+ end
24
+
25
+ def self.decode(bytes)
26
+ if bytes.bytesize < HEADER_LEN
27
+ raise Zfp::PackerError, "truncated header (#{bytes.bytesize} < #{HEADER_LEN} bytes)"
28
+ end
29
+
30
+ magic, type_b, mode_b, rank, flags, d0, d1, d2, d3, param =
31
+ bytes[0, HEADER_LEN].unpack(PACK_FMT)
32
+
33
+ raise Zfp::PackerError, "bad magic bytes (expected ZFP\\x01)" unless magic == MAGIC
34
+
35
+ type = BYTE_TO_TYPE.fetch(type_b) { raise Zfp::PackerError, "unknown type byte #{type_b}" }
36
+ mode = BYTE_TO_MODE.fetch(mode_b) { raise Zfp::PackerError, "unknown mode byte #{mode_b}" }
37
+ shape = [d0, d1, d2, d3].first(rank)
38
+ numo = flags & 1 == 1
39
+ params = decode_params(mode, param)
40
+ data = bytes[HEADER_LEN..]
41
+
42
+ [type, shape, mode, params, numo, data]
43
+ end
44
+
45
+ private_class_method def self.extract_param(mode, params)
46
+ case mode
47
+ when :fixed_rate then params[:rate].to_f
48
+ when :fixed_precision then params[:precision].to_f
49
+ when :fixed_accuracy then params[:tolerance].to_f
50
+ else 0.0
51
+ end
52
+ end
53
+
54
+ private_class_method def self.decode_params(mode, param)
55
+ case mode
56
+ when :fixed_rate then { rate: param }
57
+ when :fixed_precision then { precision: param.round }
58
+ when :fixed_accuracy then { tolerance: param }
59
+ else {}
60
+ end
61
+ end
62
+ end
63
+ end
data/lib/zfp/stream.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ class Stream
5
+ def initialize(mode, params)
6
+ @mode = mode
7
+ @params = params
8
+ end
9
+
10
+ def compress(field)
11
+ zfp_ptr = bitstream = output_buf = nil
12
+ zfp_ptr = Zfp::FFI.zfp_stream_open(nil)
13
+ apply_mode(zfp_ptr, field)
14
+ bufsize = Zfp::FFI.zfp_stream_maximum_size(zfp_ptr, field.pointer)
15
+ output_buf = ::FFI::MemoryPointer.new(:uint8, bufsize)
16
+ bitstream = Zfp::FFI.stream_open(output_buf, bufsize)
17
+ Zfp::FFI.zfp_stream_set_bit_stream(zfp_ptr, bitstream)
18
+ Zfp::FFI.zfp_stream_rewind(zfp_ptr)
19
+ written = Zfp::FFI.zfp_compress(zfp_ptr, field.pointer)
20
+ raise Zfp::CompressionFailed, "zfp_compress returned 0" if written == 0
21
+ output_buf.read_bytes(written)
22
+ ensure
23
+ Zfp::FFI.stream_close(bitstream) if bitstream && !bitstream.null?
24
+ Zfp::FFI.zfp_stream_close(zfp_ptr) if zfp_ptr && !zfp_ptr.null?
25
+ end
26
+
27
+ def decompress(field, compressed_bytes)
28
+ zfp_ptr = bitstream = input_buf = nil
29
+ input_buf = ::FFI::MemoryPointer.new(:uint8, compressed_bytes.bytesize)
30
+ input_buf.put_bytes(0, compressed_bytes)
31
+ zfp_ptr = Zfp::FFI.zfp_stream_open(nil)
32
+ apply_mode(zfp_ptr, field)
33
+ bitstream = Zfp::FFI.stream_open(input_buf, compressed_bytes.bytesize)
34
+ Zfp::FFI.zfp_stream_set_bit_stream(zfp_ptr, bitstream)
35
+ Zfp::FFI.zfp_stream_rewind(zfp_ptr)
36
+ read = Zfp::FFI.zfp_decompress(zfp_ptr, field.pointer)
37
+ raise Zfp::DecompressionFailed, "zfp_decompress returned 0" if read == 0
38
+ ensure
39
+ Zfp::FFI.stream_close(bitstream) if bitstream && !bitstream.null?
40
+ Zfp::FFI.zfp_stream_close(zfp_ptr) if zfp_ptr && !zfp_ptr.null?
41
+ end
42
+
43
+ private
44
+
45
+ def apply_mode(zfp_ptr, field)
46
+ case @mode
47
+ when :fixed_rate
48
+ Zfp::FFI.zfp_stream_set_rate(zfp_ptr, @params[:rate].to_f,
49
+ field.zfp_type_int, field.dims, 0)
50
+ when :fixed_precision
51
+ Zfp::FFI.zfp_stream_set_precision(zfp_ptr, @params[:precision])
52
+ when :fixed_accuracy
53
+ Zfp::FFI.zfp_stream_set_accuracy(zfp_ptr, @params[:tolerance].to_f)
54
+ when :reversible
55
+ Zfp::FFI.zfp_stream_set_reversible(zfp_ptr)
56
+ else
57
+ raise Zfp::InvalidMode, "Unknown mode: #{@mode}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ module TypeCoercion
5
+ RUBY_TO_ZFP_TYPE = {
6
+ int32: Zfp::FFI::ZFP_TYPE_INT32,
7
+ int64: Zfp::FFI::ZFP_TYPE_INT64,
8
+ float: Zfp::FFI::ZFP_TYPE_FLOAT,
9
+ double: Zfp::FFI::ZFP_TYPE_DOUBLE
10
+ }.freeze
11
+
12
+ PACK_FORMAT = {
13
+ float: "e*",
14
+ double: "E*",
15
+ int32: "l<*",
16
+ int64: "q<*"
17
+ }.freeze
18
+
19
+ ELEMENT_SIZE = {
20
+ float: 4,
21
+ double: 8,
22
+ int32: 4,
23
+ int64: 8
24
+ }.freeze
25
+
26
+ class << self
27
+ def detect_type(data)
28
+ return nil unless numo?(data)
29
+ case data
30
+ when Numo::SFloat then :float
31
+ when Numo::DFloat then :double
32
+ when Numo::Int32 then :int32
33
+ when Numo::Int64 then :int64
34
+ else raise Zfp::InvalidType, "Unsupported Numo type: #{data.class}"
35
+ end
36
+ end
37
+
38
+ def detect_shape(data)
39
+ return nil unless numo?(data)
40
+ data.shape
41
+ end
42
+
43
+ def numo?(data)
44
+ defined?(Numo::NArray) && data.is_a?(Numo::NArray)
45
+ end
46
+
47
+ def to_buffer(data, type)
48
+ numo?(data) ? numo_to_buffer(data, type) : array_to_buffer(data, type)
49
+ end
50
+
51
+ def from_buffer(ptr, type, shape, as_numo)
52
+ as_numo ? buffer_to_numo(ptr, type, shape) : buffer_to_array(ptr, type, shape)
53
+ end
54
+
55
+ private
56
+
57
+ def array_to_buffer(array, type)
58
+ fmt = PACK_FORMAT.fetch(type) { raise Zfp::InvalidType, "Unknown type: #{type}" }
59
+ packed = array.flatten.pack(fmt)
60
+ ptr = ::FFI::MemoryPointer.new(:uint8, packed.bytesize)
61
+ ptr.put_bytes(0, packed)
62
+ ptr
63
+ end
64
+
65
+ def numo_to_buffer(narray, type)
66
+ fmt = PACK_FORMAT.fetch(type) { raise Zfp::InvalidType, "Unknown type: #{type}" }
67
+ packed = narray.to_a.flatten.pack(fmt)
68
+ ptr = ::FFI::MemoryPointer.new(:uint8, packed.bytesize)
69
+ ptr.put_bytes(0, packed)
70
+ ptr
71
+ end
72
+
73
+ def buffer_to_array(ptr, type, shape)
74
+ count = shape.reduce(:*)
75
+ bytes = ptr.read_bytes(count * ELEMENT_SIZE[type])
76
+ bytes.unpack(PACK_FORMAT[type])
77
+ end
78
+
79
+ def buffer_to_numo(ptr, type, shape)
80
+ count = shape.reduce(:*)
81
+ bytes = ptr.read_bytes(count * ELEMENT_SIZE[type])
82
+ array = bytes.unpack(PACK_FORMAT[type])
83
+ numo_class_for(type).cast(array).reshape(*shape)
84
+ end
85
+
86
+ def numo_class_for(type)
87
+ case type
88
+ when :float then Numo::SFloat
89
+ when :double then Numo::DFloat
90
+ when :int32 then Numo::Int32
91
+ when :int64 then Numo::Int64
92
+ else raise Zfp::InvalidType, "Unknown type: #{type}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zfp
4
+ VERSION = "0.1.0"
5
+ end
data/lib/zfp.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zfp/version"
4
+ require_relative "zfp/errors"
5
+ require_relative "zfp/ffi"
6
+ require_relative "zfp/type_coercion"
7
+ require_relative "zfp/field"
8
+ require_relative "zfp/stream"
9
+ require_relative "zfp/packer"
10
+ require_relative "zfp/codec"
11
+
12
+ module Zfp
13
+ def self.compress(data, type: nil, shape: nil, mode:, **params)
14
+ type ||= TypeCoercion.detect_type(data) or raise InvalidType, "type: required for Ruby Array input"
15
+ shape ||= TypeCoercion.detect_shape(data) or raise InvalidShape, "shape: required for Ruby Array input"
16
+ Codec.new(type: type, shape: shape, mode: mode, **params).compress(data)
17
+ end
18
+
19
+ def self.decompress(bytes, type:, shape:, mode:, numo: false, **params)
20
+ Codec.new(type: type, shape: shape, mode: mode, numo: numo, **params).decompress(bytes)
21
+ end
22
+
23
+ def self.pack(data, type: nil, shape: nil, mode:, **params)
24
+ type ||= TypeCoercion.detect_type(data) or raise InvalidType, "type: required for Ruby Array input"
25
+ shape ||= TypeCoercion.detect_shape(data) or raise InvalidShape, "shape: required for Ruby Array input"
26
+ as_numo = TypeCoercion.numo?(data)
27
+ Codec.new(type: type, shape: shape, mode: mode, numo: as_numo, **params).pack(data)
28
+ end
29
+
30
+ def self.unpack(bytes)
31
+ type, shape, mode, params, numo, compressed = Packer.decode(bytes)
32
+ Codec.new(type: type, shape: shape, mode: mode, numo: numo, **params).decompress(compressed)
33
+ end
34
+ end
data/sig/zfp.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Zfp
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zfp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dewayne VanHoozer
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: numo-narray
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: debug_me
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: |
69
+ Compress and decompress arrays of float/double/int32/int64 in 1-4
70
+ dimensions using ZFP's fixed-rate, fixed-precision, fixed-accuracy,
71
+ or lossless reversible modes. Accepts Ruby Array and Numo::NArray.
72
+ email:
73
+ - dvanhoozer@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - README.md
79
+ - lib/zfp.rb
80
+ - lib/zfp/codec.rb
81
+ - lib/zfp/errors.rb
82
+ - lib/zfp/ffi.rb
83
+ - lib/zfp/field.rb
84
+ - lib/zfp/packer.rb
85
+ - lib/zfp/stream.rb
86
+ - lib/zfp/type_coercion.rb
87
+ - lib/zfp/version.rb
88
+ - sig/zfp.rbs
89
+ homepage: https://github.com/madbomber/zfp
90
+ licenses: []
91
+ metadata: {}
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.0.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 4.0.10
107
+ specification_version: 4
108
+ summary: Ruby FFI bindings for the ZFP floating-point compression library
109
+ test_files: []