v2.0.0
Loading...
Searching...
No Matches
mna_graph.cpp
Go to the documentation of this file.
1//=============================================================================================================
29
30//=============================================================================================================
31// INCLUDES
32//=============================================================================================================
33
34#include "mna_graph.h"
35#include "mna_op_registry.h"
36
37#include <QJsonArray>
38#include <QCborArray>
39#include <QSet>
40#include <QQueue>
41
42//=============================================================================================================
43// USED NAMESPACES
44//=============================================================================================================
45
46using namespace MNALIB;
47
48//=============================================================================================================
49// DEFINE MEMBER METHODS
50//=============================================================================================================
51
55
56//=============================================================================================================
57// Node management
58//=============================================================================================================
59
61{
62 m_nodes.append(node);
63}
64
65//=============================================================================================================
66
67void MnaGraph::removeNode(const QString& nodeId)
68{
69 for (int i = 0; i < m_nodes.size(); ++i) {
70 if (m_nodes[i].id == nodeId) {
71 m_nodes.removeAt(i);
72 return;
73 }
74 }
75}
76
77//=============================================================================================================
78
79MnaNode& MnaGraph::node(const QString& nodeId)
80{
81 for (int i = 0; i < m_nodes.size(); ++i) {
82 if (m_nodes[i].id == nodeId) {
83 return m_nodes[i];
84 }
85 }
86 static MnaNode dummy;
87 return dummy;
88}
89
90//=============================================================================================================
91
92const MnaNode& MnaGraph::node(const QString& nodeId) const
93{
94 for (int i = 0; i < m_nodes.size(); ++i) {
95 if (m_nodes[i].id == nodeId) {
96 return m_nodes[i];
97 }
98 }
99 static const MnaNode dummy;
100 return dummy;
101}
102
103//=============================================================================================================
104
105QList<MnaNode>& MnaGraph::nodes()
106{
107 return m_nodes;
108}
109
110//=============================================================================================================
111
112const QList<MnaNode>& MnaGraph::nodes() const
113{
114 return m_nodes;
115}
116
117//=============================================================================================================
118
119bool MnaGraph::hasNode(const QString& nodeId) const
120{
121 for (const MnaNode& n : m_nodes) {
122 if (n.id == nodeId) {
123 return true;
124 }
125 }
126 return false;
127}
128
129//=============================================================================================================
130// Connection helper
131//=============================================================================================================
132
133bool MnaGraph::connect(const QString& srcNodeId, const QString& srcPortName,
134 const QString& dstNodeId, const QString& dstPortName)
135{
136 // Verify source node and port exist
137 if (!hasNode(srcNodeId) || !hasNode(dstNodeId)) {
138 return false;
139 }
140
141 const MnaNode& srcNode = node(srcNodeId);
142 bool srcFound = false;
143 for (const MnaPort& p : srcNode.outputs) {
144 if (p.name == srcPortName) {
145 srcFound = true;
146 break;
147 }
148 }
149 if (!srcFound) {
150 return false;
151 }
152
153 // Find and update the destination input port
154 MnaNode& dstNode = node(dstNodeId);
155 for (int i = 0; i < dstNode.inputs.size(); ++i) {
156 if (dstNode.inputs[i].name == dstPortName) {
157 dstNode.inputs[i].sourceNodeId = srcNodeId;
158 dstNode.inputs[i].sourcePortName = srcPortName;
159 return true;
160 }
161 }
162
163 return false;
164}
165
166//=============================================================================================================
167// Validation
168//=============================================================================================================
169
170bool MnaGraph::validate(QStringList* errors) const
171{
172 bool valid = true;
173 auto addError = [&](const QString& msg) {
174 valid = false;
175 if (errors) {
176 errors->append(msg);
177 }
178 };
179
180 // Build adjacency for cycle detection
181 QMap<QString, QSet<QString>> adj;
182 QMap<QString, int> inDegree;
183 for (const MnaNode& n : m_nodes) {
184 if (!adj.contains(n.id)) {
185 adj[n.id] = {};
186 }
187 if (!inDegree.contains(n.id)) {
188 inDegree[n.id] = 0;
189 }
190 }
191
192 // Populate adjacency from input port connections
193 for (const MnaNode& n : m_nodes) {
194 for (const MnaPort& p : n.inputs) {
195 if (!p.sourceNodeId.isEmpty()) {
196 if (!adj.contains(p.sourceNodeId)) {
197 addError(QStringLiteral("Node '%1' input port '%2' references unknown source node '%3'")
198 .arg(n.id, p.name, p.sourceNodeId));
199 } else {
200 if (!adj[p.sourceNodeId].contains(n.id)) {
201 adj[p.sourceNodeId].insert(n.id);
202 inDegree[n.id]++;
203 }
204 }
205 }
206 }
207 }
208
209 // Check acyclicity via Kahn's algorithm
210 QQueue<QString> queue;
211 for (auto it = inDegree.constBegin(); it != inDegree.constEnd(); ++it) {
212 if (it.value() == 0) {
213 queue.enqueue(it.key());
214 }
215 }
216
217 int visited = 0;
218 while (!queue.isEmpty()) {
219 QString current = queue.dequeue();
220 visited++;
221 for (const QString& neighbor : adj.value(current)) {
222 inDegree[neighbor]--;
223 if (inDegree[neighbor] == 0) {
224 queue.enqueue(neighbor);
225 }
226 }
227 }
228
229 if (visited != m_nodes.size()) {
230 addError(QStringLiteral("Graph contains a cycle"));
231 }
232
233 // Validate each node against its schema
234 const MnaOpRegistry& registry = MnaOpRegistry::instance();
235 for (const MnaNode& n : m_nodes) {
236 if (!registry.hasOp(n.opType)) {
237 addError(QStringLiteral("Node '%1' has unregistered op type '%2'")
238 .arg(n.id, n.opType));
239 continue;
240 }
241
242 MnaOpSchema schema = registry.schema(n.opType);
243 QStringList schemaErrors;
244 if (!schema.validate(n, &schemaErrors)) {
245 for (const QString& e : schemaErrors) {
246 addError(QStringLiteral("Node '%1': %2").arg(n.id, e));
247 }
248 }
249
250 // Check that required input ports are connected
251 for (const MnaOpSchemaPort& sp : schema.inputPorts) {
252 if (!sp.required)
253 continue;
254 for (const MnaPort& np : n.inputs) {
255 if (np.name == sp.name && np.sourceNodeId.isEmpty()) {
256 addError(QStringLiteral("Node '%1': required input port '%2' is not connected")
257 .arg(n.id, sp.name));
258 }
259 }
260 }
261 }
262
263 // Check cross-edge dataKind compatibility
264 for (const MnaNode& n : m_nodes) {
265 for (const MnaPort& inp : n.inputs) {
266 if (inp.sourceNodeId.isEmpty() || inp.sourcePortName.isEmpty())
267 continue;
268 // Find source node and output port
269 for (const MnaNode& srcNode : m_nodes) {
270 if (srcNode.id != inp.sourceNodeId)
271 continue;
272 for (const MnaPort& srcOut : srcNode.outputs) {
273 if (srcOut.name == inp.sourcePortName) {
274 if (inp.dataKind != MnaDataKind::Custom &&
275 srcOut.dataKind != MnaDataKind::Custom &&
276 inp.dataKind != srcOut.dataKind) {
277 addError(QStringLiteral("Edge %1.%2 -> %3.%4: data kind mismatch (%5 != %6)")
278 .arg(srcNode.id, srcOut.name, n.id, inp.name)
279 .arg(static_cast<int>(srcOut.dataKind))
280 .arg(static_cast<int>(inp.dataKind)));
281 }
282 }
283 }
284 }
285 }
286 }
287
288 return valid;
289}
290
291//=============================================================================================================
292// Topological sort
293//=============================================================================================================
294
295QStringList MnaGraph::topologicalSort() const
296{
297 // Kahn's algorithm
298 QMap<QString, QSet<QString>> adj;
299 QMap<QString, int> inDegree;
300
301 for (const MnaNode& n : m_nodes) {
302 adj[n.id] = {};
303 inDegree[n.id] = 0;
304 }
305
306 for (const MnaNode& n : m_nodes) {
307 for (const MnaPort& p : n.inputs) {
308 if (!p.sourceNodeId.isEmpty() && adj.contains(p.sourceNodeId)) {
309 if (!adj[p.sourceNodeId].contains(n.id)) {
310 adj[p.sourceNodeId].insert(n.id);
311 inDegree[n.id]++;
312 }
313 }
314 }
315 }
316
317 QQueue<QString> queue;
318 for (auto it = inDegree.constBegin(); it != inDegree.constEnd(); ++it) {
319 if (it.value() == 0) {
320 queue.enqueue(it.key());
321 }
322 }
323
324 QStringList sorted;
325 while (!queue.isEmpty()) {
326 QString current = queue.dequeue();
327 sorted.append(current);
328 for (const QString& neighbor : adj.value(current)) {
329 inDegree[neighbor]--;
330 if (inDegree[neighbor] == 0) {
331 queue.enqueue(neighbor);
332 }
333 }
334 }
335
336 return sorted;
337}
338
339//=============================================================================================================
340// Dependency queries
341//=============================================================================================================
342
343QStringList MnaGraph::upstreamNodes(const QString& nodeId) const
344{
345 QStringList upstream;
346 if (!hasNode(nodeId)) {
347 return upstream;
348 }
349
350 const MnaNode& n = node(nodeId);
351 QSet<QString> visited;
352 QQueue<QString> queue;
353
354 for (const MnaPort& p : n.inputs) {
355 if (!p.sourceNodeId.isEmpty() && !visited.contains(p.sourceNodeId)) {
356 visited.insert(p.sourceNodeId);
357 queue.enqueue(p.sourceNodeId);
358 }
359 }
360
361 while (!queue.isEmpty()) {
362 QString current = queue.dequeue();
363 upstream.append(current);
364 if (hasNode(current)) {
365 const MnaNode& cn = node(current);
366 for (const MnaPort& p : cn.inputs) {
367 if (!p.sourceNodeId.isEmpty() && !visited.contains(p.sourceNodeId)) {
368 visited.insert(p.sourceNodeId);
369 queue.enqueue(p.sourceNodeId);
370 }
371 }
372 }
373 }
374
375 return upstream;
376}
377
378//=============================================================================================================
379
380QStringList MnaGraph::downstreamNodes(const QString& nodeId) const
381{
382 QStringList downstream;
383 if (!hasNode(nodeId)) {
384 return downstream;
385 }
386
387 // Build forward adjacency
388 QMap<QString, QSet<QString>> adj;
389 for (const MnaNode& n : m_nodes) {
390 adj[n.id] = {};
391 }
392 for (const MnaNode& n : m_nodes) {
393 for (const MnaPort& p : n.inputs) {
394 if (!p.sourceNodeId.isEmpty() && adj.contains(p.sourceNodeId)) {
395 adj[p.sourceNodeId].insert(n.id);
396 }
397 }
398 }
399
400 QSet<QString> visited;
401 QQueue<QString> queue;
402 for (const QString& neighbor : adj.value(nodeId)) {
403 if (!visited.contains(neighbor)) {
404 visited.insert(neighbor);
405 queue.enqueue(neighbor);
406 }
407 }
408
409 while (!queue.isEmpty()) {
410 QString current = queue.dequeue();
411 downstream.append(current);
412 for (const QString& neighbor : adj.value(current)) {
413 if (!visited.contains(neighbor)) {
414 visited.insert(neighbor);
415 queue.enqueue(neighbor);
416 }
417 }
418 }
419
420 return downstream;
421}
422
423//=============================================================================================================
424
425QStringList MnaGraph::dirtyNodes() const
426{
427 QStringList dirty;
428 for (const MnaNode& n : m_nodes) {
429 if (n.dirty) {
430 dirty.append(n.id);
431 }
432 }
433 return dirty;
434}
435
436//=============================================================================================================
437// Serialization
438//=============================================================================================================
439
440QJsonObject MnaGraph::toJson() const
441{
442 QJsonObject json;
443
444 // Nodes
445 QJsonArray nodesArr;
446 for (const MnaNode& n : m_nodes) {
447 nodesArr.append(n.toJson());
448 }
449 json[QStringLiteral("nodes")] = nodesArr;
450
451 // Graph-level inputs
452 if (!graphInputs.isEmpty()) {
453 QJsonArray arr;
454 for (const MnaPort& p : graphInputs) {
455 arr.append(p.toJson());
456 }
457 json[QStringLiteral("graph_inputs")] = arr;
458 }
459
460 // Graph-level outputs
461 if (!graphOutputs.isEmpty()) {
462 QJsonArray arr;
463 for (const MnaPort& p : graphOutputs) {
464 arr.append(p.toJson());
465 }
466 json[QStringLiteral("graph_outputs")] = arr;
467 }
468
469 // Parameter tree
470 QJsonObject ptJson = paramTree.toJson();
471 if (!ptJson.isEmpty()) {
472 json[QStringLiteral("param_tree")] = ptJson;
473 }
474
475 return json;
476}
477
478//=============================================================================================================
479
480MnaGraph MnaGraph::fromJson(const QJsonObject& json)
481{
482 MnaGraph graph;
483
484 const QJsonArray nodesArr = json.value(QStringLiteral("nodes")).toArray();
485 for (const QJsonValue& v : nodesArr) {
486 graph.addNode(MnaNode::fromJson(v.toObject()));
487 }
488
489 const QJsonArray giArr = json.value(QStringLiteral("graph_inputs")).toArray();
490 for (const QJsonValue& v : giArr) {
491 graph.graphInputs.append(MnaPort::fromJson(v.toObject()));
492 }
493
494 const QJsonArray goArr = json.value(QStringLiteral("graph_outputs")).toArray();
495 for (const QJsonValue& v : goArr) {
496 graph.graphOutputs.append(MnaPort::fromJson(v.toObject()));
497 }
498
499 if (json.contains(QStringLiteral("param_tree"))) {
500 graph.paramTree = MnaParamTree::fromJson(json.value(QStringLiteral("param_tree")).toObject());
501 }
502
503 return graph;
504}
505
506//=============================================================================================================
507
508QCborMap MnaGraph::toCbor() const
509{
510 QCborMap cbor;
511
512 QCborArray nodesArr;
513 for (const MnaNode& n : m_nodes) {
514 nodesArr.append(n.toCbor());
515 }
516 cbor.insert(QStringLiteral("nodes"), nodesArr);
517
518 if (!graphInputs.isEmpty()) {
519 QCborArray arr;
520 for (const MnaPort& p : graphInputs) {
521 arr.append(p.toCbor());
522 }
523 cbor.insert(QStringLiteral("graph_inputs"), arr);
524 }
525
526 if (!graphOutputs.isEmpty()) {
527 QCborArray arr;
528 for (const MnaPort& p : graphOutputs) {
529 arr.append(p.toCbor());
530 }
531 cbor.insert(QStringLiteral("graph_outputs"), arr);
532 }
533
534 return cbor;
535}
536
537//=============================================================================================================
538
539MnaGraph MnaGraph::fromCbor(const QCborMap& cbor)
540{
541 MnaGraph graph;
542
543 const QCborArray nodesArr = cbor.value(QStringLiteral("nodes")).toArray();
544 for (const QCborValue& v : nodesArr) {
545 graph.addNode(MnaNode::fromCbor(v.toMap()));
546 }
547
548 const QCborArray giArr = cbor.value(QStringLiteral("graph_inputs")).toArray();
549 for (const QCborValue& v : giArr) {
550 graph.graphInputs.append(MnaPort::fromCbor(v.toMap()));
551 }
552
553 const QCborArray goArr = cbor.value(QStringLiteral("graph_outputs")).toArray();
554 for (const QCborValue& v : goArr) {
555 graph.graphOutputs.append(MnaPort::fromCbor(v.toMap()));
556 }
557
558 return graph;
559}
In-memory directed acyclic graph of MnaNode operations — connectivity, validation,...
Process-wide singleton catalog mapping opType strings to their MnaOpSchema and (for built-in ops) exe...
MNE Analysis Container Format (mna/mnx).
@ Custom
User-defined data kind.
Definition mna_types.h:104
MnaNode & node(const QString &nodeId)
Definition mna_graph.cpp:79
void addNode(const MnaNode &node)
Definition mna_graph.cpp:60
bool validate(QStringList *errors=nullptr) const
QStringList dirtyNodes() const
bool hasNode(const QString &nodeId) const
QStringList downstreamNodes(const QString &nodeId) const
QCborMap toCbor() const
void removeNode(const QString &nodeId)
Definition mna_graph.cpp:67
QList< MnaPort > graphInputs
Named, typed entry points.
Definition mna_graph.h:86
QList< MnaPort > graphOutputs
Named, typed exit points.
Definition mna_graph.h:87
MnaParamTree paramTree
Hierarchical parameter store with formula-driven bindings.
Definition mna_graph.h:93
QList< MnaNode > & nodes()
QJsonObject toJson() const
QStringList topologicalSort() const
static MnaGraph fromJson(const QJsonObject &json)
QStringList upstreamNodes(const QString &nodeId) const
bool connect(const QString &srcNodeId, const QString &srcPortName, const QString &dstNodeId, const QString &dstPortName)
static MnaGraph fromCbor(const QCborMap &cbor)
Single executable step in an MNA pipeline graph, with attributes, typed ports, exec mode,...
Definition mna_node.h:75
QList< MnaPort > inputs
Input ports.
Definition mna_node.h:80
static MnaNode fromJson(const QJsonObject &json)
Definition mna_node.cpp:124
static MnaNode fromCbor(const QCborMap &cbor)
Definition mna_node.cpp:236
QList< MnaPort > outputs
Output ports.
Definition mna_node.h:81
Process-wide lookup from opType to MnaOpSchema and implementation function.
static MnaOpRegistry & instance()
bool hasOp(const QString &opType) const
MnaOpSchema schema(const QString &opType) const
QString name
Port name.
bool required
Must be connected?
Operation schema for graph validation.
QList< MnaOpSchemaPort > inputPorts
Expected input ports.
bool validate(const MnaNode &node, QStringList *errors=nullptr) const
static MnaParamTree fromJson(const QJsonObject &obj)
Named, typed port on an MNA graph node with upstream link and optional real-time stream binding.
Definition mna_port.h:63
QString name
Port name (unique within a node).
Definition mna_port.h:64
QString sourcePortName
Which output port on that node?
Definition mna_port.h:70
MnaDataKind dataKind
Data kind flowing through this port.
Definition mna_port.h:65
static MnaPort fromJson(const QJsonObject &json)
Definition mna_port.cpp:133
QString sourceNodeId
Which node produces this input? (empty → graph-level input).
Definition mna_port.h:69
static MnaPort fromCbor(const QCborMap &cbor)
Definition mna_port.cpp:190