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