Skip to content

Commit 9e6f47c

Browse files
Optimization by claude
1 parent 80d8a1f commit 9e6f47c

3 files changed

Lines changed: 206 additions & 110 deletions

File tree

include/bioimage_cpp/graph/grid_features.hxx

Lines changed: 147 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22

33
#include "bioimage_cpp/array_view.hxx"
44
#include "bioimage_cpp/detail/edge_hash.hxx"
5-
#include "bioimage_cpp/detail/grid.hxx"
65
#include "bioimage_cpp/graph/grid_graph.hxx"
76

8-
#include <algorithm>
97
#include <array>
108
#include <cmath>
119
#include <cstddef>
1210
#include <cstdint>
1311
#include <stdexcept>
1412
#include <string>
15-
#include <unordered_set>
1613
#include <utility>
1714
#include <vector>
1815

@@ -21,13 +18,17 @@ namespace bioimage_cpp::graph {
2118
namespace detail_grid_features {
2219

2320
template <std::size_t D>
24-
std::vector<std::ptrdiff_t> graph_shape_vector(const GridGraph<D> &graph) {
25-
std::vector<std::ptrdiff_t> shape(D);
26-
const auto &grid_shape = graph.shape();
21+
std::array<std::ptrdiff_t, D> offset_to_array(
22+
const std::vector<std::ptrdiff_t> &offset
23+
) {
24+
if (offset.size() != D) {
25+
throw std::invalid_argument("each offset must have length matching graph ndim");
26+
}
27+
std::array<std::ptrdiff_t, D> result{};
2728
for (std::size_t axis = 0; axis < D; ++axis) {
28-
shape[axis] = static_cast<std::ptrdiff_t>(grid_shape[axis]);
29+
result[axis] = offset[axis];
2930
}
30-
return shape;
31+
return result;
3132
}
3233

3334
inline std::ptrdiff_t l1_norm(const std::vector<std::ptrdiff_t> &offset) {
@@ -38,6 +39,16 @@ inline std::ptrdiff_t l1_norm(const std::vector<std::ptrdiff_t> &offset) {
3839
return result;
3940
}
4041

42+
template <std::size_t D>
43+
std::ptrdiff_t l1_norm(const std::array<std::ptrdiff_t, D> &offset) {
44+
std::ptrdiff_t result = 0;
45+
for (std::size_t axis = 0; axis < D; ++axis) {
46+
const auto value = offset[axis];
47+
result += value < 0 ? -value : value;
48+
}
49+
return result;
50+
}
51+
4152
template <std::size_t D>
4253
void require_graph_shape(
4354
const GridGraph<D> &graph,
@@ -87,30 +98,34 @@ void require_affinity_shape(
8798
}
8899
}
89100

101+
// Two long-range offsets `a` and `b` produce the same set of edges iff
102+
// `b = -a` — each offset, restricted to its valid source region, is
103+
// internally injective, and `edge_key` canonicalizes endpoint order, so
104+
// only sign-flipped offset pairs collide. Checking pairs is O(n_offsets^2)
105+
// — a small constant in practice — and avoids sorting the full lifted
106+
// edge list at the end of `grid_affinity_features_with_lifted`.
90107
template <std::size_t D>
91-
std::uint64_t local_edge_id(
92-
const GridGraph<D> &graph,
93-
const std::uint64_t node,
94-
const std::vector<std::ptrdiff_t> &offset
108+
void check_no_negated_long_range_offsets(
109+
const std::vector<std::vector<std::ptrdiff_t>> &offsets
95110
) {
96-
std::size_t axis = D;
97-
std::ptrdiff_t step = 0;
98-
for (std::size_t candidate = 0; candidate < D; ++candidate) {
99-
if (offset[candidate] != 0) {
100-
axis = candidate;
101-
step = offset[candidate];
102-
break;
111+
for (std::size_t i = 0; i < offsets.size(); ++i) {
112+
if (l1_norm(offsets[i]) <= 1) continue;
113+
for (std::size_t j = i + 1; j < offsets.size(); ++j) {
114+
if (l1_norm(offsets[j]) <= 1) continue;
115+
bool negated = true;
116+
for (std::size_t axis = 0; axis < D; ++axis) {
117+
if (offsets[i][axis] != -offsets[j][axis]) {
118+
negated = false;
119+
break;
120+
}
121+
}
122+
if (negated) {
123+
throw std::invalid_argument(
124+
"offsets produce duplicate long-range grid edges"
125+
);
126+
}
103127
}
104128
}
105-
if (axis == D || (step != 1 && step != -1) || l1_norm(offset) != 1) {
106-
throw std::invalid_argument("local offsets must have L1 norm 1");
107-
}
108-
109-
auto pivot = graph.coordinates(node);
110-
if (step < 0) {
111-
--pivot[axis];
112-
}
113-
return graph.edge_id(axis, pivot);
114129
}
115130

116131
template <std::size_t D>
@@ -126,7 +141,7 @@ std::array<std::uint64_t, D> uint64_strides_from_shape(
126141
}
127142

128143
template <std::size_t D>
129-
std::size_t local_offset_axis(const std::vector<std::ptrdiff_t> &offset) {
144+
std::size_t local_offset_axis(const std::array<std::ptrdiff_t, D> &offset) {
130145
for (std::size_t axis = 0; axis < D; ++axis) {
131146
if (offset[axis] != 0) {
132147
return axis;
@@ -135,10 +150,40 @@ std::size_t local_offset_axis(const std::vector<std::ptrdiff_t> &offset) {
135150
return D;
136151
}
137152

153+
// Template-recursive enumeration of (source_node, target_node, coordinate)
154+
// triples for a fixed grid offset. Flat node ids are computed incrementally
155+
// by addition only. With `Axis` known at compile time, the compiler unrolls
156+
// the entire nest for small D and inlines `callback`.
157+
template <std::size_t D, std::size_t Axis, class Callback>
158+
void for_each_valid_offset_link_impl(
159+
const std::array<std::uint64_t, D> &begin,
160+
const std::array<std::uint64_t, D> &end,
161+
const std::array<std::uint64_t, D> &strides,
162+
const std::int64_t delta,
163+
std::array<std::uint64_t, D> &coordinate,
164+
const std::uint64_t node,
165+
Callback &&callback
166+
) {
167+
if constexpr (Axis == D) {
168+
const auto target =
169+
static_cast<std::uint64_t>(static_cast<std::int64_t>(node) + delta);
170+
callback(node, target, coordinate);
171+
} else {
172+
std::uint64_t axis_node = node + begin[Axis] * strides[Axis];
173+
for (std::uint64_t coord = begin[Axis]; coord < end[Axis]; ++coord) {
174+
coordinate[Axis] = coord;
175+
for_each_valid_offset_link_impl<D, Axis + 1>(
176+
begin, end, strides, delta, coordinate, axis_node, callback
177+
);
178+
axis_node += strides[Axis];
179+
}
180+
}
181+
}
182+
138183
template <std::size_t D, class Callback>
139184
void for_each_valid_offset_link(
140185
const GridGraph<D> &graph,
141-
const std::vector<std::ptrdiff_t> &offset,
186+
const std::array<std::ptrdiff_t, D> &offset,
142187
Callback &&callback
143188
) {
144189
const auto &shape = graph.shape();
@@ -158,42 +203,28 @@ void for_each_valid_offset_link(
158203
delta += static_cast<std::int64_t>(axis_offset) *
159204
static_cast<std::int64_t>(strides[axis]);
160205
}
161-
162206
std::array<std::uint64_t, D> coordinate{};
163-
auto recurse = [&](auto &&self, const std::size_t axis, const std::uint64_t node) -> void {
164-
if (axis == D) {
165-
const auto target =
166-
static_cast<std::uint64_t>(static_cast<std::int64_t>(node) + delta);
167-
callback(node, target, coordinate);
168-
return;
169-
}
170-
auto axis_node = node + begin[axis] * strides[axis];
171-
for (std::uint64_t coord = begin[axis]; coord < end[axis]; ++coord) {
172-
coordinate[axis] = coord;
173-
self(self, axis + 1, axis_node);
174-
axis_node += strides[axis];
175-
}
176-
};
177-
recurse(recurse, 0, 0);
207+
for_each_valid_offset_link_impl<D, 0>(
208+
begin, end, strides, delta, coordinate, 0, callback
209+
);
178210
}
179211

212+
// Compute the local edge id from a source coordinate. The per-axis pivot
213+
// adjustment for negative offsets is hoisted into `pivot_adjustment` — a
214+
// constant per (offset, axis) the caller pre-computes once, so the inner
215+
// loop is a branch-free dot product.
180216
template <std::size_t D>
181217
std::uint64_t local_edge_id_from_source_coordinate(
182-
const GridGraph<D> &graph,
183-
const std::vector<std::ptrdiff_t> &offset,
218+
const std::uint64_t edge_offset_base,
184219
const std::array<std::uint64_t, D> &source_coordinate,
185-
const std::size_t axis,
186-
const std::array<std::uint64_t, D> &edge_strides
220+
const std::array<std::uint64_t, D> &edge_strides,
221+
const std::uint64_t pivot_adjustment
187222
) {
188223
std::uint64_t local_edge = 0;
189-
for (std::size_t coord_axis = 0; coord_axis < D; ++coord_axis) {
190-
auto pivot = source_coordinate[coord_axis];
191-
if (coord_axis == axis && offset[axis] < 0) {
192-
--pivot;
193-
}
194-
local_edge += pivot * edge_strides[coord_axis];
224+
for (std::size_t axis = 0; axis < D; ++axis) {
225+
local_edge += source_coordinate[axis] * edge_strides[axis];
195226
}
196-
return graph.edge_offset(axis) + local_edge;
227+
return edge_offset_base + local_edge - pivot_adjustment;
197228
}
198229

199230
} // namespace detail_grid_features
@@ -210,8 +241,9 @@ void grid_boundary_features(
210241
throw std::invalid_argument("out shape must be (number_of_edges,)");
211242
}
212243

213-
for (std::uint64_t edge = 0; edge < graph.number_of_edges(); ++edge) {
214-
const auto uv = graph.uv(edge);
244+
const auto &uv_ids = graph.uv_ids();
245+
for (std::size_t edge = 0; edge < uv_ids.size(); ++edge) {
246+
const auto &uv = uv_ids[edge];
215247
out.data[edge] = 0.5 * (boundary_map.data[uv.first] + boundary_map.data[uv.second]);
216248
}
217249
}
@@ -245,24 +277,35 @@ void grid_local_affinity_features(
245277
}
246278

247279
for (std::size_t channel = 0; channel < offsets.size(); ++channel) {
248-
const auto axis = detail_grid_features::local_offset_axis<D>(offsets[channel]);
280+
const auto offset_array =
281+
detail_grid_features::offset_to_array<D>(offsets[channel]);
282+
const auto axis = detail_grid_features::local_offset_axis<D>(offset_array);
249283
auto edge_shape = graph.shape();
250284
--edge_shape[axis];
251-
const auto edge_strides = detail_grid_features::uint64_strides_from_shape(edge_shape);
252-
const auto channel_offset = static_cast<std::uint64_t>(channel) * number_of_nodes;
285+
const auto edge_strides =
286+
detail_grid_features::uint64_strides_from_shape(edge_shape);
287+
const auto edge_offset_base = graph.edge_offset(axis);
288+
const std::uint64_t pivot_adjustment =
289+
offset_array[axis] < 0 ? edge_strides[axis] : 0;
290+
const auto channel_offset =
291+
static_cast<std::uint64_t>(channel) * number_of_nodes;
253292
detail_grid_features::for_each_valid_offset_link<D>(
254293
graph,
255-
offsets[channel],
294+
offset_array,
256295
[&](const std::uint64_t node, const std::uint64_t, const auto &coordinate) {
257-
const auto edge = detail_grid_features::local_edge_id_from_source_coordinate<D>(
258-
graph, offsets[channel], coordinate, axis, edge_strides
259-
);
260-
if (valid_edges.data[edge] != 0) {
261-
throw std::invalid_argument("offsets produce duplicate local grid edges");
296+
const auto edge =
297+
detail_grid_features::local_edge_id_from_source_coordinate<D>(
298+
edge_offset_base, coordinate, edge_strides, pivot_adjustment
299+
);
300+
if (valid_edges.data[edge] != 0) {
301+
throw std::invalid_argument(
302+
"offsets produce duplicate local grid edges"
303+
);
304+
}
305+
weights.data[edge] = affinities.data[channel_offset + node];
306+
valid_edges.data[edge] = 1;
262307
}
263-
weights.data[edge] = affinities.data[channel_offset + node];
264-
valid_edges.data[edge] = 1;
265-
});
308+
);
266309
}
267310
}
268311

@@ -281,6 +324,7 @@ GridLiftedAffinityFeatures grid_affinity_features_with_lifted(
281324
const std::vector<std::vector<std::ptrdiff_t>> &offsets
282325
) {
283326
detail_grid_features::require_affinity_shape(graph, affinities, offsets);
327+
detail_grid_features::check_no_negated_long_range_offsets<D>(offsets);
284328

285329
const auto number_of_nodes = graph.number_of_nodes();
286330
GridLiftedAffinityFeatures result;
@@ -298,47 +342,52 @@ GridLiftedAffinityFeatures grid_affinity_features_with_lifted(
298342
result.lifted_offset_ids.reserve(static_cast<std::size_t>(lifted_capacity));
299343

300344
for (std::size_t channel = 0; channel < offsets.size(); ++channel) {
301-
const auto l1 = detail_grid_features::l1_norm(offsets[channel]);
302-
const auto channel_offset = static_cast<std::uint64_t>(channel) * number_of_nodes;
345+
const auto offset_array =
346+
detail_grid_features::offset_to_array<D>(offsets[channel]);
347+
const auto l1 = detail_grid_features::l1_norm(offset_array);
348+
const auto channel_offset =
349+
static_cast<std::uint64_t>(channel) * number_of_nodes;
303350
if (l1 == 1) {
304-
const auto axis = detail_grid_features::local_offset_axis<D>(offsets[channel]);
351+
const auto axis = detail_grid_features::local_offset_axis<D>(offset_array);
305352
auto edge_shape = graph.shape();
306353
--edge_shape[axis];
307-
const auto edge_strides = detail_grid_features::uint64_strides_from_shape(edge_shape);
354+
const auto edge_strides =
355+
detail_grid_features::uint64_strides_from_shape(edge_shape);
356+
const auto edge_offset_base = graph.edge_offset(axis);
357+
const std::uint64_t pivot_adjustment =
358+
offset_array[axis] < 0 ? edge_strides[axis] : 0;
308359
detail_grid_features::for_each_valid_offset_link<D>(
309360
graph,
310-
offsets[channel],
361+
offset_array,
311362
[&](const std::uint64_t node, const std::uint64_t, const auto &coordinate) {
312-
const auto edge = detail_grid_features::local_edge_id_from_source_coordinate<D>(
313-
graph, offsets[channel], coordinate, axis, edge_strides
314-
);
315-
if (result.valid_local_edges[static_cast<std::size_t>(edge)] != 0) {
316-
throw std::invalid_argument("offsets produce duplicate local grid edges");
363+
const auto edge =
364+
detail_grid_features::local_edge_id_from_source_coordinate<D>(
365+
edge_offset_base, coordinate, edge_strides, pivot_adjustment
366+
);
367+
if (result.valid_local_edges[static_cast<std::size_t>(edge)] != 0) {
368+
throw std::invalid_argument(
369+
"offsets produce duplicate local grid edges"
370+
);
371+
}
372+
result.local_weights[static_cast<std::size_t>(edge)] =
373+
affinities.data[channel_offset + node];
374+
result.valid_local_edges[static_cast<std::size_t>(edge)] = 1;
317375
}
318-
result.local_weights[static_cast<std::size_t>(edge)] =
319-
affinities.data[channel_offset + node];
320-
result.valid_local_edges[static_cast<std::size_t>(edge)] = 1;
321-
});
376+
);
322377
} else {
323378
detail_grid_features::for_each_valid_offset_link<D>(
324379
graph,
325-
offsets[channel],
380+
offset_array,
326381
[&](const std::uint64_t node, const std::uint64_t target, const auto &) {
327-
const auto uv = bioimage_cpp::detail::edge_key(node, target);
328-
result.lifted_uvs.push_back(uv);
329-
result.lifted_weights.push_back(affinities.data[channel_offset + node]);
330-
result.lifted_offset_ids.push_back(static_cast<std::uint64_t>(channel));
331-
});
382+
const auto uv = bioimage_cpp::detail::edge_key(node, target);
383+
result.lifted_uvs.push_back(uv);
384+
result.lifted_weights.push_back(affinities.data[channel_offset + node]);
385+
result.lifted_offset_ids.push_back(static_cast<std::uint64_t>(channel));
386+
}
387+
);
332388
}
333389
}
334390

335-
auto sorted_lifted_uvs = result.lifted_uvs;
336-
std::sort(sorted_lifted_uvs.begin(), sorted_lifted_uvs.end());
337-
if (std::adjacent_find(sorted_lifted_uvs.begin(), sorted_lifted_uvs.end()) !=
338-
sorted_lifted_uvs.end()) {
339-
throw std::invalid_argument("offsets produce duplicate long-range grid edges");
340-
}
341-
342391
return result;
343392
}
344393

0 commit comments

Comments
 (0)