contrek 1.3.9 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b6523977828bd1e9398d438e045ed1335826630c472a8d1caff6cf0b09ed94e
4
- data.tar.gz: 247332cbd56024ef7e7847294c1ffa32bc59d0a13ef65dc23f5bd6641126b993
3
+ metadata.gz: 8d3c8751ab1e2182443227811912d1b0ad864dde99b62aae50c51b1265314bf3
4
+ data.tar.gz: d3a1ad93edd90e4ebb41a4e1371cd0ed5a2c134fbc093f898a920b8e81e8c249
5
5
  SHA512:
6
- metadata.gz: c632b70f8e0130d39a70c7d13e37f0a0fbec38e045a0c681e7327adc9cf62655e9d35897e0b0f0963777297fd04d4be171f1204a4b9b6974c03344d8318910fd
7
- data.tar.gz: 8941c3749e72b0d3fbdf90fb5a5516b8e7730c3b71a826e6332a87809333b48d9dd18d2136e3917693d6c0009a035ffce4c904295263a1163fa81e542de2fd7c
6
+ metadata.gz: e423133c5e9dfe2f490c84e2c47bdd585d9771949ccf8dbd1afd21b254dea876c7bc18f5cd1e71e327d50cec3de2a7abe5bcb39029f578ab043b43c9a785b607
7
+ data.tar.gz: 3b343824bbe74a380e6e3f084d628c00a7b3efeac373cf65a80b50de66827d5c62b44a4e33ed248b3144336a35c7712e96681b8f754e1dbcee280680b3caf9ab
data/CHANGELOG.md CHANGED
@@ -150,3 +150,6 @@ All notable changes to this project will be documented in this file.
150
150
 
151
151
  ## [1.3.9] - 2026-08-09
152
152
  - Removed the curses development dependency along with the old TerminalTracker; actually Bitmaps::Rendering.to_window renders visually better results.
153
+
154
+ ## [1.4.0] - 2026-08-13
155
+ - Added deterministic merge mode usefull when you need a byte-for-byte reproducible outputs across different runs (by default, the merge stage is non-deterministic to maximize performance and thread utilization).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.3.9)
4
+ contrek (1.4.0)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/README.md CHANGED
@@ -376,11 +376,9 @@ The native implementation uses the number of available CPU cores reported by the
376
376
 
377
377
  ## A note about determinism
378
378
 
379
- The tracing stage itself is deterministic.
379
+ By default, the merge stage is **non-deterministic** to maximize performance and thread utilization.
380
380
 
381
- The merge stage is not.
382
-
383
- Each stripe is processed independently and adjacent stripes are merged as soon as they become available. Depending on thread scheduling, the merge order may differ from one execution to another.
381
+ Each stripe is processed independently, and adjacent stripes are merged opportunistically as soon as they become available. Depending on thread scheduling, the merge order may differ between runs.
384
382
 
385
383
  For example, given three stripes:
386
384
 
@@ -400,7 +398,13 @@ while another execution may produce:
400
398
  B1 + (B2 + B3)
401
399
  ```
402
400
 
403
- Both executions generate equivalent polygons, but the order of intermediate merge operations is differentso the final coordinate sequence is not guaranteed to be byte-for-byte identical across executions.
401
+ Both executions generate equivalent polygons, but the order of intermediate merge operations is different so the final coordinate sequence is not guaranteed to be byte-for-byte identical across executions.
402
+
403
+ If you require byte-for-byte reproducible outputs across different runs, you can enable the **deterministic merge mode** (setting `deterministic: true`).
404
+
405
+ In deterministic mode, stripes are merged strictly according to a fixed binary reduction tree based on their order and index.
406
+
407
+ > **Requirement:** Deterministic mode requires an **even number of stripes/tiles** to process correctly.
404
408
 
405
409
  ## Native execution
406
410
 
@@ -28,6 +28,7 @@ void FinderUtils::sanitize_options(pf_Options& options, const Options& incoming_
28
28
  options.named_sequences);
29
29
  options.bounds = incoming_options.get<bool>("bounds", options.bounds);
30
30
  options.unsafe_mode = incoming_options.get<bool>("unsafe_mode", options.unsafe_mode);
31
+ options.deterministic = incoming_options.get<bool>("deterministic", options.deterministic);
31
32
 
32
33
  if (const Options* compress = incoming_options.get_options("compress")) {
33
34
  options.compress_uniq = compress->get<bool>("uniq", options.compress_uniq);
@@ -53,6 +53,7 @@ struct pf_Options {
53
53
  bool named_sequences = false;
54
54
  bool unsafe_mode = false;
55
55
  bool bounds = false;
56
+ bool deterministic = false;
56
57
  int connectivity_offset = 0;
57
58
  float compress_visvalingam_tolerance = 10.0;
58
59
  int number_of_tiles = 1;
@@ -135,16 +135,17 @@ Tile* Cluster::merge_tiles() {
135
135
  }
136
136
  }
137
137
  }
138
-
139
- double past_tot_outer = tiles_.front()->benchmarks.outer + tiles_.back()->benchmarks.outer;
140
- double past_tot_inner = tiles_.front()->benchmarks.inner + tiles_.back()->benchmarks.inner;
138
+ Tile* first_tile = tiles_.front();
139
+ Tile* last_tile = tiles_.back();
140
+ double past_tot_outer = first_tile->benchmarks.outer + last_tile->benchmarks.outer;
141
+ double past_tot_inner = first_tile->benchmarks.inner + last_tile->benchmarks.inner;
141
142
  Benchmarks b{
142
143
  tot_outer + past_tot_outer,
143
144
  tot_inner + past_tot_inner
144
145
  };
145
146
 
146
147
  Tile* tile = new Tile(
147
- this->finder_, tiles_.front()->start_x(), tiles_.back()->end_x(), tiles_.front()->name() + tiles_.back()->name(), b);
148
+ this->finder_, first_tile->start_x(), last_tile->end_x(), first_tile->name() + last_tile->name(), first_tile->index() / 2, b, first_tile->order() + 1);
148
149
 
149
150
  tile->assign_shapes(new_shapes);
150
151
  for (const auto shape : detach_shapes) {
@@ -10,6 +10,7 @@
10
10
  #include <utility>
11
11
  #include <vector>
12
12
  #include <string>
13
+ #include <algorithm>
13
14
  #include "Finder.h"
14
15
  #include "../../bitmaps/Bitmap.h"
15
16
  #include "../../matchers/Matcher.h"
@@ -35,6 +36,9 @@ Finder::Finder(int number_of_threads, Bitmap *bitmap, Matcher *matcher, const Op
35
36
  if (cw < 1.0) {
36
37
  throw std::runtime_error("One pixel tile width minimum!");
37
38
  }
39
+ if (this->options_.deterministic && (this->options_.number_of_tiles % 2 != 0)) {
40
+ throw std::invalid_argument("Deterministic mode requires an even number of tiles!");
41
+ }
38
42
  int x = 0;
39
43
  for (int tile_index = 0; tile_index < this->options_.number_of_tiles; tile_index++)
40
44
  { int tile_end_x = static_cast<int>(cw * (tile_index + 1));
@@ -65,14 +69,14 @@ Finder::Finder(int number_of_threads, Bitmap *bitmap, Matcher *matcher, const Op
65
69
  }
66
70
 
67
71
  Tile* tile = new Tile(this, payload.tile_start_x, payload.tile_end_x,
68
- std::to_string(payload.tile_index), Benchmarks {0, 0});
72
+ std::to_string(payload.tile_index), payload.tile_index, Benchmarks {0, 0});
69
73
  tile->initial_process(finder);
70
74
  tiles_.queue_push(tile);
71
75
  });
72
76
 
73
77
  x = tile_end_x - 1;
74
78
  }
75
- this->process_tiles();
79
+ this->process_tiles(this->options_.deterministic);
76
80
  reports["init"] = cpu_timer.stop();
77
81
  }
78
82
 
@@ -82,7 +86,7 @@ Finder::Finder(int number_of_threads, const Options& options)
82
86
  reports["init"] = 0;
83
87
  }
84
88
 
85
- void Finder::process_tiles() {
89
+ void Finder::process_tiles(bool deterministic) {
86
90
  std::vector<Tile*> arriving_tiles;
87
91
 
88
92
  while (true) {
@@ -97,7 +101,14 @@ void Finder::process_tiles() {
97
101
  arriving_tiles.begin(),
98
102
  arriving_tiles.end(),
99
103
  [&](Tile* t) {
100
- return (t->start_x() == (tile->end_x() - 1)) || ((t->end_x() - 1) == tile->start_x());
104
+ bool is_adjacent = (t->start_x() == (tile->end_x() - 1)) ||
105
+ ((t->end_x() - 1) == tile->start_x());
106
+ if (!is_adjacent) return false;
107
+ if (deterministic && !this->last_couple(tile, t)) {
108
+ if (tile->order() != t->order()) return false;
109
+ if (std::min(t->index(), tile->index()) % 2 != 0) return false;
110
+ }
111
+ return true;
101
112
  });
102
113
 
103
114
  if (it != arriving_tiles.end()) {
@@ -124,6 +135,13 @@ void Finder::process_tiles() {
124
135
  }
125
136
  }
126
137
 
138
+ bool Finder::last_couple(const Tile* tile_a, const Tile* tile_b) const {
139
+ bool a_is_first = tile_a->index() < tile_b->index();
140
+ const Tile* first = a_is_first ? tile_a : tile_b;
141
+ const Tile* last = a_is_first ? tile_b : tile_a;
142
+ return (first->start_x() == 0) && (last->end_x() == this->maximum_width());
143
+ }
144
+
127
145
  Finder::~Finder() {
128
146
  if (this->whole_tile) {
129
147
  delete this->whole_tile; // last tile to be deleted (not owned by a cluster)
@@ -40,13 +40,14 @@ class Finder : public Poolable {
40
40
  std::mutex finders_mutex;
41
41
  std::map<std::string, double> reports;
42
42
  CpuTimer cpu_timer;
43
+ bool last_couple(const Tile* tile_a, const Tile* tile_b) const;
43
44
 
44
45
  protected:
45
46
  Queue<Tile*> tiles_;
46
47
  int maximum_width_;
47
48
  int height = 0;
48
49
  Tile* whole_tile = nullptr;
49
- void process_tiles();
50
+ void process_tiles(bool deterministic = false);
50
51
 
51
52
  public:
52
53
  using Poolable::Poolable;
@@ -48,7 +48,7 @@ void Merger::add_tile(ProcessResult& result)
48
48
  }
49
49
 
50
50
  int end_x = this->current_x + result.width;
51
- Tile* tile = new Tile(this, this->current_x, end_x, std::to_string(tiles_.size()), Benchmarks {0, 0});
51
+ Tile* tile = new Tile(this, this->current_x, end_x, std::to_string(tiles_.size()), tiles_.size(), Benchmarks {0, 0});
52
52
  tile->assign_raw_polygons(result.polygons, result.treemap);
53
53
  tiles_.queue_push(tile);
54
54
 
@@ -20,12 +20,14 @@
20
20
  class Shape;
21
21
  class Polyline;
22
22
 
23
- Tile::Tile(Finder *finder, int start_x, int end_x, std::string name, const Benchmarks& b)
23
+ Tile::Tile(Finder *finder, int start_x, int end_x, std::string name, int index, const Benchmarks& b, int order)
24
24
  : finder(finder),
25
25
  start_x_(start_x),
26
26
  end_x_(end_x),
27
27
  name_(name),
28
- benchmarks(b) {
28
+ index_(index),
29
+ benchmarks(b),
30
+ order_(order) {
29
31
  this->shapes_pool = new ShapePool();
30
32
  this->shapes_pool->set_owner(this);
31
33
  this->shapes_pools.push_back(this->shapes_pool);
@@ -29,11 +29,13 @@ class Tile {
29
29
  Finder *finder;
30
30
  int start_x_;
31
31
  int end_x_;
32
+ int index_;
33
+ int order_;
32
34
  std::string name_;
33
35
  std::vector<Shape*> shapes_;
34
36
 
35
37
  public:
36
- Tile(Finder *finder, int start_x, int end_x, std::string name, const Benchmarks& b);
38
+ Tile(Finder *finder, int start_x, int end_x, std::string name, int index, const Benchmarks& b, int order = 0);
37
39
  virtual ~Tile();
38
40
  Tile *prev = nullptr;
39
41
  Tile *next = nullptr;
@@ -41,6 +43,8 @@ class Tile {
41
43
  Cluster *cluster = nullptr;
42
44
  int start_x() const { return start_x_; }
43
45
  int end_x() const { return end_x_; }
46
+ int index() const { return index_; }
47
+ int order() const { return order_; }
44
48
  std::string name() const { return name_; }
45
49
  const std::vector<Shape*>& shapes() const { return shapes_; }
46
50
  std::vector<Shape*>& shapes() { return shapes_; }
@@ -106,6 +106,8 @@ module Contrek
106
106
  start_x: @tiles.first.start_x,
107
107
  end_x: @tiles.last.end_x,
108
108
  benchmarks: {outer: tot_outer + past_tot_outer, inner: tot_inner + past_tot_inner},
109
+ index: @tiles.first.index / 2,
110
+ order: @tiles.first.order + 1,
109
111
  name: @tiles.first.name + @tiles.last.name
110
112
  )
111
113
 
@@ -39,6 +39,7 @@ module Contrek
39
39
  x = 0
40
40
  current_versus = options[:versus]
41
41
  raise "Define versus!" if current_versus.nil?
42
+ raise ArgumentError, "Deterministic mode requires an even number of tiles!" if options[:deterministic] && @number_of_tiles.odd?
42
43
 
43
44
  @number_of_tiles.times do |tile_index|
44
45
  tile_end_x = (cw * (tile_index + 1)).to_i
@@ -62,7 +63,7 @@ module Contrek
62
63
  finder: self,
63
64
  start_x: payload[:tile_start_x],
64
65
  end_x: payload[:tile_end_x],
65
- name: payload[:tile_index].to_s
66
+ index: payload[:tile_index]
66
67
  )
67
68
  tile.initial_process!(finder)
68
69
  @tiles << tile
@@ -70,7 +71,7 @@ module Contrek
70
71
 
71
72
  x = tile_end_x - 1
72
73
  end
73
- process_tiles!(bitmap, height: bitmap.h)
74
+ process_tiles!(bitmap, height: bitmap.h, deterministic: @options[:deterministic])
74
75
  end.real
75
76
  end
76
77
 
@@ -106,7 +107,12 @@ module Contrek
106
107
 
107
108
  private
108
109
 
109
- def process_tiles!(bitmap, height:)
110
+ def last_couple?(tile_a, tile_b)
111
+ ordereds = [tile_a, tile_b].sort_by(&:index)
112
+ ordereds.first.start_x == 0 && ordereds.last.end_x == maximum_width
113
+ end
114
+
115
+ def process_tiles!(bitmap, height:, deterministic: false)
110
116
  arriving_tiles = []
111
117
  loop do
112
118
  tile = @tiles.pop
@@ -114,7 +120,17 @@ module Contrek
114
120
  @whole_tile = tile
115
121
  return
116
122
  end
117
- if (twin_tile = arriving_tiles.find { |b| (b.start_x == (tile.end_x - 1)) || ((b.end_x - 1) == tile.start_x) })
123
+ twin_tile = arriving_tiles.find do |t|
124
+ is_adjacent = (t.start_x == (tile.end_x - 1)) || ((t.end_x - 1) == tile.start_x)
125
+ next false unless is_adjacent
126
+ if deterministic && !last_couple?(tile, t)
127
+ next false if tile.order != t.order
128
+ next false if [t.index, tile.index].min.odd?
129
+ end
130
+ true
131
+ end
132
+
133
+ if twin_tile
118
134
  cluster = Cluster.new(finder: self, height: height)
119
135
  if twin_tile.start_x == (tile.end_x - 1)
120
136
  cluster.add(tile)
@@ -126,13 +142,12 @@ module Contrek
126
142
  enqueue!(cluster: cluster) do |payload|
127
143
  merged_tile = payload[:cluster].merge_tiles!
128
144
  @tiles << merged_tile
129
- # usefull external access to each merged_tile
130
- @block&.call(merged_tile, bitmap)
145
+ @block&.call(merged_tile, bitmap) # usefull external access to each merged_tile
131
146
  end
132
147
  arriving_tiles.delete(twin_tile)
133
- next
148
+ else
149
+ arriving_tiles << tile
134
150
  end
135
- arriving_tiles << tile
136
151
  end
137
152
  end
138
153
  end
@@ -35,7 +35,7 @@ module Contrek
35
35
  finder: self,
36
36
  start_x: @current_x,
37
37
  end_x: end_x,
38
- name: @tiles.size.to_s
38
+ index: @tiles.size
39
39
  )
40
40
  tile.assign_raw_polygons!(result[:polygons])
41
41
 
@@ -3,14 +3,16 @@
3
3
  module Contrek
4
4
  module Concurrent
5
5
  class Tile
6
- attr_reader :start_x, :end_x, :benchmarks, :shapes, :name
6
+ attr_reader :start_x, :end_x, :benchmarks, :shapes, :name, :index, :order
7
7
  attr_accessor :prev, :next, :circular_next, :cluster
8
8
 
9
- def initialize(finder:, start_x:, end_x:, name:, benchmarks: {})
9
+ def initialize(finder:, start_x:, end_x:, index:, name: nil, benchmarks: {}, order: 0)
10
10
  @finder = finder
11
11
  @start_x = start_x
12
12
  @end_x = end_x
13
- @name = name
13
+ @index = index
14
+ @order = order
15
+ @name = name || index.to_s
14
16
  @prev = nil
15
17
  @next = nil
16
18
  @benchmarks = {outer: 0, inner: 0}.merge(benchmarks)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Contrek
4
- VERSION = "1.3.9"
4
+ VERSION = "1.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrek
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.9
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni