Writing a relationship in C++ or Python

September 10, 2026

Every relationship and source term in this series so far has been a real equation, typed into the math field or built from reference chips. That covers a lot of ground, but not everything: a vendor's tire model, a lookup table, a control law with a branch in it. For exactly that gap, a relationship (or a node's own source term) can be implemented in C++ or Python instead, real code Konjugate compiles or launches locally and runs on the same schedule as an equation.

The example is a variation on "Getting started"'s hot plate and room air: the Cooling relationship now switches between two rate constants depending on how large the temperature gap is, fast above 20 degrees, slow below it. That's a genuine branch, not something a single equation naturally expresses, and it's one line of code.

Two parameters, still an equation for now

The relationship starts out ordinary: two custom parameters, fastRate and slowRate, sitting right next to the usual state references.

The edge builder with two parameters added, still in Equation mode
Fast rate and Slow rate show up in Available references immediately, exactly like source.plateTemperature and target.roomTemperature.

Nothing about adding parameters depends on which implementation gets chosen next. They're available as bindings either way.

Switching to C++

Implementation, in the same builder, offers Equation, C++ program or Python program. Choosing C++ swaps the equation field for a provider source panel and generates a starter template on the spot:

The edge builder with C++ program selected, showing the generated starter template
The template already names describe()/evaluate() and the SDK header. Bindings and the output key are still empty at this point.

Bindings and the output key

Bindings, underneath, is how a value gets from the model into the provider: a key the code will read it by, and a reference (a state on either endpoint, or a parameter) to read it from. Four bindings and an output key later:

The provider section filled in: four bindings, an output key, and real source in the textarea
plateTemperature, roomTemperature, fastRate and slowRate all bound; the output key roomTemperatureGradient names what the provider's single return value updates.

The source itself is plain text here, editable directly in this textarea if that's all you need. The full authoring experience lives one click away.

The code editor

Open code editor opens a dedicated window, not a panel bolted onto the builder: a real CodeMirror editor with C++ syntax highlighting, a theme picker and live validation running in the background as you type.

The provider editor window, showing syntax-highlighted C++ and a
A real clang++ -fsyntax-only check runs on every edit, debounced, not a guess at what might compile.

That check is the real compiler, not a lint approximation: C++ source gets clang++ -fsyntax-only against the actual SDK header, Python gets python3 -c "ast.parse(...)". The status line and Apply button track it directly, Apply stays disabled until the last check passed. Applying writes the source back into the same live-edit session the inline equation editors use, so the whole edit, template, bindings, output key and code, collapses into one undo step.

Running it

Create edge, then Run, same as any other model. The first run compiles the provider; every run after that reuses the cached build unless the source changes.

The completed 40-second run: Room air reaches 78.4647
Room air: 78.4647. The branch is really running: fast while the gap is above 20 degrees, slow once it narrows.

Compiler and interpreter overrides

Konjugate auto-detects a C++ compiler and a Python interpreter per machine. The button in the title bar opens Compiler & interpreter, where that detection can be overridden if it finds the wrong one, or none at all:

The Compiler & interpreter dialog, showing an auto-detected clang++ and python3
Auto-detected: /Library/Developer/CommandLineTools/usr/bin/clang++, and python3 on PATH. Browse and Test are there for the case auto-detection misses.

This is a machine-local preference, not part of the project. Opening the same project on a different computer just re-detects.

The same relationship in Python

Switching Implementation to Python instead generates a different starter template, targeting a small konjugate package rather than a C++ header. The finished provider, doing the identical job:

from konjugate import (
    EvaluationContext,
    InputView,
    OutputCollector,
    RelationshipDescription,
    RelationshipProvider,
    ScalarPort,
)


class Cooling(RelationshipProvider):
    def describe(self):
        return RelationshipDescription(
            "cooling",
            "Cooling",
            [
                ScalarPort("plateTemperature", "plateTemperature", ""),
                ScalarPort("roomTemperature", "roomTemperature", ""),
                ScalarPort("fastRate", "fastRate", ""),
                ScalarPort("slowRate", "slowRate", ""),
            ],
            ScalarPort("roomTemperatureGradient", "roomTemperatureGradient", ""),
        )

    def evaluate(self, context, inputs, outputs):
        gap = inputs["plateTemperature"] - inputs["roomTemperature"]
        rate = inputs["fastRate"] if gap > 20.0 else inputs["slowRate"]
        outputs.add_gradient(rate * gap)

Same bindings, same output key, same bindings-to-keyword-arguments shape, just Python's own syntax and an inputs["key"] lookup instead of context.inputs.at("key"). Run the identical 40 seconds and Room air lands on 78.4646829548178, the same answer as the C++ version to full precision. Nothing about the model, the bindings or the schedule changes between languages; only the implementation does.

Source terms work the same way

Everything on this page applies just as directly to a node's own source term, not only a relationship. "Tuning a parameter against measured data" already showed a source term with its own tunable parameter; that same source-term editor offers the identical Equation/C++/Python choice, the identical bindings list and the identical code editor. A parameter bound into C++ or Python code is still an ordinary parameter everywhere else in Konjugate: still tunable, still usable as a live control during a run.

Where to go from here

"Exporting a simulation as standalone code" covers what happens to a provider like this one when the whole model gets exported: its source is pasted straight into the generated file, not re-derived. "Installing add-ons and plugins" covers the other way a C++ or Python provider reaches a project, installed as a reusable .kjp plugin rather than authored inline.


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

Comments