DOLFINx 0.12.0.0
DOLFINx C++
Loading...
Searching...
No Matches
sort.h
1// Copyright (C) 2021-2025 Igor Baratta and Paul T. Kühner
2//
3// This file is part of DOLFINx (https://www.fenicsproject.org)
4//
5// SPDX-License-Identifier: LGPL-3.0-or-later
6
7#pragma once
8
9#include <algorithm>
10#include <bit>
11#include <cassert>
12#include <concepts>
13#include <cstdint>
14#include <functional>
15#include <iterator>
16#include <limits>
17#include <numeric>
18#include <optional>
19#include <span>
20#include <type_traits>
21#include <utility>
22#include <vector>
23
24namespace dolfinx
25{
26struct _unsigned_projection
27{
28 // Transforms the projected value to an unsigned int (if signed),
29 // while maintaining relative order by
30 // x ↦ x + |std::numeric_limits<I>::min()|
31 template <std::signed_integral T>
32 constexpr std::make_unsigned_t<T> operator()(T e) const noexcept
33 {
34 using uT = std::make_unsigned_t<T>;
35
36 // Assert binary structure for bit shift
37 static_assert(static_cast<uT>(std::numeric_limits<T>::min())
38 + static_cast<uT>(std::numeric_limits<T>::max())
39 == static_cast<uT>(T(-1)));
40 static_assert(std::numeric_limits<uT>::digits
41 == std::numeric_limits<T>::digits + 1);
42 static_assert(std::bit_cast<uT>(std::numeric_limits<T>::min())
43 == (uT(1) << (sizeof(T) * 8 - 1)));
44
45 return std::bit_cast<uT>(std::forward<T>(e))
46 ^ (uT(1) << (sizeof(T) * 8 - 1));
47 }
48};
49
51inline constexpr _unsigned_projection unsigned_projection{};
52
79template <int BITS = 8, typename P = std::identity,
80 std::ranges::random_access_range R>
81constexpr void radix_sort(R&& range, P proj = {})
82{
83 using bits_t = std::make_unsigned_t<
84 std::remove_cvref_t<std::invoke_result_t<P, std::iter_value_t<R>>>>;
85 constexpr bits_t _BITS = BITS;
86
87 // Value type
88 using T = std::iter_value_t<R>;
89
90 // Index type (if no projection is provided it holds I == T)
91 using I = std::remove_cvref_t<std::invoke_result_t<P, T>>;
92 using uI = std::make_unsigned_t<I>;
93
94 if constexpr (!std::is_same_v<uI, I>)
95 {
96 radix_sort<_BITS>(std::forward<R>(range), [&proj](const T& e) -> uI
97 { return unsigned_projection(proj(e)); });
98 return;
99 }
100
101 if (range.size() <= 1)
102 return;
103
104 // Sort N bits at a time
105 constexpr uI bucket_size = 1 << _BITS;
106 uI mask = (uI(1) << _BITS) - 1;
107 constexpr uI top_bit = uI(1) << (sizeof(uI) * 8 - 1);
108
109 // Adjacency list arrays for computing insertion position. counter is
110 // pre-filled below with the first pass's histogram (bucketing on the
111 // low BITS bits, which is always the correct pass-0 bucket regardless
112 // of the top-bit special case), so that pass can skip a second full
113 // traversal to build it.
114 std::array<I, bucket_size> counter{};
115 std::array<I, bucket_size> offset;
116
117 // Single pass computing the maximum projected value, whether all
118 // elements share the top bit (in which case it carries no ordering
119 // information and can be dropped, reducing the iteration count), and
120 // the first pass's histogram
121 uI max_value = 0;
122 bool all_first_bit = true;
123 for (const auto& e : range)
124 {
125 uI v = proj(e);
126 max_value = std::max(max_value, v);
127 all_first_bit = all_first_bit && (v & top_bit);
128 counter[v & mask]++;
129 }
130
131 if (all_first_bit)
132 max_value = max_value & ~top_bit;
133
134 // Compute number of iterations, most significant digit (N bits) of
135 // maxvalue
136 I its = 0;
137 while (max_value)
138 {
139 max_value >>= _BITS;
140 its++;
141 }
142
143 uI mask_offset = 0;
144 std::vector<T> buffer(range.size());
145 std::span<T> current_perm = range;
146 std::span<T> next_perm = buffer;
147 for (I i = 0; i < its; i++)
148 {
149 if (i > 0)
150 {
151 // Zero counter array
152 std::ranges::fill(counter, 0);
153
154 // Count number of elements per bucket
155 for (auto c : current_perm)
156 counter[(proj(c) & mask) >> mask_offset]++;
157 }
158
159 // Exclusive prefix sum, used directly as the insertion cursor for
160 // each bucket
161 std::exclusive_scan(counter.begin(), counter.end(), offset.begin(), I(0));
162 for (auto c : current_perm)
163 {
164 uI bucket = (proj(c) & mask) >> mask_offset;
165 next_perm[offset[bucket]++] = c;
166 }
167
168 mask = mask << _BITS;
169 mask_offset += _BITS;
170
171 std::swap(current_perm, next_perm);
172 }
173
174 // Copy data back to array
175 if (its % 2 != 0)
176 std::ranges::copy(buffer, range.begin());
177}
178
199template <typename T, int BITS = 16>
200std::vector<std::int32_t> sort_by_perm(std::span<const T> x, std::size_t shape1,
201 std::optional<std::size_t> ncols
202 = std::nullopt)
203{
204 static_assert(std::is_integral_v<T>, "Integral required.");
205
206 if (x.empty())
207 return std::vector<std::int32_t>{};
208
209 assert(shape1 > 0);
210 assert(x.size() % shape1 == 0);
211 std::size_t n = ncols.value_or(shape1);
212 assert(n <= shape1);
213 const std::size_t shape0 = x.size() / shape1;
214 std::vector<std::int32_t> perm(shape0);
215 std::iota(perm.begin(), perm.end(), 0);
216
217 // Sort by each of the leading `n` columns, right to left. Col 0 has
218 // the most significant "digit"; any columns from `n` to `shape1 - 1`
219 // are excluded from the key.
220 std::vector<T> column(shape0);
221 for (std::size_t i = 0; i < n; ++i)
222 {
223 std::size_t col = n - 1 - i;
224 for (std::size_t j = 0; j < shape0; ++j)
225 column[j] = x[j * shape1 + col];
226 radix_sort<BITS>(perm, [column = std::cref(column)](auto index)
227 { return column.get()[index]; });
228 }
229
230 return perm;
231}
232
249template <typename T, int BITS = 16>
250std::vector<std::int32_t> sort_by_perm(std::span<std::span<const T>> x)
251{
252 static_assert(std::is_integral_v<T>, "Integral required.");
253 if (x.empty())
254 return {};
255
256 std::size_t shape1 = x.size();
257 std::size_t shape0 = x.front().size();
258
259 std::vector<std::int32_t> perm(shape0);
260 std::iota(perm.begin(), perm.end(), 0);
261
262 // Each column is already its own contiguous span -- no copy needed,
263 // just index into `x` for the column itself.
264 for (std::size_t i = 0; i < shape1; ++i)
265 {
266 std::size_t col = shape1 - 1 - i;
267 assert(x[col].size() == shape0);
268 std::span<const T> column = x[col];
269 radix_sort<BITS>(perm, [column](auto index) { return column[index]; });
270 }
271 return perm;
272}
273
274} // namespace dolfinx
Top-level namespace.
Definition defines.h:12
constexpr _unsigned_projection unsigned_projection
Projection from signed to signed int.
Definition sort.h:51
constexpr void radix_sort(R &&range, P proj={})
Sort a range with radix sorting algorithm. The bucket size is determined by the number of bits to sor...
Definition sort.h:81
std::vector< std::int32_t > sort_by_perm(std::span< const T > x, std::size_t shape1, std::optional< std::size_t > ncols=std::nullopt)
Compute the permutation array that sorts a 2D array by row.
Definition sort.h:200