v2.0.0
Loading...
Searching...
No Matches
mna_graph_executor.cpp
Go to the documentation of this file.
1//=============================================================================================================
34
35//=============================================================================================================
36// INCLUDES
37//=============================================================================================================
38
39#include "mna_graph_executor.h"
40#include "mna_graph.h"
41#include "mna_op_registry.h"
42
43#include <QCryptographicHash>
44#include <QDir>
45#include <QElapsedTimer>
46#include <QSysInfo>
47#include <QTemporaryFile>
48
49#include <algorithm>
50#ifndef WASMBUILD
51#include <QProcess>
52#endif
53
54//=============================================================================================================
55// USED NAMESPACES
56//=============================================================================================================
57
58using namespace MNALIB;
59
60//=============================================================================================================
61// STATIC INITIALIZATION
62//=============================================================================================================
63
64MnaGraphExecutor::ProgressCallback MnaGraphExecutor::s_progressCallback;
65
66//=============================================================================================================
67// DEFINE MEMBER METHODS
68//=============================================================================================================
69
71 const QVariantMap& graphInputs)
72{
73 Context ctx;
74 ctx.graphInputs = graphInputs;
75
76 // Populate context with graph-level inputs keyed as "graph::portName"
77 for (auto it = graphInputs.constBegin(); it != graphInputs.constEnd(); ++it) {
78 ctx.results.insert(QStringLiteral("graph::") + it.key(), it.value());
79 }
80
81 // Evaluate parameter tree bindings before execution
82 graph.paramTree.evaluate(ctx.results);
83 applyParamTree(graph);
84
85 const QStringList order = graph.topologicalSort();
86 const int total = order.size();
87
88 for (int i = 0; i < total; ++i) {
89 const QString& nodeId = order[i];
90
91 if (s_progressCallback) {
92 s_progressCallback(nodeId, i + 1, total);
93 }
94
95 if (!runNode(graph, graph.node(nodeId), ctx)) {
96 break;
97 }
98 }
99
100 // Re-evaluate parameter tree after execution (for on_change bindings)
101 graph.paramTree.evaluate(ctx.results);
102
103 return ctx;
104}
105
106//=============================================================================================================
107
109 Context& existing)
110{
111 // Find dirty nodes and all their downstream dependents
112 QStringList dirty = graph.dirtyNodes();
113 QSet<QString> toExecute;
114 for (const QString& nodeId : dirty) {
115 toExecute.insert(nodeId);
116 const QStringList downstream = graph.downstreamNodes(nodeId);
117 for (const QString& d : downstream) {
118 toExecute.insert(d);
119 }
120 }
121
122 applyParamTree(graph);
123
124 // Get topological order, filter to only those that need execution
125 const QStringList fullOrder = graph.topologicalSort();
126 QStringList order;
127 for (const QString& nodeId : fullOrder) {
128 if (toExecute.contains(nodeId)) {
129 order.append(nodeId);
130 }
131 }
132
133 const int total = order.size();
134
135 for (int i = 0; i < total; ++i) {
136 const QString& nodeId = order[i];
137
138 if (s_progressCallback) {
139 s_progressCallback(nodeId, i + 1, total);
140 }
141
142 if (!runNode(graph, graph.node(nodeId), existing)) {
143 break;
144 }
145 }
146
147 graph.paramTree.evaluate(existing.results);
148
149 return existing;
150}
151
152//=============================================================================================================
153
154void MnaGraphExecutor::applyParamTree(MnaGraph& graph)
155{
156 for (const QString& path : graph.paramTree.allPaths()) {
157 // Path format: "nodeId/attrKey"
158 const int sep = path.indexOf(QLatin1Char('/'));
159 if (sep > 0 && graph.hasNode(path.left(sep))) {
160 graph.node(path.left(sep)).attributes.insert(path.mid(sep + 1), graph.paramTree.param(path));
161 }
162 }
163}
164
165//=============================================================================================================
166
167bool MnaGraphExecutor::runNode(MnaGraph& graph, MnaNode& node, Context& ctx)
168{
169 QVariantMap inputs;
170 for (const MnaPort& p : node.inputs) {
171 if (!p.sourceNodeId.isEmpty()) {
172 inputs.insert(p.name, ctx.results.value(p.sourceNodeId + QStringLiteral("::") + p.sourcePortName));
173 }
174 }
175 // Checks see the node's attributes and inputs by name, post checks its outputs too.
176 QVariantMap scope = node.attributes;
177 scope.insert(inputs);
178
179 MnaVerification& verification = node.verification;
180 MnaProvenance& provenance = verification.provenance;
181 provenance = MnaProvenance();
182 provenance.mneCppVersion = QStringLiteral(MNE_CPP_VERSION);
183 provenance.qtVersion = QString::fromLatin1(qVersion());
184 provenance.osInfo = QSysInfo::prettyProductName() + QLatin1Char(' ') + QSysInfo::currentCpuArchitecture();
185 provenance.hostName = QSysInfo::machineHostName();
186 provenance.resolvedAttributes = node.attributes;
187 for (auto it = inputs.constBegin(); it != inputs.constEnd(); ++it) {
188 provenance.inputHashes.insert(it.key(), QString::fromLatin1(QCryptographicHash::hash(it.value().toString().toUtf8(), QCryptographicHash::Sha256).toHex()));
189 }
190
191 const auto failsHard = [](const QList<MnaVerificationResult>& results) {
192 return std::any_of(results.cbegin(), results.cend(), [](const MnaVerificationResult& r) {
193 return !r.passed && r.severity == QLatin1String("error");
194 });
195 };
196 verification.preResults = runChecks(graph, node, QStringLiteral("pre"), scope);
197 verification.postResults.clear();
198 if (failsHard(verification.preResults)) {
199 ctx.abortedNode = node.id;
200 return false;
201 }
202
203 provenance.startedAt = QDateTime::currentDateTimeUtc();
204 QElapsedTimer timer;
205 timer.start();
206 const QVariantMap outputs = executeNode(node, inputs);
207 provenance.wallTimeMs = timer.elapsed();
208 provenance.finishedAt = QDateTime::currentDateTimeUtc();
209
210 for (auto it = outputs.constBegin(); it != outputs.constEnd(); ++it) {
211 ctx.results.insert(node.id + QStringLiteral("::") + it.key(), it.value());
212 }
213 node.dirty = false;
214 node.executedAt = provenance.finishedAt;
215
216 scope.insert(outputs);
217 verification.postResults = runChecks(graph, node, QStringLiteral("post"), scope);
218 if (failsHard(verification.postResults)) {
219 ctx.abortedNode = node.id;
220 return false;
221 }
222 return true;
223}
224
225//=============================================================================================================
226
227QList<MnaVerificationResult> MnaGraphExecutor::runChecks(const MnaGraph& graph, const MnaNode& node, const QString& phase, const QVariantMap& scope)
228{
229 QList<MnaVerificationResult> results;
230 for (const MnaVerificationCheck& check : node.verification.checks) {
231 if (check.phase != phase) {
232 continue;
233 }
234 MnaVerificationResult result;
235 result.checkId = check.id;
236 result.severity = check.severity;
237 result.evaluatedAt = QDateTime::currentDateTimeUtc();
238 if (!check.script.code.isEmpty()) {
239 MnaNode scriptNode = node;
241 scriptNode.script = check.script;
242 const QVariantMap out = executeNode(scriptNode, scope);
243 result.actualValue = out.value(QStringLiteral("exit_code"));
244 result.passed = result.actualValue.toInt() == 0;
245 } else {
246 result.actualValue = graph.paramTree.evaluateExpression(check.expression, scope);
247 result.passed = result.actualValue.userType() == QMetaType::Bool && result.actualValue.toBool();
248 }
249 result.message = result.passed ? QStringLiteral("PASS: %1").arg(check.description)
250 : QStringLiteral("FAIL [%1]: %2").arg(check.severity, check.description);
251 if (!result.passed && !check.onFail.isEmpty()) {
252 result.message += QStringLiteral(" (%1)").arg(check.onFail);
253 }
254 results.append(result);
255 }
256 return results;
257}
258
259//=============================================================================================================
260
262 const QVariantMap& inputs)
263{
264 // Script execution — inline code via interpreter
265 if (node.execMode == MnaNodeExecMode::Script) {
266#ifdef WASMBUILD
267 QVariantMap outputs;
268 outputs.insert(QStringLiteral("stderr"), QStringLiteral("Script execution not supported in WebAssembly build (QProcess unavailable)"));
269 outputs.insert(QStringLiteral("exit_code"), -1);
270 return outputs;
271#else
272 const MnaScript& script = node.script;
273
274 // Determine file extension from language
275 QString ext = QStringLiteral(".txt");
276 if (script.language == QLatin1String("python"))
277 ext = QStringLiteral(".py");
278 else if (script.language == QLatin1String("shell"))
279 ext = QStringLiteral(".sh");
280 else if (script.language == QLatin1String("r"))
281 ext = QStringLiteral(".R");
282 else if (script.language == QLatin1String("matlab"))
283 ext = QStringLiteral(".m");
284 else if (script.language == QLatin1String("octave"))
285 ext = QStringLiteral(".m");
286 else if (script.language == QLatin1String("julia"))
287 ext = QStringLiteral(".jl");
288
289 // Substitute {{placeholder}} tokens in the code
290 QString code = script.code;
291 for (auto it = inputs.constBegin(); it != inputs.constEnd(); ++it) {
292 code.replace(QStringLiteral("{{") + it.key() + QStringLiteral("}}"),
293 it.value().toString());
294 }
295 for (auto it = node.attributes.constBegin(); it != node.attributes.constEnd(); ++it) {
296 code.replace(QStringLiteral("{{") + it.key() + QStringLiteral("}}"),
297 it.value().toString());
298 }
299
300 // Write code to temporary file
301 QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/mna_script_XXXXXX") + ext);
302 tempFile.setAutoRemove(!script.keepTempFile);
303 if (!tempFile.open()) {
304 QVariantMap outputs;
305 outputs.insert(QStringLiteral("stderr"), QStringLiteral("Failed to create temporary script file"));
306 outputs.insert(QStringLiteral("exit_code"), -1);
307 return outputs;
308 }
309 tempFile.write(code.toUtf8());
310 tempFile.close();
311
312 // Determine interpreter
313 QString interpreter = script.interpreter;
314 if (interpreter.isEmpty()) {
315 if (script.language == QLatin1String("python"))
316 interpreter = QStringLiteral("python3");
317 else if (script.language == QLatin1String("shell"))
318 interpreter = QStringLiteral("/bin/bash");
319 else if (script.language == QLatin1String("r"))
320 interpreter = QStringLiteral("Rscript");
321 else if (script.language == QLatin1String("matlab"))
322 interpreter = QStringLiteral("matlab");
323 else if (script.language == QLatin1String("octave"))
324 interpreter = QStringLiteral("octave");
325 else if (script.language == QLatin1String("julia"))
326 interpreter = QStringLiteral("julia");
327 }
328
329 QStringList args = script.interpreterArgs;
330 args.append(tempFile.fileName());
331
332 QProcess process;
333 process.start(interpreter, args);
334 process.waitForFinished(-1);
335
336 QVariantMap outputs;
337 outputs.insert(QStringLiteral("stdout"), QString::fromUtf8(process.readAllStandardOutput()));
338 outputs.insert(QStringLiteral("stderr"), QString::fromUtf8(process.readAllStandardError()));
339 outputs.insert(QStringLiteral("exit_code"), process.exitCode());
340
341 return outputs;
342#endif
343 }
344
345 // IPC execution
346 if (node.execMode == MnaNodeExecMode::Ipc) {
347#ifdef WASMBUILD
348 QVariantMap outputs;
349 outputs.insert(QStringLiteral("stderr"), QStringLiteral("IPC execution not supported in WebAssembly build (QProcess unavailable)"));
350 outputs.insert(QStringLiteral("exit_code"), -1);
351 return outputs;
352#else
353 QProcess process;
354 if (!node.ipcWorkDir.isEmpty()) {
355 process.setWorkingDirectory(node.ipcWorkDir);
356 }
357
358 // Substitute {{placeholder}} tokens in arguments
359 QStringList resolvedArgs;
360 for (const QString& arg : node.ipcArgs) {
361 QString resolved = arg;
362 for (auto it = inputs.constBegin(); it != inputs.constEnd(); ++it) {
363 resolved.replace(QStringLiteral("{{") + it.key() + QStringLiteral("}}"),
364 it.value().toString());
365 }
366 // Also substitute from attributes
367 for (auto it = node.attributes.constBegin(); it != node.attributes.constEnd(); ++it) {
368 resolved.replace(QStringLiteral("{{") + it.key() + QStringLiteral("}}"),
369 it.value().toString());
370 }
371 resolvedArgs.append(resolved);
372 }
373
374 process.start(node.ipcCommand, resolvedArgs);
375 process.waitForFinished(-1);
376
377 QVariantMap outputs;
378 outputs.insert(QStringLiteral("stdout"), QString::fromUtf8(process.readAllStandardOutput()));
379 outputs.insert(QStringLiteral("stderr"), QString::fromUtf8(process.readAllStandardError()));
380 outputs.insert(QStringLiteral("exit_code"), process.exitCode());
381
382 // Populate outputs from cached results if specified
383 for (const MnaPort& p : node.outputs) {
384 if (!p.cachedResultPath.isEmpty()) {
385 outputs.insert(p.name, p.cachedResultPath);
386 }
387 }
388
389 return outputs;
390#endif
391 }
392
393 // Look up registered op function
394 const MnaOpRegistry& registry = MnaOpRegistry::instance();
395 MnaOpRegistry::OpFunc func = registry.opFunc(node.opType);
396
397 if (func) {
398 return func(inputs, node.attributes);
399 }
400
401 // No implementation registered — return empty
402 return {};
403}
404
405//=============================================================================================================
406
408{
409 s_progressCallback = cb;
410}
411
412//=============================================================================================================
413// Stream-mode execution
414//=============================================================================================================
415
417 PluginFactory factory)
418{
419 StreamContext ctx;
420 ctx.graph = &graph;
421
422 // 1. Validate the graph
423 QStringList errors;
424 if (!graph.validate(&errors)) {
425 qWarning() << "MnaGraphExecutor::startStream - graph validation failed:" << errors;
426 return ctx;
427 }
428
429 // 2. Topological sort
430 ctx.executionOrder = graph.topologicalSort();
431
432 // 3. Apply current parameter tree values to node attributes
433 applyParamTree(graph);
434
435 // 4. Instantiate live plugins via factory
436 for (const QString& nodeId : ctx.executionOrder) {
437 const MnaNode& n = graph.node(nodeId);
438 QObject* plugin = factory(n.opType);
439 if (!plugin) {
440 qWarning() << "MnaGraphExecutor::startStream - factory returned nullptr for opType:" << n.opType;
441 // Clean up already-created plugins
442 for (QObject* p : ctx.livePlugins) {
443 delete p;
444 }
445 ctx.livePlugins.clear();
446 return ctx;
447 }
448 ctx.livePlugins.insert(nodeId, plugin);
449 }
450
451 // 5. Wiring: the host application is responsible for connecting
452 // Qt signals/slots between the QObject* instances based on
453 // the port connections encoded in each node's input ports.
454 // The mna library provides the graph topology; the host app
455 // knows the concrete signal/slot signatures.
456
457 ctx.running = true;
458 return ctx;
459}
460
461//=============================================================================================================
462
464{
465 if (!ctx.running) {
466 return;
467 }
468
469 ctx.running = false;
470
471 // Stop in reverse topological order
472 for (int i = ctx.executionOrder.size() - 1; i >= 0; --i) {
473 const QString& nodeId = ctx.executionOrder[i];
474 QObject* plugin = ctx.livePlugins.value(nodeId);
475 delete plugin;
476 }
477
478 ctx.livePlugins.clear();
479 ctx.executionOrder.clear();
480 ctx.graph = nullptr;
481}
In-memory directed acyclic graph of MNALIB::MnaNode operations — connectivity, validation,...
Stateless executor that runs an MNALIB::MnaGraph in topological order, in batch or stream mode,...
Process-wide singleton catalog mapping opType strings to their MNALIB::MnaOpSchema and (for built-in ...
MNE Analysis Container Format (mna/mnx).
@ Ipc
Delegates to an external process via inter-process communication.
Definition mna_types.h:131
@ Script
Inline code executed via interpreter (Python, shell, R, …).
Definition mna_types.h:132
In-memory DAG of MnaNode operations with validation, topological sort and JSON/CBOR persistence.
Definition mna_graph.h:68
MnaNode & node(const QString &nodeId)
Definition mna_graph.cpp:79
bool validate(QStringList *errors=nullptr) const
QStringList dirtyNodes() const
bool hasNode(const QString &nodeId) const
QStringList downstreamNodes(const QString &nodeId) const
MnaParamTree paramTree
Hierarchical parameter store with formula-driven bindings.
Definition mna_graph.h:95
QStringList topologicalSort() const
static void setProgressCallback(ProgressCallback cb)
static Context executeIncremental(MnaGraph &graph, Context &existing)
static StreamContext startStream(MnaGraph &graph, PluginFactory factory)
static QVariantMap executeNode(const MnaNode &node, const QVariantMap &inputs)
std::function< void(const QString &nodeId, int current, int total)> ProgressCallback
Progress callback type.
std::function< QObject *(const QString &opType)> PluginFactory
static void stopStream(StreamContext &ctx)
static Context execute(MnaGraph &graph, const QVariantMap &graphInputs)
QMap< QString, QVariant > results
nodeId::portName → data (QVariant wrapping domain objects or file paths)
QVariantMap graphInputs
Graph-level inputs (populated before execution).
QStringList executionOrder
Topological order used for startup/shutdown.
bool running
Whether the stream is active.
MnaGraph * graph
The pipeline graph (owned externally).
QMap< QString, QObject * > livePlugins
nodeId → live plugin instance (QObject* avoids scan dependency)
Single executable step in an MNA pipeline graph, with attributes, typed ports, exec mode,...
Definition mna_node.h:77
MnaNodeExecMode execMode
Execution mode.
Definition mna_node.h:85
QString ipcCommand
External executable command.
Definition mna_node.h:88
QStringList ipcArgs
Command-line arguments (supports {{placeholder}} tokens).
Definition mna_node.h:89
bool dirty
Whether node needs re-execution.
Definition mna_node.h:102
MnaScript script
Inline source code, interpreter, language.
Definition mna_node.h:94
QList< MnaPort > inputs
Input ports.
Definition mna_node.h:82
QVariantMap attributes
Operation parameters.
Definition mna_node.h:80
QDateTime executedAt
Timestamp of last execution.
Definition mna_node.h:101
QString opType
Operation type (looked up in MnaOpRegistry).
Definition mna_node.h:79
MnaVerification verification
Explanation, checks, results, and provenance snapshot.
Definition mna_node.h:97
QString ipcWorkDir
Working directory for external process.
Definition mna_node.h:90
QList< MnaPort > outputs
Output ports.
Definition mna_node.h:83
QString id
Unique node identifier.
Definition mna_node.h:78
Process-wide lookup from opType to MnaOpSchema and implementation function.
static MnaOpRegistry & instance()
OpFunc opFunc(const QString &opType) const
std::function< QVariantMap(const QVariantMap &inputs, const QVariantMap &attributes)> OpFunc
Operation implementation callback type.
QVariant evaluateExpression(const QString &expr, const QMap< QString, QVariant > &results) const
QStringList evaluate(const QMap< QString, QVariant > &results)
QStringList allPaths() const
QVariant param(const QString &path) const
Named, typed port on an MNA graph node with upstream link and optional real-time stream binding.
Definition mna_port.h:65
QString name
Port name (unique within a node).
Definition mna_port.h:66
QString sourcePortName
Which output port on that node?
Definition mna_port.h:72
QString cachedResultPath
Relative path to cached result.
Definition mna_port.h:88
QString sourceNodeId
Which node produces this input? (empty → graph-level input).
Definition mna_port.h:71
Inline interpreter-launched source code carried by a Script-mode MnaNode.
Definition mna_script.h:66
bool keepTempFile
true → preserve temp script file after execution (debug aid)
Definition mna_script.h:75
QString code
The inline source code (resolved at save-time if sourceUri is set).
Definition mna_script.h:71
QString interpreter
Definition mna_script.h:68
QString language
"python", "shell", "r", "matlab", "octave", "julia"
Definition mna_script.h:67
QStringList interpreterArgs
Extra args before the script file (e.g. ["-u"] for unbuffered Python).
Definition mna_script.h:70
QString severity
"error" (abort), "warning" (log + continue), "info" (always continue)
QString expression
MnaParamTree expression over attributes, inputs and (post) outputs: "rank > 0".
QString phase
"pre" (before execution) or "post" (after execution)
QString description
Human-readable: "Covariance matrix must be positive-definite".
QString onFail
Optional remediation hint.
QString id
Unique check identifier within the node (e.g. "cov_posdef").
QString message
Formatted message: "PASS: ..." or "FAIL [error]: ...".
QString checkId
References MnaVerificationCheck::id.
QVariant actualValue
The evaluated expression result.
QString severity
Echoed from the check definition.
QDateTime evaluatedAt
When this check was evaluated.
bool passed
true if the expression evaluated to true
QString osInfo
e.g. "macOS 15.4 arm64"
QString qtVersion
e.g. "6.11.0"
qint64 wallTimeMs
Wall-clock duration in milliseconds.
QString hostName
Machine name (for cluster provenance).
QMap< QString, QString > inputHashes
portName → SHA-256
QString mneCppVersion
e.g. "2.2.0"
MnaProvenance provenance
Complete provenance snapshot (populated by executor).
QList< MnaVerificationCheck > checks
Declarative checks (authored by user, evaluated by executor).
QList< MnaVerificationResult > preResults
Results of pre-execution checks (populated by executor).
QList< MnaVerificationResult > postResults
Results of post-execution checks (populated by executor).