Solver graph nodes#
A solver graph is a DAG of nodes connected by edges. Nodes read and write
edge data in _impl_evaluate_internal(). Edges carry typed values (fields,
scalars, indexes, …). Most nodes can be found under the shamrock::solvergraph namespace.
Nodes inherit
INode.edges can be wired with
set_edges()evaluation of the node from edges input and in-out is done using
evaluate().
Node layout#
// 1. declare inputs (X_RO) and outputs (X_RW)
#define NODE_EDGES(X_RO, X_RW) \
X_RO(shamrock::solvergraph::IDataEdge<T>, in) \
X_RW(shamrock::solvergraph::IDataEdge<T>, out)
// 2. Create the class and inherit from INode
class MyNode : public shamrock::solvergraph::INode {
public:
// 3. generate set_edges() / get_edges()
EXPAND_NODE_EDGES(NODE_EDGES)
// 4. evaluation logic
void _impl_evaluate_internal() override {
auto edges = get_edges();
// ... do stuff ...
}
// 5. labels for graph display and TeX output
std::string _impl_get_label() const override { return "MyNode"; }
std::string _impl_get_tex() const override { return ""; }
};
#undef NODE_EDGES
EXPAND_NODE_EDGES generates:
struct Edges—const T&for read-only edges,T&for read-write edgesset_edges(shared_ptr...)— wire beforeevaluate()get_edges()— typed access in_impl_evaluate_internal()get_edges_tex_symbols()— returnsstruct EdgesTexSymbols, onestd::stringTeX symbol per edge, named after itreplace_edges_tex_symbols(tex)— replaces every{<edge name>}placeholder intexby the TeX symbol of that edge
TeX output#
In _impl_get_tex(), refer to edges by name rather than by slot index
(get_ro_edge_base(i) / get_rw_edge_base(i)): slot indices silently shift
when an edge is added, removed or reordered in NODE_EDGES, while names stay
in sync with it.
Name each edge placeholder after its edge in NODE_EDGES, and use
replace_all only for values that are not edges (node members, constants):
std::string _impl_get_tex() const override {
std::string tex = R"tex(
{out}_i = f({in}_i), \quad N = {nvar}
)tex";
replace_edges_tex_symbols(tex); // {in}, {out} -> TeX symbols of those edges
shambase::replace_all(tex, "{nvar}", std::to_string(nvar)); // node member
return tex;
}
Edge macros#
Macro |
Role in |
|
|
|---|---|---|---|
|
required read-only input |
|
|
|
required read-write output |
|
|
List inputs first, outputs last.
Optional edges#
An optional input may be absent at wiring time for nodes having multiple evaluation modes.
To use optional edges use EXPAND_NODE_EDGES_OPTIONAL when NODE_EDGES
contains any X_*_OPTIONAL macro.
The following changes for those :
// Now we have to add X_RO_OPTIONAL, X_RW_OPTIONAL too
#define NODE_EDGES(X_RO, X_RW, X_RO_OPTIONAL, X_RW_OPTIONAL) \
X_RO_OPTIONAL(shamrock::solvergraph::IDataEdge<u32>, opt_a) \
X_RO_OPTIONAL(shamrock::solvergraph::IDataEdge<u32>, opt_b) \
X_RW(shamrock::solvergraph::IDataEdge<u32>, out)
class OptionalEdgeProbeNode : public shamrock::solvergraph::INode {
public:
// Use EXPAND_NODE_EDGES_OPTIONAL instead of EXPAND_NODE_EDGES
EXPAND_NODE_EDGES_OPTIONAL(NODE_EDGES)
void _impl_evaluate_internal() override {
auto edges = get_edges();
// optional inputs: edges.opt_a.has_value(), edges.opt_b.has_value()
// ...
}
std::string _impl_get_label() const override { return "OptionalEdgeProbe"; }
std::string _impl_get_tex() const override { return ""; }
};
#undef NODE_EDGES
Optional edge macros#
Macro |
|
|
|---|---|---|
|
|
|
|
|
|
Wiring#
Pass std::nullopt for absent optional inputs:
node.set_edges(std::nullopt, std::nullopt, out); // neither optional present
node.set_edges(a, std::nullopt, out); // only opt_a
node.set_edges(a, b, out); // both present
node.evaluate();
Absent optionals are stored as
INullOptEdge
placeholders so slot indices stay stable. get_ro_edge_optional returns
std::nullopt for those slots.
Composition nodes#
OperationSequence and OperationIf wrap other INodes. Nested nodes are
passed to the constructor (graph structure). Data edges are still wired with
set_edges.
auto then_node = std::make_shared<MyNode>(...); // optional
auto else_node = std::make_shared<MyNode>(...); // optional
auto cond = IDataEdge<bool>::make_shared("do_step", "do_step");
auto if_node = std::make_shared<OperationIf>("do step", then_node, else_node);
if_node->set_edges(cond);
cond->data = true; // evaluate then_node (or skip if none)
if_node->evaluate();
cond->data = false; // evaluate else_node (or skip if none)
if_node->evaluate();
To gate several nodes, wrap them in an OperationSequence and pass that as
then_node / else_node. Else-only is OperationIf("name", {}, else_node).