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 <span>
19#include <type_traits>
20#include <utility>
21#include <vector>
22
23namespace dolfinx
24{
25struct _unsigned_projection
26{
27 // Transforms the projected value to an unsigned int (if signed),
28 // while maintaining relative order by
29 // x ↦ x + |std::numeric_limits<I>::min()|
30 template <std::signed_integral T>
31 constexpr std::make_unsigned_t<T> operator()(T e) const noexcept
32 {
33 using uT = std::make_unsigned_t<T>;
34
35 // Assert binary structure for bit shift
36 static_assert(static_cast<uT>(std::numeric_limits<T>::min())
37 + static_cast<uT>(std::numeric_limits<T>::max())
38 == static_cast<uT>(T(-1)));
39 static_assert(std::numeric_limits<uT>::digits
40 == std::numeric_limits<T>::digits + 1);
41 static_assert(std::bit_cast<uT>(std::numeric_limits<T>::min())
42 == (uT(1) << (sizeof(T) * 8 - 1)));
43
44 return std::bit_cast<uT>(std::forward<T>(e))
45 ^ (uT(1) << (sizeof(T) * 8 - 1));
46 }
47};
48
50inline constexpr _unsigned_projection unsigned_projection{};
51
76template <int BITS = 8, typename P = std::identity,
77 std::ranges::random_access_range R>
78constexpr void radix_sort(R&& range, P proj = {})
79{
80 using bits_t = std::make_unsigned_t<
81 std::remove_cvref_t<std::invoke_result_t<P, std::iter_value_t<R>>>>;
82 constexpr bits_t _BITS = BITS;
83
84 // Value type
85 using T = std::iter_value_t<R>;
86
87 // Index type (if no projection is provided it holds I == T)
88 using I = std::remove_cvref_t<std::invoke_result_t<P, T>>;
89 using uI = std::make_unsigned_t<I>;
90
91 if constexpr (!std::is_same_v<uI, I>)
92 {
93 radix_sort<_BITS>(std::forward<R>(range), [&](const T& e) -> uI
94 { return unsigned_projection(proj(e)); });
95 return;
96 }
97
98 if (range.size() <= 1)
99 return;
100
101 // Sort N bits at a time
102 constexpr uI bucket_size = 1 << _BITS;
103 uI mask = (uI(1) << _BITS) - 1;
104 constexpr uI top_bit = uI(1) << (sizeof(uI) * 8 - 1);
105
106 // Adjacency list arrays for computing insertion position. counter is
107 // pre-filled below with the first pass's histogram (bucketing on the
108 // low BITS bits, which is always the correct pass-0 bucket regardless
109 // of the top-bit special case), so that pass can skip a second full
110 // traversal to build it.
111 std::array<I, bucket_size> counter{};
112 std::array<I, bucket_size> offset;
113
114 // Single pass computing the maximum projected value, whether all
115 // elements share the top bit (in which case it carries no ordering
116 // information and can be dropped, reducing the iteration count), and
117 // the first pass's histogram
118 uI max_value = 0;
119 bool all_first_bit = true;
120 for (const auto& e : range)
121 {
122 uI v = proj(e);
123 max_value = std::max(max_value, v);
124 all_first_bit = all_first_bit && (v & top_bit);
125 counter[v & mask]++;
126 }
127
128 if (all_first_bit)
129 max_value = max_value & ~top_bit;
130
131 // Compute number of iterations, most significant digit (N bits) of
132 // maxvalue
133 I its = 0;
134 while (max_value)
135 {
136 max_value >>= _BITS;
137 its++;
138 }
139
140 uI mask_offset = 0;
141 std::vector<T> buffer(range.size());
142 std::span<T> current_perm = range;
143 std::span<T> next_perm = buffer;
144 for (I i = 0; i < its; i++)
145 {
146 if (i > 0)
147 {
148 // Zero counter array
149 std::ranges::fill(counter, 0);
150
151 // Count number of elements per bucket
152 for (auto c : current_perm)
153 counter[(proj(c) & mask) >> mask_offset]++;
154 }
155
156 // Exclusive prefix sum, used directly as the insertion cursor for
157 // each bucket
158 std::exclusive_scan(counter.begin(), counter.end(), offset.begin(), I(0));
159 for (auto c : current_perm)
160 {
161 uI bucket = (proj(c) & mask) >> mask_offset;
162 next_perm[offset[bucket]++] = c;
163 }
164
165 mask = mask << _BITS;
166 mask_offset += _BITS;
167
168 std::swap(current_perm, next_perm);
169 }
170
171 // Copy data back to array
172 if (its % 2 != 0)
173 std::ranges::copy(buffer, range.begin());
174}
175
186template <typename T, int BITS = 16>
187std::vector<std::int32_t> sort_by_perm(std::span<const T> x, std::size_t shape1)
188{
189 static_assert(std::is_integral_v<T>, "Integral required.");
190
191 if (x.empty())
192 return std::vector<std::int32_t>{};
193
194 assert(shape1 > 0);
195 assert(x.size() % shape1 == 0);
196 const std::size_t shape0 = x.size() / shape1;
197 std::vector<std::int32_t> perm(shape0);
198 std::iota(perm.begin(), perm.end(), 0);
199
200 // Sort by each column, right to left. Col 0 has the most significant
201 // "digit".
202 std::vector<T> column(shape0);
203 for (std::size_t i = 0; i < shape1; ++i)
204 {
205 std::size_t col = shape1 - 1 - i;
206 for (std::size_t j = 0; j < shape0; ++j)
207 column[j] = x[j * shape1 + col];
208 radix_sort<BITS>(perm, [column = std::cref(column)](auto index)
209 { return column.get()[index]; });
210 }
211
212 return perm;
213}
214
215} // namespace dolfinx
Top-level namespace.
Definition defines.h:12
constexpr _unsigned_projection unsigned_projection
Projection from signed to signed int.
Definition sort.h:50
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:78
std::vector< std::int32_t > sort_by_perm(std::span< const T > x, std::size_t shape1)
Compute the permutation array that sorts a 2D array by row.
Definition sort.h:187