Solving a single cell in C

Solving a single cell in C#

In this demo we generate C code for a cardiac cell model, and solve a single cell with a small, standalone C program. This is how you would use the generated code from a solver written in C. If you instead want to call the generated C functions from Python, see Compiling a C-extension.

We use the ToR-ORd model of a human ventricular cardiomyocyte. You can download the model in .ode format here.

First we import what we need

import re
import shutil
import subprocess
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import gotranx

Generated files go in a separate build directory

here = Path.cwd()
build = here / "build"
build.mkdir(exist_ok=True)

Generating the C code#

Next we load the model and generate C code for it, including the generalized Rush-Larsen scheme that we will use to step the cell forward in time

ode = gotranx.load_ode(here / "ToRORd_dyn_chloride.ode")
code = gotranx.cli.gotran2c.get_code(
    ode, scheme=[gotranx.schemes.Scheme.generalized_rush_larsen]
)
(build / "model.h").write_text(code)
2026-10-21 05:31:39 [info     ] Load ode /home/runner/work/gotranx/gotranx/examples/c-single-cell/ToRORd_dyn_chloride.ode
2026-10-21 05:31:40 [warning  ] Dropping a guard whose replacement failed its numeric check value=0 var=ki
2026-10-21 05:31:40 [warning  ] Dropping a guard whose replacement failed its numeric check value=0 var=nai
2026-10-21 05:31:40 [warning  ] Dropping a guard whose replacement failed its numeric check value=0 var=cli
2026-10-21 05:31:40 [warning  ] Dropping a guard whose replacement failed its numeric check value=0 var=clss
2026-10-21 05:31:49 [info     ] Num states 52                 
2026-10-21 05:31:49 [info     ] Num parameters 144            
259533

This is the same as running the following command on the command line

gotranx ode2c ToRORd_dyn_chloride.ode --scheme generalized_rush_larsen -o build/model.h

The header contains the number of states, parameters and monitored values, functions that set the initial values, functions that look up the index of a state, parameter or monitored value by name, and the functions rhs, monitor_values and generalized_rush_larsen. For example, the signature of the scheme is

signature = re.search(r"void generalized_rush_larsen\([^)]*\)", code).group()
print(" ".join(signature.split()))
void generalized_rush_larsen(const double *__restrict states, const double t, const double dt, const double *__restrict parameters, double *values)

The C program#

The program main.c (shown in full at the bottom of this page) includes the generated header, sets the initial states and parameters, and steps the cell for two beats (2000 ms) with a time step of 0.01 ms. Every 0.1 ms it writes the time, the membrane potential v, the intracellular calcium concentration cai and the fast sodium current INa (a monitored value) to a CSV file. It looks these up by name with state_index and monitor_index, so it does not depend on the order of the states.

We compile the program with the C compiler on the system

compiler = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
if compiler is None:
    raise RuntimeError("This demo needs a C compiler (cc, gcc or clang)")
executable = build / "main"
subprocess.run(
    [compiler, "-O2", "-I", str(build), str(here / "main.c"), "-o", str(executable), "-lm"],
    check=True,
)
CompletedProcess(args=['/usr/bin/cc', '-O2', '-I', '/home/runner/work/gotranx/gotranx/examples/c-single-cell/build', '/home/runner/work/gotranx/gotranx/examples/c-single-cell/main.c', '-o', '/home/runner/work/gotranx/gotranx/examples/c-single-cell/build/main', '-lm'], returncode=0)

and run it

results = build / "results.csv"
subprocess.run([str(executable), str(results)], check=True)
CompletedProcess(args=['/home/runner/work/gotranx/gotranx/examples/c-single-cell/build/main', '/home/runner/work/gotranx/gotranx/examples/c-single-cell/build/results.csv'], returncode=0)

The results#

Finally we load the CSV file and plot the results

data = np.loadtxt(results, delimiter=",", skiprows=1)
t, v, cai, INa = data.T
fig, ax = plt.subplots(3, 1, sharex=True, figsize=(8, 8))
ax[0].plot(t, v)
ax[0].set_ylabel("v (mV)")
ax[1].plot(t, cai * 1000)
ax[1].set_ylabel("cai (µM)")
ax[2].plot(t, INa)
ax[2].set_ylabel("INa (µA/µF)")
ax[2].set_xlabel("Time (ms)")
fig.tight_layout()
plt.show()
../../_images/df6ed2d60d9fe0d7fcac0fac711e28b91bb769f34109b953e62c641e749f0df1.png

Source code#

main.c#

// Solve a single cell with the C code generated by `gotranx ode2c`.
//
// Usage: main <output.csv>
//
// The generated header (model.h) provides everything used below: the number of states and
// monitored values, the initial values, index lookups by name, monitor_values and the
// numerical scheme (here generalized_rush_larsen).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "model.h"

static int lookup(int index, const char *name)
{
    if (index < 0) {
        fprintf(stderr, "No variable named '%s' in the model\n", name);
        exit(1);
    }
    return index;
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <output.csv>\n", argv[0]);
        return 1;
    }

    const double dt = 0.01;     // ms
    const double t_end = 2000.0;  // ms, two beats with the default 1000 ms stimulus period
    const int save_every = 10;  // save every 0.1 ms

    double *parameters = malloc(NUM_PARAMS * sizeof(double));
    double *states = malloc(NUM_STATES * sizeof(double));
    double *values = malloc(NUM_STATES * sizeof(double));
    double *monitors = malloc(NUM_MONITORED * sizeof(double));
    init_parameter_values(parameters);
    init_state_values(states);

    // Look the variables up by name, so the program does not depend on their order
    const int v = lookup(state_index("v"), "v");
    const int cai = lookup(state_index("cai"), "cai");
    const int INa = lookup(monitor_index("INa"), "INa");

    FILE *out = fopen(argv[1], "w");
    if (out == NULL) {
        fprintf(stderr, "Could not open %s\n", argv[1]);
        return 1;
    }
    fprintf(out, "t,v,cai,INa\n");

    const int num_steps = (int)(t_end / dt);
    for (int step = 0; step <= num_steps; step++) {
        const double t = step * dt;
        if (step % save_every == 0) {
            monitor_values(t, states, parameters, monitors);
            fprintf(out, "%g,%.10g,%.10g,%.10g\n", t, states[v], states[cai], monitors[INa]);
        }
        // The scheme writes the states at t + dt into `values`
        generalized_rush_larsen(states, t, dt, parameters, values);
        memcpy(states, values, NUM_STATES * sizeof(double));
    }

    fclose(out);
    free(parameters);
    free(states);
    free(values);
    free(monitors);
    return 0;
}