---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
kernelspec:
  display_name: Python 3
  language: python
  name: archimedes
---

```{code-cell} python
:tags: [hide-cell]
# ruff: noqa: N802, N803, N806, N815, N816

import numpy as np
from scipy import signal

import archimedes as arc
```

```{code-cell} python
:tags: [remove-cell]
from utils import cleanup

from archimedes.docs.utils import display_text

cleanup()  # Clean up any previous generated code
```

```{code-cell} python
:tags: [remove-cell]
from pathlib import Path

plot_dir = Path.cwd() / "_plots"
plot_dir.mkdir(exist_ok=True)
```

# Generated C API

In the previous part of this tutorial, we learned how to automatically translate Python code to highly efficient C implementations.
However, the C code generated by CasADi uses a generic interface that requires that all arrays be initialized externally.
If we want to call the generated function from our own C code or use it in an embedded setting, this creates overhead of having to manually determine and adjust array sizes, names, initializations, etc.

In embedded systems, memory management is critical. The "kernel" implementation of the algorithm is extremely efficient, but it expects:

1. Pre-allocated memory buffers for all inputs and outputs
2. Properly configured pointers to these buffers
3. Initialization of working memory needed by the algorithm

Manually handling these details would be tedious and error-prone - especially when algorithms change during development.

Archimedes handles this with an "interface layer", which creates a predictable and easy-to-use API for working with the low-level kernel code.  On this page we will go through some of the details of this interface layer.

First, we will generate the code just as before.

```{code-cell} python
:tags: [hide-cell]

# Note: give descriptive names for return values (these don't need
# to match the variable names, but must be different from arg names)
@arc.compile(return_names=("u_hist", "y_hist"))
def iir_filter(u, b, a, u_prev, y_prev):
    # Update input history
    u_prev[1:] = u_prev[:-1]
    u_prev[0] = u

    # Compute output using the direct II transposed structure
    y = (np.dot(b, u_prev) - np.dot(a[1:], y_prev[: len(a) - 1])) / a[0]

    # Update output history
    y_prev[1:] = y_prev[:-1]
    y_prev[0] = y

    return u_prev, y_prev


# Design a simple IIR filter with SciPy
dt = 0.01  # Sampling time [seconds]
Wn = 10  # Cutoff frequency [Hz]
order = 4
b, a = signal.butter(order, Wn, "low", analog=False, fs=1 / dt)

# Create "template" arguments for type inference
u = 1.0
u_prev = np.zeros(len(b))
y_prev = np.zeros(len(a) - 1)
args = (u, b, a, u_prev, y_prev)

arc.codegen(iir_filter, args)
```

## The interface code

Last time we looked at the kernel code, which used flattened arrays of pointers to access inputs, outputs, and working memory.
While highly efficient, this code on its own is difficult to work with directly, particularly for more complex functions with multiple arguments and return values.

Here's what the "interface" code looks like:


```{code-cell} python
:tags: [remove-input]
with open("iir_filter.h", "r") as f:
    c_code = f.read()

display_text(c_code)
```

Now, instead of manually wrangling pointer arrays, we have function-specific input and output structs with meaningful names.

The header is enough for us to work with this in our own code, but for completeness here's the corresponding C code:


```{code-cell} python
:tags: [remove-input]
with open("iir_filter.c", "r") as f:
    c_code = f.read()

display_text(c_code)
```

If you are familiar with C, this code will be largely self-explanatory.
It takes care of all array and pointer initialization, specifically initializing all arrays to the values of the "template arguments" we passed to `codegen`.

One additional piece that's worth noting: the kernel code had some nice properties including deterministic and static memory allocation, and portable, self-contained code.  The interface layer preserves these features, so it is also suitable for either standalone C/C++ applications or deployment to embedded platforms.

## Working with the generated API

In order to use the generated code we have to do three things:

```c
#include "iir_filter.h"

// 1. Declare the input, output, and workspace structures
iir_filter_arg_t arg;
iir_filter_res_t res;
iir_filter_work_t work;

// 2. Initialize the structs
iir_filter_init(&arg, &res, &work);

// 3. Call the function
iir_filter_step(&arg, &res, &work);
```

Note that, as in the "quick start" example earlier in this series, for recursive filters like the IIR, the `iir_filter_step` function does not place the outputs back in the input arrays.
For example, a more useful iteration written as a callback might look like the following:

```c
int n = sizeof(arg.b) / sizeof(arg.b[0]) - 1;  // Filter order
float y;  // Output, to be fed to actuator, control algorithm, etc.

void controller_callback(void) {
    arg.u = dma_buffer[0];  // Read from sensor, other algorithm output, etc.
    iir_filter_step(&arg, &res, &work);
    y = res.y_hist[0];  // Output, to be fed to actuator, control algorithm, etc.

    // Copy output arrays back to inputs
    for (int j = 0; j < n; j++) {
        arg.u_prev[j] = res.u_hist[j];
        arg.y_prev[j] = res.y_hist[j];
    }
    arg.u_prev[n] = res.u_hist[n];
}
```

As a side note, you may notice here that we're using roughly twice as much memory as we strictly need, since in this case we know that the outputs will be fed back to the inputs.
This will generally be the case for recursive structures (IIR filters, sensor fusion, and similar algorithms) and highlights a limitation of the "functionally pure" assumption underlying code generation in Archimedes (and CasADi).
While the generated code is efficient, it is not necessarily optimal; if you are working with tightly resource-constrained platforms or otherwise need extreme performance, you may still need hand-coding or other specialized tools.

On the other hand, if you can afford to keep a few extra floats in memory and perform the associated copy operations, this workflow provides a clean way to abstract the algorithm implementation from the rest of the embedded application.

For example, if we want to choose a different order and cutoff filter, we can do so easily:

```{code-cell} python
:tags: [remove-cell]
cleanup()  # Cleanup any previous runs
```

```{code-cell} python
# Design an IIR filter with SciPy
dt = 0.01  # Sampling time [seconds]
Wn = 20  # Cutoff frequency [Hz]
order = 2
b, a = signal.butter(order, Wn, "low", analog=False, fs=1 / dt)

# Create "template" arguments for type inference
u = 1.0
u_prev = np.zeros(len(b))
y_prev = np.zeros(len(a) - 1)
args = (u, b, a, u_prev, y_prev)

arc.codegen(iir_filter, args)
```

```{code-cell} python
:tags: [remove-input]
with open("iir_filter.h", "r") as f:
    c_code = f.read()

display_text(c_code)
```

This has an identical API to the first version and will automatically initialize the correct filter coefficients, meaning we can tune the filter and re-deploy without changing any of the application code!

### Platform Portability

Similar usage patterns can be applied in a range of contexts; since the generated code is self-contained it is highly portable (provided the compiler supports ANSI C).

For example, here's a "cookbook recipe" for running a generated function in a timed loop on an Arduino.

```{code-cell} python
:tags: [remove-input]
with open("iir.ino", "r") as f:
    c_code = f.read()

display_text(c_code)
```

Note that the "callback" function is identical to the previous snippet; the same auto-generated code can target everything from rapid prototyping platforms to production-grade embedded controllers to Unix clusters.

For example, the same timed-loop behavior could be implemented on an STM32 with something like the following:


```{code-cell} python
:tags: [remove-input]
with open("stm32_main.c", "r") as f:
    c_code = f.read()

display_text(c_code)
```

## Summary

In this part of the hardware deployment tutorial, we covered the basic structure of the "interface" layer, or API, generated by Archimedes as a way to abstract application code from the low-level kernel implementation.

For this simple case, managing inputs, outputs, and state as individual scalars and arrays works fine.
However, as functions grow in complexity, manually dealing with individual arrays quickly becomes difficult to maintain.
In the final part of this series we will rewrite this same example using [structured data types](../../trees.md) to autogenerate C `struct` types for hierarchical structured data.

```{code-cell} python
:tags: [remove-cell]
cleanup()
```