MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
RelationOperators.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "NetworkOperator.hpp"
4
8
10
11class RelationNetwork;
12
13/**
14 * @class Source
15 * @brief Anything that can supply a number when evaluated
16 *
17 * The right-hand side of a mapping wherever an operator parameter should be
18 * drivable by whatever is at hand:
19 * - A constant returns itself.
20 * - A node returns its get_last_output().
21 * - A NodeNetwork plus an index returns that network's
22 * get_node_output(index), or 0.0 if it has nothing there. Works the same
23 * whether the network is this RelationNetwork or any other NodeNetwork.
24 * - A callable is invoked on whichever thread evaluates the source, so it
25 * follows the same rules as a node hook on the audio thread.
26 */
27class Source {
28public:
29 using Function = std::function<double()>;
30
31 Source(double constant)
32 : m_value(constant)
33 {
34 }
35
36 template <std::derived_from<Node> NodeType>
37 Source(std::shared_ptr<NodeType> node)
38 : m_value(std::shared_ptr<Node>(std::move(node)))
39 {
40 }
41
42 /**
43 * @brief One indexed output of a NodeNetwork
44 * @param network Read through get_node_output(index) on every evaluate()
45 * @param index Position within network, the same index
46 * NodeNetwork::map_parameter's ONE_TO_ONE mode and
47 * apply_one_to_one() read
48 */
49 template <std::derived_from<NodeNetwork> NetworkType>
50 Source(std::shared_ptr<NetworkType> network, size_t index)
51 : m_value(NetworkIndex { .network = std::shared_ptr<NodeNetwork>(std::move(network)), .index = index })
52 {
53 }
54
55 template <typename Callable>
56 requires(!std::same_as<std::remove_cvref_t<Callable>, Source>
57 && std::is_invocable_r_v<double, Callable&>)
58 Source(Callable&& callable)
59 : m_value(Function(std::forward<Callable>(callable)))
60 {
61 }
62
63 /**
64 * @brief Current value
65 * @return The constant, the node's last output, the network index's
66 * output, or the callable's result. Zero for an empty node, a
67 * network index the network has no value for, or an empty
68 * callable.
69 */
70 [[nodiscard]] double evaluate() const
71 {
72 if (const auto* constant = std::get_if<double>(&m_value)) {
73 return *constant;
74 }
75 if (const auto* node = std::get_if<std::shared_ptr<Node>>(&m_value)) {
76 return *node ? (*node)->get_last_output() : 0.0;
77 }
78 if (const auto* index = std::get_if<NetworkIndex>(&m_value)) {
79 return index->network ? index->network->get_node_output(index->index).value_or(0.0) : 0.0;
80 }
81 const auto& function = std::get<Function>(m_value);
82 return function ? function() : 0.0;
83 }
84
85private:
86 struct NetworkIndex {
87 std::shared_ptr<NodeNetwork> network;
88 size_t index;
89 };
90
91 std::variant<double, std::shared_ptr<Node>, NetworkIndex, Function> m_value;
92};
93
94/**
95 * @class RelationOperator
96 * @brief Operator that acts on a network's slots over one block at a time
97 *
98 * The network supplies the current slot storage through set_slots(), called
99 * once when the operator is added and again at the start of every
100 * process_batch(). An operator never caches a slot address past the current
101 * cycle, since the network's slot storage can grow (and reallocate) between
102 * cycles.
103 *
104 * A concrete operator has no generic initialize()/reset() lifecycle: it
105 * sizes any internal state from its own constructor arguments.
106 *
107 * Once per block the host calls begin_block(), which applies mapped
108 * parameters, then process(frames).
109 *
110 * An operator has no clock of its own; process_batch() is its only cadence.
111 * Anything finer (per-sample or event-driven behavior) is composed from
112 * outside: node hooks, tasks, or direct calls to the operator's own public
113 * methods. An operator that acts only when called leaves process() empty.
114 * Operators communicate through slots: level and activity writes, and
115 * rendered blocks.
116 *
117 * An operator that publishes signals overrides publishes_output(),
118 * get_output_count(), and get_output_block().
119 */
120class MAYAFLUX_API RelationOperator : public NetworkOperator {
121public:
122 /**
123 * @brief Supply the current slot storage
124 * @param slots RelationNetwork's slot storage, valid only for the caller's
125 * current cycle
126 * @note Called by RelationNetwork::add() and at the start of every
127 * process_batch(). An operator is not usable before the first call.
128 */
129 void set_slots(std::vector<RelationSlot>& slots) noexcept { m_slots = &slots; }
130
131 /**
132 * @brief Apply mapped parameters at the start of a block
133 */
135 {
136 for (const auto& binding : m_bindings) {
137 set_parameter(binding.parameter, binding.source.evaluate());
138 }
139 }
140
141 /**
142 * @brief Drive an operator parameter from a source
143 * @param parameter Name passed to set_parameter
144 * @param source Evaluated once per block, at begin_block()
145 *
146 * A parameter has at most one mapping. Mapping it again replaces the
147 * source. Must not run concurrently with begin_block().
148 */
149 void map(std::string parameter, Source source)
150 {
151 auto it = std::ranges::find(m_bindings, parameter, &Binding::parameter);
152 if (it != m_bindings.end()) {
153 it->source = std::move(source);
154 } else {
155 m_bindings.push_back({ .parameter = std::move(parameter), .source = std::move(source) });
156 }
157 }
158
159 /**
160 * @brief Remove the mapping of a parameter. No-op when absent.
161 * @note Must not run concurrently with begin_block().
162 */
163 void unmap(std::string_view parameter)
164 {
165 std::erase_if(m_bindings, [&](const Binding& b) { return b.parameter == parameter; });
166 }
167
168 [[nodiscard]] virtual bool publishes_output() const { return false; }
169 [[nodiscard]] virtual size_t get_output_count() const { return 0; }
170 [[nodiscard]] virtual std::span<const double> get_output_block(size_t /*index*/) const { return {}; }
171
172protected:
173 [[nodiscard]] std::span<RelationSlot> slots() const noexcept
174 {
175 return m_slots ? std::span<RelationSlot>(*m_slots) : std::span<RelationSlot> {};
176 }
177
178private:
179 struct Binding {
180 std::string parameter;
182 };
183
184 std::vector<Binding> m_bindings;
185 std::vector<RelationSlot>* m_slots { nullptr };
186};
187
188/**
189 * @class StepOperator
190 * @brief General operator whose body is a callable, run once per block
191 *
192 * Mapped parameters are stored by name and readable from the body through
193 * parameter().
194 *
195 * @code
196 * net->add<StepOperator>([](const std::shared_ptr<StepOperator>& self, std::span<RelationSlot> slots, uint32_t frames) {
197 * for (auto& slot : slots) {
198 * if (slot.level > self->parameter("limit")) {
199 * slot.set_active(false);
200 * }
201 * }
202 * });
203 * @endcode
204 */
205class MAYAFLUX_API StepOperator final : public RelationOperator, public std::enable_shared_from_this<StepOperator> {
206public:
207 using Body = std::function<void(const std::shared_ptr<StepOperator>& self, std::span<RelationSlot> slots, uint32_t frames)>;
208
209 explicit StepOperator(Body body)
210 : m_body(std::move(body))
211 {
212 }
213
214 void process(float frames) override;
215 void set_parameter(std::string_view param, double value) override;
216 [[nodiscard]] std::optional<double> query_state(std::string_view query) const override;
217 [[nodiscard]] std::string_view get_type_name() const override { return "Step"; }
218
219 /**
220 * @brief Value of a mapped or set parameter
221 * @return The value, or 0.0 when the parameter was never set
222 */
223 [[nodiscard]] double parameter(std::string_view name) const;
224
225private:
227 std::map<std::string, double, std::less<>> m_parameters;
228};
229
230/**
231 * @class AdvanceOperator
232 * @brief Produces every slot's signal for the block
233 *
234 * For each slot with a node, advances it for the whole block through
235 * extract_node_samples(). A slot without a node keeps its stored
236 * level for every frame.
237 *
238 * @note Muting (slot.active == false) zeroes what gets published to the
239 * slot's block, but never its level: level always reflects the slot's
240 * real current value, muted or not. Gating a slot from its own level
241 * (an envelope follower, a threshold trigger) works as expected
242 * because of this.
243 *
244 * A node's hooks (on_impulse, on_phase_wrap, on_tick, ...) fire during this
245 * advance, since set_node() enables them.
246 *
247 * Activity is read once per slot per block, at the start of process(). Place
248 * AdvanceOperator after operators whose slot writes should apply this block,
249 * and before DeriveOperator and CombineOperator, which read rendered blocks.
250 */
251class MAYAFLUX_API AdvanceOperator final : public RelationOperator {
252public:
253 void process(float frames) override;
254
255 /** @note AdvanceOperator has no parameters. */
256 void set_parameter(std::string_view /*param*/, double /*value*/) override { }
257 [[nodiscard]] std::optional<double> query_state(std::string_view /*query*/) const override { return std::nullopt; }
258 [[nodiscard]] std::string_view get_type_name() const override { return "Advance"; }
259
260private:
261 std::vector<double> m_scratch;
262 size_t m_scratch_pos {};
263};
264
265/**
266 * @class DeriveOperator
267 * @brief Defines one slot's block as a function of other slots' blocks
268 *
269 * For each frame, calls the function with the input slots' values in the order
270 * given and writes the result to the target slot's block. An input equal to
271 * the target reads that slot's previous frame, or its previous block's last
272 * value at frame 0. Place after AdvanceOperator.
273 *
274 * Three forms of rule:
275 * - a plain function of the input values;
276 * - a function of the input values and a coefficient array, which keeps the
277 * rule's state and can be retuned, saved and shared between rules;
278 * - a boolean function of the input values and a coefficient array, such as
279 * the parallel kernels of Kinesis::Discrete (Boolean.hpp). The block holds
280 * 0 or 1, and the target's activity follows the result, so its on_enter and
281 * on_leave callbacks fire on each change.
282 *
283 * Coefficients are parameters "c0", "c1", ...: map("c2", source) retunes
284 * coefficient 2 at the start of every block.
285 *
286 * @code
287 * namespace D = Kinesis::Discrete;
288 * n->add<DeriveOperator>(2, std::vector<size_t> { 0, 1 }, D::comparator, D::comparator_state(0.2));
289 * n->get_slot(2).on_enter.push_back([bell] { bell->excite(0.6); });
290 * @endcode
291 */
292class MAYAFLUX_API DeriveOperator final : public RelationOperator {
293public:
294 using Function = std::function<double(std::span<const double>)>;
295 using CoefFunction = std::function<double(std::span<const double>, std::span<double>)>;
296
297 /**
298 * @param target Index of the slot written
299 * @param inputs Indices of the slots read
300 * @param function Receives the input values of one frame
301 */
302 DeriveOperator(size_t target, std::vector<size_t> inputs, Function function);
303
304 /**
305 * @param target Index of the slot written
306 * @param inputs Indices of the slots read
307 * @param function Receives the input values of one frame and the coefficients
308 * @param coefficients Starting coefficients, owned by this operator
309 */
310 DeriveOperator(size_t target, std::vector<size_t> inputs, CoefFunction function, std::vector<double> coefficients);
311
312 /**
313 * @param target Index of the slot written
314 * @param inputs Indices of the slots read
315 * @param function Callable returning exactly bool, taking the input values
316 * of one frame as a const std::vector<double>& and the
317 * coefficients as a std::span<double>
318 * @param coefficients Starting coefficients, owned by this operator
319 */
320 template <typename Gate>
321 requires(std::invocable<Gate&, const std::vector<double>&, std::span<double>>
322 && std::same_as<std::invoke_result_t<Gate&, const std::vector<double>&, std::span<double>>, bool>)
323 DeriveOperator(size_t target, std::vector<size_t> inputs, Gate function, std::vector<double> coefficients)
324 : m_target(target)
325 , m_inputs(std::move(inputs))
326 , m_values(m_inputs.size(), 0.0)
327 , m_coefficients(std::move(coefficients))
328 , m_kernel([gate = std::move(function)](const std::vector<double>& values, std::span<double> coefs) {
329 return gate(values, coefs) ? 1.0 : 0.0;
330 })
331 , m_gate(true)
332 {
333 }
334
335 void process(float frames) override;
336
337 /**
338 * @brief Set one coefficient
339 * @param param "c" followed by the coefficient's index, for example "c2".
340 * Anything else, or an index past the array, is ignored.
341 */
342 void set_parameter(std::string_view param, double value) override;
343
344 /**
345 * @return The coefficient named as in set_parameter(), or nullopt
346 */
347 [[nodiscard]] std::optional<double> query_state(std::string_view query) const override;
348
349 [[nodiscard]] std::string_view get_type_name() const override { return "Derive"; }
350
351 /**
352 * @brief Replace the coefficient array
353 * @note Must not run concurrently with process(); to retune during a
354 * block use map() or set_parameter() instead.
355 */
356 void set_coefficients(std::vector<double> coefficients) { m_coefficients = std::move(coefficients); }
357
358 [[nodiscard]] const std::vector<double>& get_coefficients() const noexcept { return m_coefficients; }
359
360private:
361 using Kernel = std::function<double(const std::vector<double>&, std::span<double>)>;
362
363 [[nodiscard]] static std::optional<size_t> coefficient_index(std::string_view param);
364
365 size_t m_target;
366 std::vector<size_t> m_inputs;
367 std::vector<double> m_values;
368 std::vector<double> m_coefficients;
370 bool m_gate {};
371};
372
373/**
374 * @class CombineOperator
375 * @brief Turns slot blocks into published outputs
376 *
377 * Holds a slots-by-outputs table of weights, shareable through weights(), and
378 * one block per output. Each
379 * output is the weighted sum of slot values in that frame, or, when a combine
380 * function is set on it, the function's result over all slot values of the
381 * frame. Place after AdvanceOperator and DeriveOperator.
382 *
383 * Construction sets weight 1.0 from every slot to output 0 and zero
384 * elsewhere. Weight cells may be written from any thread.
385 *
386 * @note weights() keeps the row count given at construction; it does not
387 * grow when the network's slot count does. A slot beyond that row
388 * count is simply left out of every output until weights()->assign()
389 * is called to match, which re-seeds every cell.
390 */
391class MAYAFLUX_API CombineOperator final : public RelationOperator {
392public:
393 /**
394 * @class Output
395 * @brief One published signal
396 */
397 class Output {
398 public:
399 using Function = std::function<double(std::span<const double>)>;
400
401 /**
402 * @brief Replace the weighted sum with a function of all slot values
403 * @param function Called once per frame with the frame's slot values
404 */
405 void combine(Function function)
406 {
407 m_function = std::move(function);
408 }
409
410 /** @brief Return to the weighted sum */
412 {
413 m_function = nullptr;
414 }
415
416 [[nodiscard]] bool has_combine() const noexcept { return static_cast<bool>(m_function); }
417 [[nodiscard]] std::span<const double> block() const noexcept { return m_block; }
418
419 private:
420 friend class CombineOperator;
421
423 std::vector<double> m_block;
424 };
425
426 /**
427 * @param slot_count Number of slots of the host network
428 * @param output_count Number of outputs published
429 */
430 explicit CombineOperator(size_t slot_count, size_t output_count = 1);
431
432 /**
433 * @brief Combine through a table shared with other operators
434 * @param table Slot-to-output weights, one row per slot. Not re-seeded.
435 * One output is published per column.
436 * @throws std::invalid_argument when table is null
437 */
438 explicit CombineOperator(std::shared_ptr<Kinesis::Stochastic::Weights> table);
439
440 void process(float frames) override;
441
442 /** @note CombineOperator has no parameters. */
443 void set_parameter(std::string_view /*param*/, double /*value*/) override { }
444 [[nodiscard]] std::optional<double> query_state(std::string_view /*query*/) const override { return std::nullopt; }
445 [[nodiscard]] std::string_view get_type_name() const override { return "Combine"; }
446
447 [[nodiscard]] bool publishes_output() const override { return true; }
448 [[nodiscard]] size_t get_output_count() const override { return m_outputs.size(); }
449 [[nodiscard]] std::span<const double> get_output_block(size_t index) const override;
450
451 /**
452 * @brief Slot-to-output weights, slot_count rows by output_count columns
453 */
454 [[nodiscard]] const std::shared_ptr<Kinesis::Stochastic::Weights>& weights() const noexcept { return m_weights; }
455
456 /**
457 * @brief One weight cell, readable and assignable from any thread
458 * @param slot Row, the slot index
459 * @param output Column, the output index
460 * @throws std::out_of_range when slot or output lies outside the table.
461 * The row count is fixed at construction, so a slot added to the
462 * network afterwards has no row until weights()->assign().
463 * @note The returned cell must not be kept across weights()->assign().
464 */
465 [[nodiscard]] Kinesis::Stochastic::Weights::Cell weight(size_t slot, size_t output);
466
467 [[nodiscard]] Output& output(size_t index) { return m_outputs[index]; }
468 [[nodiscard]] const Output& output(size_t index) const { return m_outputs[index]; }
469
470private:
471 std::shared_ptr<Kinesis::Stochastic::Weights> m_weights;
472 std::vector<Output> m_outputs;
473 std::vector<double> m_values;
474};
475
476} // namespace MayaFlux::Nodes::Network
Core::GlobalNetworkConfig network
Definition Config.cpp:43
double weight
size_t b
glm::vec3 value
std::vector< std::byte > values
Definition VDBWriter.cpp:26
std::string name
Definition VKDevice.cpp:123
uint32_t index
Definition VKDevice.cpp:122
Proxy for one cell with atomic load and store.
Definition Weights.hpp:44
void set_parameter(std::string_view, double) override
std::optional< double > query_state(std::string_view) const override
Query operator internal state.
std::string_view get_type_name() const override
Type name for introspection.
Produces every slot's signal for the block.
std::function< double(std::span< const double >)> Function
void combine(Function function)
Replace the weighted sum with a function of all slot values.
std::span< const double > block() const noexcept
std::optional< double > query_state(std::string_view) const override
Query operator internal state.
std::shared_ptr< Kinesis::Stochastic::Weights > m_weights
const std::shared_ptr< Kinesis::Stochastic::Weights > & weights() const noexcept
Slot-to-output weights, slot_count rows by output_count columns.
void set_parameter(std::string_view, double) override
std::string_view get_type_name() const override
Type name for introspection.
const Output & output(size_t index) const
Turns slot blocks into published outputs.
void set_coefficients(std::vector< double > coefficients)
Replace the coefficient array.
std::function< double(std::span< const double >, std::span< double >)> CoefFunction
std::function< double(std::span< const double >)> Function
std::string_view get_type_name() const override
Type name for introspection.
DeriveOperator(size_t target, std::vector< size_t > inputs, Gate function, std::vector< double > coefficients)
const std::vector< double > & get_coefficients() const noexcept
std::function< double(const std::vector< double > &, std::span< double >)> Kernel
Defines one slot's block as a function of other slots' blocks.
Domain-agnostic interpretive lens for network processing.
Abstract base class for structured collections of nodes with defined relationships.
void begin_block()
Apply mapped parameters at the start of a block.
void unmap(std::string_view parameter)
Remove the mapping of a parameter.
virtual std::span< const double > get_output_block(size_t) const
std::span< RelationSlot > slots() const noexcept
void set_slots(std::vector< RelationSlot > &slots) noexcept
Supply the current slot storage.
void map(std::string parameter, Source source)
Drive an operator parameter from a source.
Operator that acts on a network's slots over one block at a time.
std::function< double()> Function
Source(std::shared_ptr< NetworkType > network, size_t index)
One indexed output of a NodeNetwork.
std::variant< double, std::shared_ptr< Node >, NetworkIndex, Function > m_value
double evaluate() const
Current value.
Source(std::shared_ptr< NodeType > node)
Anything that can supply a number when evaluated.
std::string_view get_type_name() const override
Type name for introspection.
std::function< void(const std::shared_ptr< StepOperator > &self, std::span< RelationSlot > slots, uint32_t frames)> Body
std::map< std::string, double, std::less<> > m_parameters
General operator whose body is a callable, run once per block.
Base interface for all computational processing nodes.
Definition Node.hpp:126