Exporting your simulation as standalone code

September 10, 2026

Every page in this series so far has run a model inside Konjugate itself, the desktop app doing the integrating. Export simulation code, a button in the title bar next to Export results as CSV, does the opposite: it turns the currently loaded model into a real, self-contained .cpp or .py file that reproduces one run with no Konjugate dependency at all, not even installed. The file it writes is plain, readable text you can compile yourself and hand to someone whose machine has never had Konjugate on it.

The example reuses "Getting started"'s hot plate and room air, this time with the cooling relationship left at its real rate constant, 0.05, rather than a deliberately wrong guess. The point of this page is fidelity, not fitting.

The export dialog

Clicking the button opens a small dialog with three choices:

The Export simulation code dialog, default state: Standalone program, C++, Serial
Format, Language and Execution: every combination is generated by the same underlying code, not a different exporter per language.

Format chooses between a standalone program (this page) and a real compiled FMU, a large enough feature to cover on its own. Language is C++ or Python. Execution picks how the generated program dispatches its per-step node work, more on that below. Choosing Python narrows Execution automatically:

The same dialog with Python and MPI selected
OpenMP and std::thread quietly become unavailable the moment Language switches to Python. Python's GIL means threads wouldn't actually run this loop in parallel, so the dialog doesn't offer to pretend otherwise. MPI, via mpi4py, still works.

Building and running the export

Export… writes the file through the operating system's own save dialog. Nothing further happens inside Konjugate itself once that file is on disk. The file opens with its own build instructions in a header comment, so nothing here has to be memorized:

$ c++ -std=c++20 -O2 coolingModel.cpp -o coolingModel
$ ./coolingModel --target-time 40 --output results.csv
Wrote results.csv
$ tail -1 results.csv
40,90,80.5312673030786

Room air's temperature inside the app after the same 40-second run reads 80.5313. The exported program's 80.5312673030786 is the same number, just printed to more digits. That agreement isn't a coincidence: the export shares its graph-building and expression-transpiling code with the engine's own execution path (src/codeExport.mjs), targets the same fixed-step Explicit Euler integration, and sums contributions to a state in the same order the engine does, a node's own source terms first, then edges, since floating-point addition isn't associative and a different order can mean a genuinely different answer at the last few digits.

The Python export is generated from the identical model and answers the same question:

$ python3 coolingModel.py --target-time 40 --output results.csv
Wrote results.csv
$ tail -1 results.csv
40,90,80.5312673

What's actually reproduced

The generator targets exact numerical fidelity with the engine's own solver, not an approximation of it:

  • Each node advances at globalTimeStep / substepsPerGlobalStep. A binding into a different node's state reads a frozen snapshot taken at the start of the current global step; a binding into a node's own state reads that node's live, substep-updated value, exactly as a run inside the app does.
  • Bidirectional edges apply the same evaluated expression to both endpoints, negated on the other side, and disabled nodes or edges are excluded exactly as the engine excludes them.
  • A live parameter (Mode: Live) has no runtime control stream to receive in a standalone program, so it's baked in as a plain constant at its stored value. That's the one place a plain export doesn't carry over everything an interactive run can do. An FMU, covered separately, handles this differently.
  • A C++ or Python provider written inline on a relationship or source term is embedded by pasting its own source straight into the generated file, alongside a small vendored copy of the SDK types it already targets. Export is refused outright, with an error naming the offending node or edge, for a provider implemented in the other language, an installed plugin reference with no embeddable source, or a computational-node provider exported to C++ (which the engine itself has no C++ path for either).

Parallel execution options

Every mode the Execution dropdown offers runs the exact same math. Each node's computation only ever reads a frozen snapshot for other nodes or its own live local state, so nothing about correctness depends on execution order or concurrency. What changes is how the per-step node loop is dispatched:

  • Serial (default): a plain sequential loop. No build flags needed, and the only option Python offers besides MPI.
  • OpenMP: #pragma omp parallel for over the same nodes, mirroring the engine's own thread-pool backend. Linux and Windows just need a flag; macOS's shipped clang++ has no bundled OpenMP, so the generated file's own header spells out the one-time brew install libomp and the exact include/link paths libomp's keg-only install needs:
$ brew install libomp
$ c++ -std=c++20 -O2 -Xpreprocessor -fopenmp \
    -I"$(brew --prefix libomp)/include" -L"$(brew --prefix libomp)/lib" -lomp \
    coolingModelOpenmp.cpp -o coolingModelOpenmp
$ ./coolingModelOpenmp --target-time 40 --output resultsOpenmp.csv
$ tail -1 resultsOpenmp.csv
40,90,80.5312673030786

Bit-for-bit identical to the serial run above. For a model this small there's nothing to actually parallelize across, but the guarantee is the same regardless of size.

  • std::thread: the same dispatch, spawning and joining one thread per node every global step instead of reusing an OpenMP-managed pool. No special compiler flag beyond -pthread, at the cost of a fresh thread spawn every step: the simplest correct option when a project can't or doesn't want to depend on OpenMP.
  • MPI: genuinely new territory rather than a mirror of the app's own execution backends, which stop at one process. Nodes are split into contiguous blocks across ranks, each rank integrates only its own block, and one collective call per step reconciles the full state vector. Needs an MPI implementation (OpenMPI, MPICH, …); build with mpic++ and run with mpirun -n <ranks>, exactly like any other compiled program. Python's MPI export is the same algorithm through mpi4py (pip install mpi4py, needing the same underlying MPI install).

The other format in this dialog

Choosing FMU (FMI 2.0 Co-Simulation) in the same dialog collapses Language to C++ only and hides Execution entirely: an FMU is always a compiled binary running sequentially.

The same dialog with FMU format selected: Language locks to C++, Execution disappears, and a Merge platform FMUs button appears
An FMU is a fundamentally different kind of export: a real Co-Simulation component another tool drives, not a program you run directly. Substantial enough to cover on its own.

That path, a real .fmu for Simulink, Dymola or OpenModelica to load, plus the mirror direction of importing one someone else built, gets its own page.

Where to go from here

"Getting started" covers building this same hot plate and room air model from nothing. "Running Konjugate from the command line" covers the engine's own CLI: a different way to run a model with no window involved, but still through Konjugate itself rather than a file that no longer needs it.


Follow Konjugate on LinkedIn for updates, or subscribe via RSS.

Comments