Skip to content

Commit c42c2c8

Browse files
Solve merge conflict
2 parents f8b4284 + fad254f commit c42c2c8

47 files changed

Lines changed: 888 additions & 286 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MIGRATION_GUIDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2293,9 +2293,10 @@ Important differences:
22932293
per-point radius is dynamic (the distance value at each point), matching
22942294
nifty; there is no fixed-radius mode.
22952295
- The algorithm is otherwise identical to nifty, including its float
2296-
arithmetic, so results match element-for-element. It uses an O(N²)
2297-
pairwise distance matrix; threshold the distance map first to keep the
2298-
candidate count modest.
2296+
arithmetic, so results match element-for-element. It is O(N²) in time but
2297+
uses only O(`number_of_threads` · N) auxiliary memory (no dense N×N matrix),
2298+
and `number_of_threads` parallelizes the pairwise evaluation. Still threshold
2299+
the distance map first to keep the candidate count modest.
22992300

23002301
## scikit-fmm
23012302

include/bioimage_cpp/affinities/compute_affinities.hxx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "bioimage_cpp/array_view.hxx"
4+
#include "bioimage_cpp/detail/grid.hxx"
45
#include "bioimage_cpp/detail/threading.hxx"
56

67
#include <algorithm>
@@ -66,9 +67,9 @@ void compute_affinities_2d(
6667
}
6768

6869
// Sub-rectangle where (y, x) AND (y+dy, x+dx) are both in bounds.
69-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
70+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
7071
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
71-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
72+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
7273
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
7374
if (y_begin >= y_end || x_begin >= x_end) {
7475
continue;
@@ -143,11 +144,11 @@ void compute_affinities_3d(
143144
std::fill_n(mask_channel, volume, std::uint8_t{0});
144145
}
145146

146-
const auto z_begin = std::max<std::ptrdiff_t>(0, -dz);
147+
const auto z_begin = ::bioimage_cpp::detail::axis_begin_offset(dz, depth);
147148
const auto z_end = depth - std::max<std::ptrdiff_t>(0, dz);
148-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
149+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
149150
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
150-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
151+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
151152
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
152153
if (z_begin >= z_end || y_begin >= y_end || x_begin >= x_end) {
153154
continue;

include/bioimage_cpp/affinities/compute_embedding_distances.hxx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "bioimage_cpp/array_view.hxx"
4+
#include "bioimage_cpp/detail/grid.hxx"
45
#include "bioimage_cpp/detail/threading.hxx"
56

67
#include <algorithm>
@@ -108,9 +109,9 @@ void compute_embedding_distances_2d(
108109
distances.data + static_cast<std::ptrdiff_t>(oi) * plane;
109110
std::fill_n(dist_channel, plane, ValueT{0});
110111

111-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
112+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
112113
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
113-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
114+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
114115
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
115116
if (y_begin >= y_end || x_begin >= x_end) {
116117
continue;
@@ -180,11 +181,11 @@ void compute_embedding_distances_3d(
180181
distances.data + static_cast<std::ptrdiff_t>(oi) * volume;
181182
std::fill_n(dist_channel, volume, ValueT{0});
182183

183-
const auto z_begin = std::max<std::ptrdiff_t>(0, -dz);
184+
const auto z_begin = ::bioimage_cpp::detail::axis_begin_offset(dz, depth);
184185
const auto z_end = depth - std::max<std::ptrdiff_t>(0, dz);
185-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
186+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
186187
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
187-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
188+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
188189
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
189190
if (z_begin >= z_end || y_begin >= y_end || x_begin >= x_end) {
190191
continue;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <cstdint>
5+
#include <limits>
6+
#include <stdexcept>
7+
#include <string>
8+
9+
namespace bioimage_cpp::detail {
10+
11+
inline std::size_t checked_size_cast(const std::uint64_t value, const char *name) {
12+
if constexpr (sizeof(std::size_t) < sizeof(std::uint64_t)) {
13+
if (value > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
14+
throw std::overflow_error(std::string(name) + " exceeds size_t range");
15+
}
16+
}
17+
return static_cast<std::size_t>(value);
18+
}
19+
20+
inline std::size_t checked_size_add(
21+
const std::size_t a,
22+
const std::size_t b,
23+
const char *name
24+
) {
25+
if (b > std::numeric_limits<std::size_t>::max() - a) {
26+
throw std::overflow_error(std::string(name) + " exceeds size_t range");
27+
}
28+
return a + b;
29+
}
30+
31+
inline std::size_t checked_size_multiply(
32+
const std::size_t a,
33+
const std::size_t b,
34+
const char *name
35+
) {
36+
if (a != 0 && b > std::numeric_limits<std::size_t>::max() / a) {
37+
throw std::overflow_error(std::string(name) + " exceeds size_t range");
38+
}
39+
return a * b;
40+
}
41+
42+
inline std::uint64_t checked_u64_add(
43+
const std::uint64_t a,
44+
const std::uint64_t b,
45+
const char *name
46+
) {
47+
if (b > std::numeric_limits<std::uint64_t>::max() - a) {
48+
throw std::overflow_error(std::string(name) + " exceeds uint64 range");
49+
}
50+
return a + b;
51+
}
52+
53+
} // namespace bioimage_cpp::detail

include/bioimage_cpp/detail/grid.hxx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ inline void valid_axis_range(
104104
const auto d = static_cast<std::size_t>(delta);
105105
hi = (d >= length) ? 0 : (length - d);
106106
} else {
107-
const auto d = static_cast<std::size_t>(-delta);
107+
// Avoid negating PTRDIFF_MIN. Converting first and subtracting from
108+
// zero computes the magnitude in the unsigned domain.
109+
const auto d = std::size_t{0} - static_cast<std::size_t>(delta);
108110
lo = (d >= length) ? length : d;
109111
hi = length;
110112
}
@@ -204,4 +206,24 @@ void sweep_clipped_box_3d(
204206
}
205207
}
206208

209+
// Number of leading positions to skip along an axis of length `length` for a
210+
// signed offset `delta` (i.e. `max(0, -delta)` clamped to `length`). Returns a
211+
// plain `std::ptrdiff_t` so the affinity kernels keep their inline
212+
// `ptrdiff_t` loop bounds and inner-loop codegen, while avoiding the undefined
213+
// behaviour of negating `delta == PTRDIFF_MIN`: magnitudes at least as large as
214+
// the axis (which make the whole offset channel empty) are clamped to `length`,
215+
// so the negation only runs when `-length < delta < 0` and is always safe.
216+
inline std::ptrdiff_t axis_begin_offset(
217+
const std::ptrdiff_t delta,
218+
const std::ptrdiff_t length
219+
) {
220+
if (delta >= 0) {
221+
return 0;
222+
}
223+
if (delta <= -length) {
224+
return length;
225+
}
226+
return -delta;
227+
}
228+
207229
} // namespace bioimage_cpp::detail

include/bioimage_cpp/detail/threading.hxx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <cstddef>
55
#include <exception>
66
#include <mutex>
7+
#include <stdexcept>
78
#include <thread>
89
#include <utility>
910
#include <vector>
@@ -29,17 +30,23 @@ inline std::size_t normalize_thread_count(
2930

3031
// Split [0, number_of_work_items) into n_threads contiguous chunks and invoke
3132
// `chunk(thread_id, begin, end)` on each chunk. Thread 0 runs on the calling
32-
// thread; threads 1..n_threads-1 run in parallel std::threads. The caller is
33+
// thread; threads 1..n_threads-1 run in parallel std::jthreads. The caller is
3334
// responsible for picking n_threads via normalize_thread_count and for the
34-
// thread safety of `chunk`. If any chunk throws, every thread is still joined
35-
// and the first exception is rethrown on the calling thread (the remaining
36-
// chunks run to completion; there is no cancellation).
35+
// thread safety of `chunk`.
36+
//
37+
// Exceptions thrown by `chunk` are captured rather than allowed to escape a
38+
// worker thread (which would call std::terminate). The first exception on any
39+
// thread is stored and rethrown on the calling thread after every worker has
40+
// been joined, so nanobind can translate it into a Python exception.
3741
template <class Chunk>
3842
void parallel_for_chunks(
3943
const std::size_t n_threads,
4044
const std::size_t number_of_work_items,
4145
Chunk &&chunk
4246
) {
47+
if (n_threads == 0) {
48+
throw std::invalid_argument("parallel_for_chunks requires n_threads >= 1");
49+
}
4350
const auto bounds = [&](const std::size_t thread_id) {
4451
const auto begin = thread_id * number_of_work_items / n_threads;
4552
const auto end = (thread_id + 1) * number_of_work_items / n_threads;
@@ -48,8 +55,10 @@ void parallel_for_chunks(
4855

4956
std::exception_ptr first_exception;
5057
std::mutex exception_mutex;
51-
const auto guarded_chunk = [&](
52-
const std::size_t thread_id, const std::size_t begin, const std::size_t end
58+
const auto guarded_chunk = [&] (
59+
const std::size_t thread_id,
60+
const std::size_t begin,
61+
const std::size_t end
5362
) {
5463
try {
5564
chunk(thread_id, begin, end);
@@ -61,7 +70,11 @@ void parallel_for_chunks(
6170
}
6271
};
6372

64-
std::vector<std::thread> threads;
73+
// jthread's destructor joins, so a failure while creating a later worker
74+
// cannot destroy an earlier still-joinable thread and terminate the
75+
// process. We still join explicitly below to rethrow only after all work
76+
// has completed.
77+
std::vector<std::jthread> threads;
6578
threads.reserve(n_threads > 0 ? n_threads - 1 : 0);
6679
for (std::size_t thread_id = 1; thread_id < n_threads; ++thread_id) {
6780
const auto [begin, end] = bounds(thread_id);

0 commit comments

Comments
 (0)