MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Weights.hpp
Go to the documentation of this file.
1#pragma once
2
5
7
8/**
9 * @class Weights
10 * @brief Rows-by-columns table of doubles with atomic cell access
11 *
12 * A plain numeric table with no knowledge of what its rows and columns mean.
13 * Transition probabilities, source-to-output weights, and neighbour strengths
14 * are the same structure read by different code.
15 *
16 * Cells are read and written with relaxed atomics, so any thread can write a
17 * cell while another reads it. Row helpers and row views are not atomic and
18 * belong on the thread that consumes the table. Copying is not synchronised.
19 * Dimensions change only through assign(), which must not run concurrently
20 * with any other access.
21 *
22 * weighted_pick() turns a row into a single stochastic choice, drawing from
23 * a Stochastic instance the caller supplies and owns (one instance per
24 * thread, matching Stochastic's own contract).
25 *
26 * ## Usage
27 *
28 * ```cpp
29 * Weights transitions(5, 5);
30 * transitions.normalize_row(0);
31 *
32 * Stochastic draw(Algorithm::UNIFORM);
33 * size_t next = transitions.weighted_pick(0, draw);
34 * ```
35 */
36class Weights {
37public:
38 static_assert(std::atomic_ref<double>::is_always_lock_free);
39
40 /**
41 * @class Cell
42 * @brief Proxy for one cell with atomic load and store
43 */
44 class Cell {
45 public:
46 Cell& operator=(double value) noexcept
47 {
48 std::atomic_ref<double>(m_data[m_index]).store(value, std::memory_order_relaxed);
49 return *this;
50 }
51
52 Cell& operator=(const Cell& other) noexcept
53 {
54 return *this = static_cast<double>(other);
55 }
56
57 [[nodiscard]] operator double() const noexcept
58 {
59 return std::atomic_ref<double>(m_data[m_index]).load(std::memory_order_relaxed);
60 }
61
62 private:
63 friend class Weights;
64
65 Cell(std::vector<double>& data, size_t index) noexcept
66 : m_data(data)
67 , m_index(index)
68 {
69 }
70
71 std::vector<double>& m_data;
72 size_t m_index;
73 };
74
75 Weights() = default;
76
77 /**
78 * @param rows Row count
79 * @param cols Column count
80 * @param initial Value of every cell
81 */
82 Weights(size_t rows, size_t cols, double initial = 0.0)
83 : m_rows(rows)
84 , m_cols(cols)
85 , m_data(rows * cols, initial)
86 {
87 }
88
89 /**
90 * @brief Assign every cell from nested initializer lists
91 * @param values One inner list per row. Dimensions must match exactly.
92 * @throws std::invalid_argument on dimension mismatch
93 */
94 Weights& operator=(std::initializer_list<std::initializer_list<double>> values)
95 {
96 if (values.size() != m_rows) {
97 error<std::invalid_argument>(Journal::Component::Kinesis, Journal::Context::Runtime, std::source_location::current(),
98 "Weights: row count mismatch. Expected {} rows, received {}", m_rows, values.size());
99 }
100 size_t row = 0;
101 for (const auto& source : values) {
102 if (source.size() != m_cols) {
103 error<std::invalid_argument>(Journal::Component::Kinesis, Journal::Context::Runtime, std::source_location::current(),
104 "Weights: column count mismatch at row {}. Expected {} cols, received {}", row, m_cols, source.size());
105 }
106 size_t col = 0;
107 for (double value : source) {
108 (*this)(row, col++) = value;
109 }
110 ++row;
111 }
112 return *this;
113 }
114
115 /**
116 * @brief Replace dimensions and reset every cell
117 */
118 void assign(size_t rows, size_t cols, double initial = 0.0)
119 {
120 m_rows = rows;
121 m_cols = cols;
122 m_data.assign(rows * cols, initial);
123 }
124
125 [[nodiscard]] size_t rows() const noexcept { return m_rows; }
126 [[nodiscard]] size_t cols() const noexcept { return m_cols; }
127
128 /**
129 * @brief Access one cell
130 * @param row Row index, unchecked
131 * @param col Column index, unchecked
132 */
133 [[nodiscard]] Cell operator()(size_t row, size_t col) noexcept
134 {
135 return { m_data, row * m_cols + col };
136 }
137
138 /**
139 * @brief Atomic read of one cell
140 */
141 [[nodiscard]] double get(size_t row, size_t col) const noexcept
142 {
143 return std::atomic_ref<double>(const_cast<double&>(m_data[row * m_cols + col]))
144 .load(std::memory_order_relaxed);
145 }
146
147 /**
148 * @brief Non-atomic view of one row
149 */
150 [[nodiscard]] std::span<const double> row(size_t row) const noexcept
151 {
152 return { m_data.data() + row * m_cols, m_cols };
153 }
154
155 void fill(double value) noexcept
156 {
157 for (double& slot : m_data) {
158 std::atomic_ref<double>(slot).store(value, std::memory_order_relaxed);
159 }
160 }
161
162 void scale_row(size_t row, double factor) noexcept
163 {
164 for (size_t col = 0; col < m_cols; ++col) {
165 Cell cell = (*this)(row, col);
166 cell = static_cast<double>(cell) * factor;
167 }
168 }
169
170 /**
171 * @brief Scale a row so its cells sum to one
172 * @note A row that sums to zero is left unchanged.
173 */
174 void normalize_row(size_t row) noexcept
175 {
176 double sum = 0.0;
177 for (size_t col = 0; col < m_cols; ++col) {
178 sum += get(row, col);
179 }
180 if (sum != 0.0) {
181 scale_row(row, 1.0 / sum);
182 }
183 }
184
185 /**
186 * @brief Every cell uniform except the diagonal, each row normalized
187 * to sum to one
188 * @note Assumes a square table. A row beyond the column count has no
189 * diagonal cell to zero and is filled uniformly as-is.
190 *
191 * The common starting point for a transition table that should never
192 * pick its own row: every other cell equally likely, before any
193 * further shaping.
194 */
196 {
197 fill(1.0);
198 for (size_t i = 0; i < m_rows; ++i) {
199 if (i < m_cols) {
200 (*this)(i, i) = 0.0;
201 }
202 normalize_row(i);
203 }
204 }
205
206 /**
207 * @brief Write an adjacency edge list into this table
208 * @param edges Index pairs, the same shape Kinesis::EdgeList is
209 * (Spatial::ProximityGraphs' sequential_chain(),
210 * k_nearest_neighbors(), minimum_spanning_tree(), and so on
211 * all return one)
212 * @param weight Value written at each edge
213 * @param symmetric When true (the default), also writes the reverse
214 * direction for every edge, matching an undirected graph. Some
215 * EdgeList producers (k_nearest_neighbors, nearest_neighbor_graph)
216 * are already directed; pass false for those.
217 * @param normalize When true (the default), every row touched by an
218 * edge is normalized afterward, so the result is directly
219 * usable as a transition table.
220 * @note An index outside this table's bounds is skipped rather than
221 * trusted, since edges typically comes from an external
222 * computation over a possibly different point count.
223 */
224 void apply_edges(std::span<const std::pair<size_t, size_t>> edges,
225 double weight = 1.0, bool symmetric = true, bool normalize = true) noexcept
226 {
227 for (const auto& [a, b] : edges) {
228 if (a < m_rows && b < m_cols) {
229 (*this)(a, b) = weight;
230 }
231 if (symmetric && b < m_rows && a < m_cols) {
232 (*this)(b, a) = weight;
233 }
234 }
235 if (normalize) {
236 for (size_t i = 0; i < m_rows; ++i) {
237 normalize_row(i);
238 }
239 }
240 }
241
242 /**
243 * @brief Weighted random draw over a row of nonnegative weights
244 * @param weights Row to draw from. Negative entries are treated as zero.
245 * @param generator Entropy source for the draw
246 * @return Index into weights, or weights.size() when every weight is
247 * zero or negative and no draw is possible
248 *
249 * Does not require a Weights instance or normalized weights; any span
250 * of nonnegative values is a valid distribution to draw from.
251 */
252 [[nodiscard]] static size_t weighted_pick(std::span<const double> weights, Stochastic& generator)
253 {
254 double total = 0.0;
255 for (double weight : weights) {
256 total += std::max(0.0, weight);
257 }
258
259 if (total <= 0.0) {
260 return weights.size();
261 }
262
263 const double target = generator(0.0, total);
264 double running = 0.0;
265 for (size_t i = 0; i < weights.size(); ++i) {
266 running += std::max(0.0, weights[i]);
267 if (target <= running) {
268 return i;
269 }
270 }
271 return weights.size() - 1;
272 }
273
274 /**
275 * @brief Weighted random draw over one row of this table
276 * @param row_index Row index, unchecked
277 * @param generator Entropy source for the draw
278 * @return Index into the row, or cols() when every weight in the row
279 * is zero or negative
280 */
281 [[nodiscard]] size_t weighted_pick(size_t row_index, Stochastic& generator) const
282 {
283 return weighted_pick(row(row_index), generator);
284 }
285
286private:
287 size_t m_rows {};
288 size_t m_cols {};
289 std::vector<double> m_data;
290};
291
292} // namespace MayaFlux::Kinesis::Stochastic
uint32_t total
size_t a
double weight
size_t b
glm::vec3 value
std::vector< std::byte > values
Definition VDBWriter.cpp:26
uint32_t index
Definition VKDevice.cpp:122
Unified generative infrastructure for stochastic and procedural algorithms.
Cell & operator=(const Cell &other) noexcept
Definition Weights.hpp:52
Cell & operator=(double value) noexcept
Definition Weights.hpp:46
Cell(std::vector< double > &data, size_t index) noexcept
Definition Weights.hpp:65
Proxy for one cell with atomic load and store.
Definition Weights.hpp:44
std::span< const double > row(size_t row) const noexcept
Non-atomic view of one row.
Definition Weights.hpp:150
void assign(size_t rows, size_t cols, double initial=0.0)
Replace dimensions and reset every cell.
Definition Weights.hpp:118
static size_t weighted_pick(std::span< const double > weights, Stochastic &generator)
Weighted random draw over a row of nonnegative weights.
Definition Weights.hpp:252
void fill(double value) noexcept
Definition Weights.hpp:155
Weights & operator=(std::initializer_list< std::initializer_list< double > > values)
Assign every cell from nested initializer lists.
Definition Weights.hpp:94
void apply_edges(std::span< const std::pair< size_t, size_t > > edges, double weight=1.0, bool symmetric=true, bool normalize=true) noexcept
Write an adjacency edge list into this table.
Definition Weights.hpp:224
Cell operator()(size_t row, size_t col) noexcept
Access one cell.
Definition Weights.hpp:133
void uniform_excluding_self() noexcept
Every cell uniform except the diagonal, each row normalized to sum to one.
Definition Weights.hpp:195
void scale_row(size_t row, double factor) noexcept
Definition Weights.hpp:162
Weights(size_t rows, size_t cols, double initial=0.0)
Definition Weights.hpp:82
double get(size_t row, size_t col) const noexcept
Atomic read of one cell.
Definition Weights.hpp:141
size_t weighted_pick(size_t row_index, Stochastic &generator) const
Weighted random draw over one row of this table.
Definition Weights.hpp:281
void normalize_row(size_t row) noexcept
Scale a row so its cells sum to one.
Definition Weights.hpp:174
Rows-by-columns table of doubles with atomic cell access.
Definition Weights.hpp:36
@ Runtime
General runtime operations (default fallback)
@ Kinesis
General mathematical and physics algorithns.