C-Block Examples¶
All five examples live under examples/cblock/ and are fully self-contained
runnable scripts. Each demonstrates a progressively more advanced use case.
Run any example from the repository root:
python examples/cblock/01_passthrough_gain/01_passthrough_gain.py
Example 1 — Passthrough gain¶
Files: examples/cblock/01_passthrough_gain/
| File | Purpose |
|---|---|
gain_block.c |
Stateless C-Block that multiplies its input by 3× |
01_passthrough_gain.py |
Compile, load, wire into a circuit, assert output |
What it demonstrates¶
- The minimal C-Block skeleton: one required export (
pulsim_cblock_step), one required integer (pulsim_cblock_abi_version), no state. - Using
compile_cblock()to compile on the fly andCBlockLibraryto load. - Wiring the block inside a
SignalEvaluatorgraph with aCONSTANTsource.
Key code¶
/* gain_block.c */
#include "pulsim/v1/cblock_abi.h"
PULSIM_CBLOCK_EXPORT int pulsim_cblock_abi_version = PULSIM_CBLOCK_ABI_VERSION;
PULSIM_CBLOCK_EXPORT int pulsim_cblock_step(
PulsimCBlockCtx* ctx, double t, double dt,
const double* in, double* out)
{
(void)ctx; (void)t; (void)dt;
out[0] = 3.0 * in[0];
return 0;
}
lib = compile_cblock(C_SOURCE / "gain_block.c", name="gain_block")
blk = CBlockLibrary(lib, n_inputs=1, n_outputs=1)
assert blk.step(0.0, 1e-6, [2.0]) == [6.0]
Design notes¶
Calling (void)ctx suppresses the "unused parameter" warning from -Wall.
For truly stateless blocks this is the correct pattern: no init, no destroy,
context is always NULL.
Example 2 — First-order IIR filter¶
Files: examples/cblock/02_first_order_filter/
| File | Purpose |
|---|---|
iir_filter.c |
First-order low-pass filter with persistent state |
02_first_order_filter.py |
Run step response, verify 95% of final value at 3τ |
What it demonstrates¶
- The
pulsim_cblock_init/pulsim_cblock_destroylifecycle: allocating a heap struct for state and freeing it cleanly. - Computing
alphafromdteach step — correct for variable-timestep solvers. - Using
blk.reset()to repeat the simulation with fresh initial conditions.
Key code¶
typedef struct { double y_prev; } State;
int pulsim_cblock_init(void** ctx_out, const PulsimCBlockInfo* info) {
State* s = malloc(sizeof(State));
s->y_prev = 0.0;
*ctx_out = s;
return 0;
}
int pulsim_cblock_step(
PulsimCBlockCtx* ctx, double t, double dt,
const double* in, double* out)
{
State* s = (State*)ctx;
double fc = 100.0; /* [Hz] */
double tau = 1.0 / (2.0 * M_PI * fc);
double alpha = dt / (tau + dt);
s->y_prev = alpha * in[0] + (1.0 - alpha) * s->y_prev;
out[0] = s->y_prev;
return 0;
}
void pulsim_cblock_destroy(PulsimCBlockCtx* ctx) { free(ctx); }
Design notes¶
The alpha = dt / (tau + dt) formula is a bilinear (Tustin) approximation of
the continuous-time pole. It converges to the exact RC behaviour as dt → 0
and is stable for any positive dt. This is preferable to the Euler approximation
alpha = 1 - exp(-dt/tau) only because it is cheaper to compute (no exp).
Example 3 — PI controller in a closed loop¶
Files: examples/cblock/03_pi_controller_closed_loop/
| File | Purpose |
|---|---|
pi_controller.c |
Clamp-output PI controller with two state variables |
03_pi_controller_closed_loop.py |
Close a voltage-regulation loop; compare C PI to the built-in PI_CONTROLLER block |
What it demonstrates¶
- A 2-input C-Block:
[error, feedforward]inputs,[duty_cycle]output. - Output clamping inside the C function to keep duty in [0, 1].
- Numeric equivalence verification between a custom C implementation and Pulsim's
built-in
PI_CONTROLLER— useful for regression testing of ported algorithms.
Key code¶
typedef struct {
double integral;
double t_prev;
} PIState;
int pulsim_cblock_step(
PulsimCBlockCtx* ctx, double t, double dt,
const double* in, double* out)
{
PIState* s = (PIState*)ctx;
double error = in[0];
double kp = 0.5, ki = 50.0;
s->integral += error * dt;
double u = kp * error + ki * s->integral;
/* clamp to [0, 1] */
out[0] = u < 0.0 ? 0.0 : (u > 1.0 ? 1.0 : u);
return 0;
}
Design notes¶
Anti-windup (clamping the integral when output saturates) is omitted for brevity but is straightforward to add:
if (u >= 0.0 && u <= 1.0) {
s->integral += error * dt; /* only integrate when not saturated */
}
Example 4 — Lookup table with file I/O¶
Files: examples/cblock/04_lookup_table_efficiency/
| File | Purpose |
|---|---|
efficiency_map.c |
2D bilinear interpolation from a CSV lookup table |
efficiency_map.csv |
3×3 grid: Vds [V] × Id [A] → loss [W] |
04_lookup_table_efficiency.py |
Set environment variable, compile, run operating points |
What it demonstrates¶
- File I/O inside
pulsim_cblock_init: reading a CSV at startup, storing the table on the heap, performing bilinear interpolation each step. - Using
PULSIM_CBLOCK_CSV_PATHas a runtime-configurable path (environment variable set before instantiatingCBlockLibrary). - Multi-output block: outputs are
[loss_W, efficiency].
Key code¶
os.environ["PULSIM_CBLOCK_CSV_PATH"] = str(CSV_PATH)
lib = compile_cblock(C_SOURCE, name="efficiency_map", extra_cflags=["-lm"])
blk = CBlockLibrary(lib, n_inputs=2, n_outputs=2)
# Exact grid point
out = blk.step(0.0, 0.0, [30.0, 10.0]) # Vds=30V, Id=10A
assert abs(out[0] - 1.50) < 1e-9 # loss_W
assert 0.0 <= out[1] <= 1.0 # efficiency in [0, 1]
Design notes¶
Passing the CSV path via an environment variable rather than a hardcoded string
keeps the C source portable and avoids recompilation when the data file changes.
Alternatively, pass it through PulsimCBlockInfo.name (encode the path there)
or use extra n_inputs as a side-channel — but the environment variable pattern
is the simplest.
Example 5 — Python callable, no compiler¶
File: examples/cblock/05_python_callable_no_compiler.py
What it demonstrates¶
PythonCBlockas a drop-in replacement forCBlockLibrary— identicalstepandresetAPI, no compilation, no shared library.- Stateless logic (sigmoid with gain) as a pure function.
- Using the same block in both prototyping and production: swap
PythonCBlockforCBlockLibraryonce the C version is ready.
Key code¶
import math
from pulsim.cblock import PythonCBlock
def sigmoid_gain(ctx, t, dt, inputs):
gain = 4.0
x = gain * inputs[0]
return [1.0 / (1.0 + math.exp(-x))]
blk = PythonCBlock(fn=sigmoid_gain, n_inputs=1, n_outputs=1, name="sigmoid")
assert abs(blk.step(0.0, 0.0, [0.0])[0] - 0.5) < 1e-12 # midpoint = 0.5
Design notes¶
PythonCBlock is optimised for convenience, not throughput. For an inner loop
running at tens of kHz simulation time, compile the function to C eventually.
PythonCBlock is ideal for:
- Rapid prototyping / algorithm exploration.
- Unit tests that don't require a compiler.
- Logic that's inherently Python (e.g. calls into
scipy,numpy, or Pandas).