# Python API in practice

`helia-aot convert` is a thin wrapper around a Python API. Everything the
command line does, it does by building a `ConvertArgs` and handing it to an
`AotConverter`, so anything you can express on the command line you can express
in a script.

Use the Python API to compose conversions in a build script or notebook, sweep
configuration values, or register custom parsers, operators and transforms.
The CLI also supports scripted conversions, but registry customization requires
Python; there is no CLI flag for loading a custom operator.

Install `helia-aot` into the environment running your script:

```sh
python -m pip install helia-aot
```

For a complete runnable script, jump to [A complete example](https://ambiqai.github.io/helia-aot/guide/python-api/#a-complete-example).

## The surface

Only the symbols in the package's API manifest are public. Everything else is
an implementation detail and may change without a release note.

| Symbol | What it is | Reference |
| --- | --- | --- |
| `ConvertArgs` | The whole configuration of one conversion, as a validated model | [helia_aot.cli.defines](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/cli/defines/) |
| `AotConverter` | The converter; construct it with a config and call `convert()` | [helia_aot.converter](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/converter/) |
| `RegistryContext` | The registries a conversion dispatches through | [helia_aot.registry.context](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/registry/context/) |
| `build_default_registry_context` | Builds a context from the built-ins, with your customizers applied | [helia_aot.registry.context](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/registry/context/) |
| `HeliaAotError` and its subclasses | The typed failures a conversion raises | [helia_aot.errors](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/errors/) |
| `MemoryPlan` | The planned placement of every tensor | [helia_aot.memory.defines](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/memory/defines/) |
| `SocPlatform` | A target's cores, memories and capabilities | [helia_aot.platforms.defines](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/platforms/defines/) |
| `get_platform`, `list_platforms` | Look up or enumerate the registered targets | [helia_aot.platforms.platforms](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/platforms/platforms/) |
| `AirModel` | The graph representation a conversion works in | [helia_aot.air.model](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/air/model/) |
| `custom_code_to_op_key` | Derives the canonical operator key from a LiteRT `customCode` | [helia_aot.air.op_keys](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/air/op_keys/) |
| `create_interpreter` | Builds an interpreter for reference execution | [helia_aot.interpreters](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/interpreters/) |

The [Python API reference](https://ambiqai.github.io/helia-aot/reference/api/helia_aot/) is generated from the
source and carries every signature, parameter and docstring. This page is about
how the pieces fit together.

## Building a configuration

`ConvertArgs` is a validated model, which gives you two ways in.

**From a YAML file**, which is the same file the command line reads:

```python
from helia_aot import ConvertArgs

config = ConvertArgs.from_yaml("kws.yaml")
```

`from_yaml` raises `FileNotFoundError` when the path does not exist and
`ValueError` when the file is not a mapping or a field does not validate. The
config also keeps the path it was loaded from, on its own `path` field.

**From a dictionary**, which is what you want when the configuration is
computed:

```python
config = ConvertArgs.model_validate(
    {
        "model": {"path": "kws_ref.tflite", "name": "kws_ref"},
        "module": {"path": "./out", "name": "kws_ref", "type": "neuralspot"},
        "platform": {"name": "apollo510_evb"},
        "memory": {"dump_residency_json": True},
    }
)
```

Configuration types and field constraints are checked at construction, so an
unknown module type fails here. Target lookup happens during conversion: a
plain `platform.name` string is not proof that a registered target exists. An
unregistered name can instead request a custom platform when its required
fields are supplied. The keys are the YAML keys; [Configuration](https://ambiqai.github.io/helia-aot/reference/configuration/) is the
generated list of all of them with their defaults and their command-line
spellings.

Unknown keys in a nested block warn with a suggestion rather than failing,
which is worth watching for in a script that builds configurations
programmatically: a typo will not stop you.

## Running a conversion

```python
from helia_aot import AotConverter

converter = AotConverter(config)
context = converter.convert()
```

`AotConverter(config, registry_context=None)` takes the configuration and,
optionally, the registries to dispatch through. Omit the context and the
converter builds the default, frozen one, which is exactly what the command
line does.

`convert()` runs the six stages in order (Load, Transform, Resolve, Plan, Emit
and Export) and either writes the module or raises. The return value is a code
generation context carrying the configuration, the resolved graph, the memory
plan, the platform and the operator list.

:::caution
That context object is not in the API manifest. Do not depend on its
internal fields as a stable result format. For anything durable, read the
residency report
below, which has a versioned schema.
:::

## A complete example

This converts a model, checks the conversion succeeded, and reads the plan back
out of the report it wrote.

```python title="convert_kws.py"
import json
from pathlib import Path

from pydantic import ValidationError

from helia_aot import AotConverter, ConvertArgs
from helia_aot.errors import HeliaAotError

try:
    config = ConvertArgs.model_validate(
        {
            "model": {"path": "kws_ref.tflite", "name": "kws_ref"},
            "module": {"path": "./out", "name": "kws_ref", "type": "neuralspot"},
            "platform": {"name": "apollo510_evb"},
            "memory": {"dump_residency_json": True},
            "force": True,
        }
    )
except ValidationError as error:
    raise SystemExit(f"invalid configuration: {error}") from error

try:
    AotConverter(config).convert()
except HeliaAotError as error:
    raise SystemExit(f"conversion failed: {error}") from error

module_dir = Path(config.module.path) / config.module.name
report = json.loads((module_dir / f"{config.module.prefix}_residency.json").read_text())

if report["schema_version"] != 4:
    raise ValueError("Review the residency schema before reading this report")
print("schema", report["schema_version"], "plan", report["plan_hash"])
for role, arenas in report["arenas"].items():
    for arena in arenas:
        print(role, arena["memory"], arena["used"], "of", arena["total_size"])
```

Run this with `kws_ref.tflite` available relative to the process's working
directory and heliaAOT installed in that Python environment. Success writes
`out/kws_ref/` and prints the report's schema, plan hash and arena sizes. The separate
`tensor_layout_hash` identifies the tensor layout; see [memory reports](https://ambiqai.github.io/helia-aot/guide/memory-reports/).
This example deliberately uses a directory output; a `.zip` or `.pack` export
contains the report inside the archive instead of at this filesystem path.

`force` is set because a conversion refuses to write into a directory that
already holds that module; in a script that reruns, say so deliberately rather
than deleting the tree blindly.

`HeliaAotError` catches typed conversion failures, whose subclasses also inherit
matching standard-library exception types. Configuration construction raises
Pydantic validation errors separately. Some input paths still raise ordinary
exceptions, such as `FileNotFoundError` for a missing golden archive and
`ValueError` for an unknown transform name. Keep unexpected tracebacks visible
rather than turning every exception into a successful script exit.
[Troubleshooting](https://ambiqai.github.io/helia-aot/guide/troubleshooting/) maps each class to what to
do about it.

## Extending the compiler

Every dispatch a conversion makes (the LiteRT parser for a node, the operator
class for a key, the transforms that run, the model hooks that run before
parsing) goes through a `RegistryContext`. Build one, apply your customizers,
and hand it to the converter:

```python
from helia_aot.registry import build_default_registry_context

registry_context = build_default_registry_context(
    customizers=[customize_registry],
)
AotConverter(config, registry_context=registry_context).convert()
```

The three keyword arguments are the whole contract:

| Argument | Default | What it controls |
| --- | --- | --- |
| `customizers` | `None` | Callables invoked with the context, in order, after the built-ins are registered |
| `allow_override` | `False` | Whether a registration may replace a built-in entry |
| `freeze` | `True` | Whether the context is frozen once the customizers have run |

Leave `allow_override` at its default when your customizer is only meant to
add: a registration that collides then raises instead of quietly shadowing a
built-in. Leave `freeze` at its default too, so nothing mutates the registries
once a conversion is under way.

[Custom operators](https://ambiqai.github.io/helia-aot/guide/custom-operators/) is the full walkthrough,
including the one canonical key that has to match across the parser, the AIR
operator and the operator class.

## Reading the plan

The residency report is the supported way to read a conversion's memory plan
from a script. Set `memory.dump_residency_json` and the conversion writes
`<prefix>_residency.json` next to the module, carrying a schema version, the
arena envelope per role, every tensor with its role, memory, offset and size,
and two hashes over the layout. A hash change says the layout changed; matching
hashes do not check the model's weight values or numerical behavior.

Gate on it the way you would gate on a linker map: fail when an arena exceeds
what the board offers, and treat a hash change as something to review.
[Testing and automation](https://ambiqai.github.io/helia-aot/guide/testing/#the-machine-readable-report)
covers using it in a pipeline and
[Memory](https://ambiqai.github.io/helia-aot/guide/memory/#the-residency-report) reads one field by
field.
