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 {
2118namespace detail_grid_features {
2219
2320template <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
3334inline 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+
4152template <std::size_t D>
4253void 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`.
90107template <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
116131template <std::size_t D>
@@ -126,7 +141,7 @@ std::array<std::uint64_t, D> uint64_strides_from_shape(
126141}
127142
128143template <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+
138183template <std::size_t D, class Callback >
139184void 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.
180216template <std::size_t D>
181217std::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