# Python experiments and results

> **Pre-v1 breaking cutover (#136):** this API reads native schema 4 and saved
> plan/build format 2 only. Pre-change APIs/results are unsupported; recreate
> plans/builds/captures. No v1 product release or archival reader is implied.

For upgrade steps, see [API migration](https://ambiqai.github.io/helia-estimator/guides/api-migration/).

For a worked example and visual tools, see [Parse and visualize results](https://ambiqai.github.io/helia-estimator/guides/view-results/).

## Validate without executing

`validate_input(reference, ValidationKind)` uses the same checks as `helx validate`.
It returns a `ValidationOutcome` with `kind`, `path`, `valid`, `summary`, `error`
a tuple of `ValidationIssue` values, and advisory `diagnostics`. Issues carry a code, path, message and
optional field; bundle failures retain the native reader's `ReadIssueCode`.
Other actionable input failures use `ValidationIssueCode.INVALID_INPUT`.
Diagnostics are typed `Diagnostic` values (`DiagnosticLevel.INFO` or `WARNING`);
they do not by themselves invalidate a config. The CLI logs these messages using
its existing logging configuration; Python callers decide how to present them.
Programming errors and interruptions still propagate.

```python
from helia_estimator.api import ValidationKind, validate_input

check = validate_input("examples/at110/kws_vela_only.yaml", ValidationKind.CONFIG)
if check.valid:
    print(check.summary)
else:
    for issue in check.issues:
        print(issue.code.value, issue.path, issue.message)
```

Kinds are `CONFIG`, `PLATFORM` (a shipped name or YAML path), `BUNDLE`, `FROZEN`
and `BUILD`. Config validation applies pre-compilation checks; bundle validation
verifies the retained manifest and files. These checks run no tools, download
nothing and write nothing. They do not establish host readiness or silicon
accuracy. The library prints nothing; the caller owns presentation.

## Collect native tables

`native_table(roots, fields=ExportFields.SUMMARY, strict=False)` supplies the same
rows as `helx export`. Pass a sequence of bundle directories, summary files or
parent directories. Discovery deduplicates overlapping paths, sorts candidates
within each root, and retains root order.

```python
from helia_estimator.api import ExportFields, native_table

collected = native_table(["results/"], ExportFields.SUMMARY)
for skipped in collected.skipped:
    print(skipped.path, skipped.message)  # Optional presentation by your application.
rows = list(collected.rows)               # JSON-ready mappings; no files written.
```

`NativeTable.rows` contains every indexed sample for `SUMMARY` or `ALL` fields;
`STATISTICS` gives one aggregate row per bundle, preserving exact counter ratios.
The dynamic flattened columns keep their existing native names. Missing values
remain `None`, distinct from measured zero. Each call owns its row mappings.

`NativeTable.skipped` contains `SkippedBundle` records with the rejected path,
structured reader issues and a diagnostic message. Incomplete attempts are
included even if they have no summary yet. `strict=True` raises `ResultReadError`
on the first rejected bundle. No matches or all inputs skipped returns empty
rows; the library leaves that decision to the caller. The CLI reports skipped
inputs on stderr and exits 1 when no rows remain, preserving its existing behavior.

These native tables are separate from the portable `export_result` contract.
Neither operation decides whether different runs or producers are comparable.

## Prepare a one-off case

`prepare_case` shares the preparation used by `helx run-case`. It resolves model,
platform and output paths against an explicit base directory without writing files,
downloading anything or starting tools. `RunCaseOptions` represents the case options;
`PreparedCase` contains the resolved specification and selected paths.

```python
from pathlib import Path
from helia_estimator.api import RunCaseOptions, prepare_case, estimate

case = prepare_case(
    RunCaseOptions(model="models/model.tflite", run_root=Path("results/one-off"), vela_only=True),
    base_dir=Path("/work/project"),
)
paths = case.materialize()
outcome = estimate(paths.config_path)
```

Materialization explicitly creates directories and writes `config.yaml`, marking any
previous result at that location incomplete. Full configuration admission still occurs
when `estimate` loads the config. Invalid preparation leaves prior files untouched;
execution and materialization failures retain the existing incomplete-attempt behavior.
Neither preparation nor materialization renders terminal output. Setup/download status
uses logging; library callers control its handlers and verbosity. Interactive Arm licence
presentation belongs to `helx setup`; ordinary execution never asks for consent.

## Run one experiment

Use `load_request(path)` for an existing version-2 YAML config, or construct the
same validated configuration in Python. `ExperimentConfig` and its nested types
are public names for the existing strict config models; all YAML options retain
their meaning. The request captures a snapshot, so later edits to the input
objects or YAML file do not change that request.

```python
from pathlib import Path
from helia_estimator.api import (
    EstimateRequest, ExperimentConfig, ExperimentDefaults,
    ExperimentFvp, ExperimentModel, ExperimentRun, estimate, load_request,
)

# Existing YAML; equivalent to helx run -c examples/at110/kws_vela_only.yaml.
request = load_request("examples/at110/kws_vela_only.yaml")

# Or supply typed values, with an explicit base for local relative paths.
request = EstimateRequest(
    ExperimentConfig(
        version=2,
        run=ExperimentRun(
            results_dir="results/my_case", platform="at110", platform_mode="ULP",
        ),
        defaults=ExperimentDefaults(fvp=ExperimentFvp(enabled=False)),
        models=[ExperimentModel(source="local/my_model.tflite", memory_modes=["Sram_Only"])],
    ),
    base_dir=Path.cwd(),
)
resolved = request.resolve()     # Validate platform and resolve paths; no run.
outcome = estimate(request)       # Compile, optionally build/run, write and verify.
print(outcome.results_dir, outcome.stage)
print(outcome.result.summary.run.model)
```

`load_request` checks the document. `request.resolve()` additionally reads local
platform definitions and resolves references against the YAML directory or the
explicit `base_dir`. Neither creates outputs, downloads a model, probes the host
or invokes tools. The snapshot covers configuration, not the contents of referenced
models or platform files; those are read during resolution/execution.

`estimate` deliberately starts a new attempt and invalidates any older bundle
at that destination before platform resolution. It uses the existing pipeline
and workspace safeguards. Actionable failures retain their existing exception
types and causes. Use the public `ConfigError` for config failures or `actionable_errors()` for the
specific failures also handled by the CLI; interruption and programming errors
propagate.
An incomplete attempt cannot return an `EstimateOutcome`. An outcome contains
a `VerifiedResult`, its bundle directory, summary CSV path and native FVP stage.
Verification establishes evidence integrity; inspect stage, sanity and energy
availability before interpreting the numbers. Vela-only is `disabled`, not an
executed FVP measurement. Library execution does not render CLI result tables.

The run's `helx.log` captures DEBUG-and-higher records admitted by your logging
configuration that reach the root logger. Library execution does not change
logger levels or existing handler filters. To include INFO diagnostics, enable
INFO in your application's logging setup before calling `estimate`; use handler
levels to control what appears on your console. Records suppressed by logger
levels or filters, or routed through non-propagating loggers, are not recovered.
The CLI configures INFO logging by default and DEBUG with `-vv`.

File requests retain their exact captured YAML bytes and original path.
`request.source_bytes` exposes that immutable byte snapshot for an admitted
config hash check; it is `None` for programmatic requests.
Programmatic requests retain a generated `input_config.yaml` in the bundle,
with HELX-owned paths resolved so it can be reloaded there. Engine-owned options
keep their existing path conventions. No caller-managed temporary YAML is needed.

The CLI and Python share `estimate`. Passing a YAML path directly, as in
`estimate("case.yaml")`, keeps the CLI's early invalidation behavior even when
the document has a schema error. `load_request("case.yaml")` alone remains
read-only and captures a valid document for later execution.

Execution-only CLI overrides have a typed equivalent:

```python
from helia_estimator.api import EstimateOptions, ModelCacheMode

outcome = estimate(
    "case.yaml",
    options=EstimateOptions(
        model="local/alternate.tflite",  # Relative to cwd, like CLI --model.
        model_cache_mode=ModelCacheMode.EPHEMERAL,
    ),
    progress_sink=lambda event: print(event.stage.value, event.phase.value),
)
```

`ProgressEvent` is frozen and names a `PipelineStage` and `ProgressPhase`
(`started`, `completed`, `failed` or `skipped`). Its `elapsed_seconds` is
host stage duration, not model latency. Events come from existing pipeline
boundaries, independently of console rendering; they do not claim whole-run
success, and resolution before the first pipeline stage may fail without an
event. Observers are synchronous and scoped to this call. Observer exceptions
normally propagate; if the pipeline is already failing, a failed notification
adds a note without replacing the original error. These notifications do not
make concurrent execution safe.

Stage timing and delivery live in the Rich-free progress layer. CLI commands pass
a console observer through the same `progress_sink` argument available to Python.
The FVP event measures simulator launch/execution; acquisition parsing and evidence
validation follow it, so `FVP completed` alone does not establish a valid result.
Run-file logging remains separate and preserves the application’s logging setup.

## Public types and actionable errors

The supported import boundary is `helia_estimator.api`. It includes `FvpStage`
for `EstimateOutcome.stage`, `PmuProfile` for `ExperimentFvp.pmu_profile`, and
`ProgressSink` for callbacks. These are the same objects used internally, not
parallel enum or exception definitions. `dir(api)` lists lazy public symbols
without importing their implementation modules.

```python
from helia_estimator.api import (
    ConfigError, FvpStage, PmuProfile, ProgressSink,
    actionable_errors, estimate, load_request,
)

try:
    request = load_request("case.yaml")
    outcome = estimate(request)
except ConfigError as exc:
    print(f"Check the configuration: {exc}")
except actionable_errors() as exc:
    print(f"Experiment could not complete: {exc}")
else:
    if outcome.stage is FvpStage.RAN:
        print("FVP measurements are available")
```

`actionable_errors()` returns a tuple of specific exception classes suitable for
an `except` clause. It shares the CLI's classification and does not wrap errors,
change their causes, or treat every `ValueError`/`OSError` as actionable. Individual
operations can still raise their documented argument and filesystem errors; for
example, file export can raise `OSError`. Caller bugs and `KeyboardInterrupt`
are not swallowed.

Importing the function does not load execution modules. Calling it lazily imports
error definitions, but performs no readiness checks, downloads, builds or runs.
This distinction also applies to discovering `ConfigError` with `dir(api)` versus
explicitly importing it, which loads the config implementation.

## Freeze and replay local inputs

`EstimateRequest` captures configuration. `freeze` additionally binds the local
model, platform and its transitive SoC/energy-profile files, any target and
legacy energy override. For FVP runs it also binds the shipped dependency
manifest and required patch bytes. It checks the existing static platform/FVP
constraints without creating outputs, preparing dependencies or downloading.

```python
from helia_estimator.api import freeze, load_frozen, estimate

# A remote model must first be downloaded explicitly. A local override is also
# useful when the original config names a remote model.
plan = freeze("case.yaml", model="local/model.tflite")
plan.save("local/case.plan.json")  # New file; refuses overwrite.

restored = load_frozen("local/case.plan.json")  # Verifies now.
restored.verify()                              # Read-only recheck.
for item in restored.inputs:
    print(item.role.value, item.path, item.sha256)
outcome = estimate(restored)                   # Or restored.run().
```

The equivalent CLI commands are:

```bash
uv run helx freeze -c case.yaml --model local/model.tflite -o local/case.plan.json
uv run helx validate frozen local/case.plan.json --json
uv run helx replay local/case.plan.json
```

The destination's parent must exist. A plan stores exact captured YAML for file
requests, independent validated settings, the original absolute reference base,
resolved settings, file sizes/SHA-256 values and software requirements. Moving
just the plan file or editing the original YAML does not change those settings.
The input files must remain at their recorded locations. No model or coefficient
bytes are embedded in the JSON. The plan can contain private local paths and
captured inline configuration; share it accordingly.

`FrozenRunError` is a `ConfigError` with a typed `issue` (`code`, `field`, optional
`path`, and `message`). A missing or changed input, changed resolved input set,
software version or relevant HELX/NPU/Vela/AOT implementation change refuses
replay before invalidating an existing result bundle. Other Python dependency
versions are checked as requirements. Replay checks again after execution; if
that check fails, the produced bundle is marked incomplete and no outcome is
returned. Keep the inputs and producer installation unchanged throughout a run;
these checks do not lock files against concurrent writes or modifications that
are subsequently restored.

This is a local replay plan with `schema_id: "helx.frozen-run"` and integer
`schema_version: 2`. Unknown versions/fields, duplicate JSON keys and mismatched
checksums are rejected. Its `snapshot_sha256` checks the v2 document excluding
that checksum field; it is not a signature or an intent/build/observation hash.
Removing a required input binding is also rejected against fresh resolution.
Treat the document as a producer-owned versioned format and use `load_frozen`,
not private dataclass fields, pickle or hand-edited plans.

The v2 boundary intentionally has these limits:

- Models must be local; ordinary `estimate` still supports URLs and its cache.
- Per-layer fenced execution with Vela requires effective `fvp.enabled: true`
  so the same FVP input and option checks cover both normal and fenced runs.
  Ordinary runs retain the separate main-stage/fenced-stage behavior.
- Arbitrary `cmake_args` and `fvp_args` are refused because they can introduce
  undeclared external files. `build_args` may only be `["--parallel", "N"]` with
  positive `N`; ordinary build defaults remain bounded.
- AOT overrides support built-in `platform.name` and `module.name`/`prefix`
  identifiers. Other upstream options need an adapter-owned input contract
  before they can be frozen; ordinary AOT runs retain their existing options.
- The manifest binds declared source/toolchain/FVP requirements, not the actual
  installed binary/source closure or ambient compiler environment. Normal
  managed-dependency readiness and licence checks still run during execution.
- Replay uses the existing compile/build/run pipeline. It can prepare missing
  managed dependencies and rebuilds as normal; it does not reuse a frozen
  executable. Use the compiled-build APIs below to compile once and execute retained bytes.

The current native result is schema 4 and its mapping is helx-schema4-draft4. Retain the
plan alongside the outcome if a caller needs that association; the native
manifest does not claim to contain the saved plan or a semantic build identity.

## Retain a compiled build

A frozen plan captures local inputs. `build` compiles the model and builds the
selected runner through the existing pipeline, then retains a separate verified
compiled-build record. It does not invoke FVP or produce inference PMU results.

```python
from helia_estimator.api import build, freeze, load_build

plan = freeze("case.yaml", model="local/model.tflite")
compiled = build(plan, "local/case-build")  # New directory; parent must exist.
print(compiled.runtime, compiled.kernel_library, compiled.image)
for tool in compiled.tools:
    print(tool.name, tool.version, tool.sha256)
compiled.save("local/copied-build")
retained = load_build("local/copied-build")
retained.verify()  # Offline; original model and provider paths are not needed.
```

```bash
uv run helx build local/case.plan.json --output local/case-build
uv run helx validate build local/case-build --json
```

The v2 directory has `build_record.json` (`schema_id: helx.compiled-build`,
integer `schema_version: 2`), the original `plan.json`, captured original inputs,
CMake file-API metadata, and an `evidence/` native schema-4 build-only bundle. It
retains the actual executable and runtime model. The record's file catalog has
relative paths, roles, sizes and SHA-256 values. Every retained file is checked;
missing, changed, additional, escaping or symlinked assets are refused. Loading
also verifies the native evidence and its runtime, kernel and artifact references.

`CompiledBuild`, `BuildArtifact`, `BuildInput`, `BuildTool`, `BuildSource` and
`LinkedArchive` expose immutable typed values. Original compiler/source/archive
paths are provenance, not paths that offline verification opens. `load_build`
and `verify` do not run Git, compilers or simulators, resolve a frozen plan,
prepare dependencies, import Vela, or contact the network. A record can be moved
and verified independently of the original installation.

During building, HELX checks the actual compiler drivers against CMake's selected
paths and records driver hashes/version text, managed Git revisions/tree and
patch identities, and hashes of explicitly named static archives in the link
recipe. It checks the compiler/source observations before and after the build.
These are retained bytes and observed build inputs, with scope
`retained-bytes-and-observed-build-inputs-v1`; they do not establish a complete
hermetic toolchain closure, all implicit system libraries, or physical silicon
qualification. `record_sha256` is the v2 document checksum and is separate from
the future semantic intent/build/observation hash trio.

Building may use normal managed dependency setup and licence checks. The caller's
cache options and progress sink are preserved. It writes only a new record
location and leaves the frozen plan's configured result/workspace destinations
untouched. A failed build retains an incomplete marker and available diagnostics;
`load_build` refuses it. Existing destinations are never overwritten. Record
contents include the captured model/platform/energy inputs and local path
provenance.

Retained builds support the LiteRT/CMSIS-NN, heliaRT/heliaCORE and heliaAOT/heliaCORE runner
builds with effective FVP enabled and per-layer fencing off. Ordinary estimation
continues to support Vela-only and fenced workflows. Execution from the retained
record uses `run_build` or `record.run`, described in [Run a retained build](https://ambiqai.github.io/helia-estimator/guides/python-results/#run-a-retained-build).

## Select runtime and kernels

`RuntimeEngine` and `KernelLibrary` are public enums. Their existing aliases are
accepted by `select_runtime`, which returns a frozen, validated `RuntimeSelection`.
The default is LiteRT/CMSIS-NN. heliaRT and heliaAOT use heliaCORE. These CPU kernel
choices are separate from the platform's NPU/FVP accelerator route.

```python
from helia_estimator.api import KernelLibrary, RuntimeEngine, select_runtime

selection = select_runtime(RuntimeEngine.HELIA_RT, KernelLibrary.HELIA_CORE)
fvp = ExperimentFvp(enabled=True, run=True, runtime_engine=selection.runtime)
print(selection.runtime.canonical_name, selection.kernel_library.value)
```

Selection checks have no operational effects. A `SelectionError` is a
`ConfigError` with a `SelectionIssueCode` and `allowed_values`, so consumers can
distinguish unknown names from unsupported pairs. Normal config validation and
the legacy `helia_rt_backend` field keep their existing behavior. The selector
does not enable new linkage combinations. `RuntimeEngine.value` retains native
spellings (`tflite_micro`, `helia_rt`, `helia_aot`); `canonical_name` is `litert`,
`helia-rt` or `helia-aot` for public naming. Historical results are unchanged.

## Inspect static runner support

`runtime_support()` returns frozen `RuntimeSupport` and `FvpSupport` records,
derived from registered adapters and helia-npu FVP profiles. It lists native
runtime identities, their effective kernel libraries, compatible subsystems/NPUs, registered memory modes and
bus-trace availability. Its explicit basis is `registered_runner_contract`.
This is neither local readiness nor qualification of arbitrary models or silicon.
Use `helx doctor` for explicit host checks. Runtime/kernel restrictions still
apply; heliaRT and heliaAOT use heliaCORE, LiteRT uses Arm CMSIS-NN.

```python
from helia_estimator.api import runtime_support

for runtime in runtime_support():
    print(runtime.runtime, runtime.targets, runtime.basis)
```

```sh
helx support
helx support --json  # One document; no host probe, model fetch, build or run.
```

## Read saved results

The additive `helia_estimator.api` reader inspects native result bundles without
compiling, running FVP, resolving models or contacting download hosts. It uses
HELX's existing Python 3.12 or 3.13 environment; import isolation does not remove the
package's declared installation dependencies.

```python
from pathlib import Path
from helia_estimator.api import inspect_result, load_result, export_native_summary

inspection = inspect_result(Path("results/my_case"))
if inspection.result is not None:
    result = inspection.result
    summary = result.summary
    print(summary.run.case_name, summary.fvp.stage)
    export_native_summary(result, Path("native-summary-copy.json"))
else:
    print(inspection.status, inspection.issues)

# Strict callers may use load_result(path), which raises ResultReadError
# containing the inspection if the native evidence cannot be verified.
```

`inspect_result` and `load_result` accept a bundle directory or its
`run_summary.json`. Verification requires the native version-1 manifest and
version-4 summary, a completed latest attempt, matching retained sizes/hashes,
safe relative paths, correctly bound summary/config/artifact references and
consistent result stages. Portable Windows backslash-relative paths can be
read on POSIX hosts; original manifest bytes and paths are retained. Duplicate
portable paths and escaping symlinks are rejected. Unlisted `workspace/` scratch
and the root `helx.log` are excluded, as with native bundle verification; they
are not verified evidence.
The copied config must have an input artifact role. Runtime arena usage is
accepted only for an executed stage.

Paths with Windows-reserved components, trailing dots/spaces or forbidden
characters are rejected even on POSIX, so host-specific aliases cannot become
distinct portable artifact identities.

Inspection statuses are `VERIFIED`, `INCOMPLETE`, `UNVERIFIED`, `INVALID` and
`UNSUPPORTED`. Only `VERIFIED` supplies a result. An incomplete marker overrides
an older summary and does not establish whether the attempt failed, was
interrupted or is still running. A missing manifest is `UNVERIFIED`; malformed
or corrupted evidence is not. These are native Python types, not a separately
owned portable JSON format.

A completed Vela-only or build-only result is valid. A completed result can
retain failed PMU sanity or unavailable energy; integrity does not establish
numerical correctness or silicon qualification. Simulator counter sources and
modeled energy keep their native meanings and provenance.

`VerifiedResult` captures exact summary/manifest content identities and retained
artifact references. Each `summary` access returns an independent typed view.
`result.read_artifact(reference)` rechecks containment, size/hash and completion
before returning a retained artifact. Verification is a snapshot, not a promise
that files on disk will never change. Native hashes establish content integrity,
not source authenticity.

`export_native_summary` writes the exact captured native JSON to a new file
outside the source bundle and refuses to overwrite an existing destination.
The output is a summary copy, **not** a new verified bundle; it does not copy
the manifest or artifacts. `parse_summary(path)` is an explicitly schema-only
reader that does not establish completion or retained artifact integrity.

## Inspect retained run identity

`run_identity(result)` returns a frozen `RunIdentity` over the captured native
summary and manifest. `EstimateOutcome.run_identity` provides the same view after
execution. Both paths work with the existing verified bundle; no tool probe,
dependency lookup, external model read or simulation is performed.

```python
from helia_estimator.api import load_result, run_identity

identity = run_identity(load_result("results/my_case"))
engine = identity.runtime.runtime
if engine.value is not None:
    print(engine.value.canonical_name)
print(identity.runtime.kernel_library.value)
print(identity.model.runner_model_sha256.value)
print(identity.acquisition.warmup_invokes.value)
print(identity.runtime.runtime_commit.unavailable_reason)
```

Each `IdentityFact[T]` has a typed `value`, native `sources` and an
`unavailable_reason`. A present value has no unavailable reason; a missing value
always has one. Sources identify native JSON pointers, for example
`run_summary.json#/run/runtime` or `manifest.json#/files/2/sha256`.
`NOT_RECORDED` means the mapped structured native fields supply no value, `UNRECOGNIZED_VALUE`
means a recorded value cannot be mapped, and `NOT_ESTABLISHED` means the semantic
identity has not been defined and bound. The native snapshot remains available
for inspecting unknown or future producer values.

| Identity component | Meaning |
|---|---|
| `evidence` | Existing `ResultIdentity`: exact manifest/summary hashes and native schema version |
| `configuration_sha256` | Exact retained YAML bytes, including path-specific settings; not a semantic intent hash |
| `runtime` | Recorded runtime and CPU kernel library; runtime/kernel versions and commits stay unknown when unrecorded |
| `model` | Separate original-model, Vela-container, runner-input, raw-command-stream and input-set identities |
| `runner_image_sha256` | Manifest-bound executable image bytes, not a complete build/dependency identity |
| `target` | Recorded SoC/platform revision, simulator target, accelerator, placement, operating mode, configured NPU clock and energy-profile hash |
| `acquisition` | Reported warmups, measured count, state policy and NPU callback scope when retained; non-executed stages have no acquisition facts |
| `hashes` | Future intent/build/observation semantic identities; schema-4 projection currently returns `NOT_ESTABLISHED` for all three |

New native builds retain `artifacts/build_provenance.json` in the verified
manifest. `build_provenance(result)` returns a frozen `BuildProvenance`, or `None`
for compile-only bundles without build facts. Its versioned facts include configured
runtime/kernel checkout commits, trees, tracked-change digests and exact tags,
plus the selected compiler path/version/digest and runner/model digests.
`run_identity` projects these recorded facts; it never probes current providers.

These are configured-source observations, not a complete linked-source closure.
Untracked files and external dependencies are outside this basis. Modified
tracked sources keep their base commit and change digest but have no release
version. A CMake source override that cannot be bound to the observed provider
stays unknown. Each missing `BuildFact` carries an `unavailable_reason`.
For AOT, `generator_version` names the package that generated the selected
module; it is separate from a linked runtime version, which remains unknown.
Retained replay preserves the original build facts. Contradictory or malformed
provenance is rejected rather than mixed with the native identity.

```python
from helia_estimator.api import build_provenance, load_result

facts = build_provenance(load_result("results/my_case"))
if facts is not None:
    print(facts.kernels.commit.value)
    print(facts.generator_version.value)
```

Current LiteRT uses CMSIS-NN; heliaRT and heliaAOT use heliaCORE. Unsupported
runtime/backend combinations are rejected. Pre-change native formats and
saved records are not read by this API.

A Vela/runner TFLite file contains more than a raw NPU command stream. Its digest
cannot identify the raw stream, original model or input tensors. Those identities
remain unknown unless separately recorded. The projector does not follow source
URIs, hash a current cache entry or infer linked runtime commits from today's
package pins. The configured clock describes the native run setting, not a
measured silicon clock. An FVP target represents the recorded simulation
configuration; it does not establish full-SoC emulation.

This is a context projection, not numerical output verification, hardware
qualification or a replay guarantee. Like `VerifiedResult`, it describes the
captured snapshot: reload the bundle to verify its current files. This projection does not modify the retained native record. See
[API migration](https://ambiqai.github.io/helia-estimator/guides/api-migration/) for the schema-4 and saved-format-2 cutover.

## Discover and copy saved visualizations

`viewer_artifacts(result)` returns frozen `ViewerArtifact` records over existing
manifest-bound files. `ViewerKind` distinguishes Model Explorer from Perfetto;
`ViewerBasis` distinguishes layer attribution, simulated bus timelines and
concatenated fenced costs. Model Explorer records also name the retained overlay
metric and require the original model graph. `original_model_uri` is context,
not a graph digest or proof that the model is still available. The catalog does
not assert complete operator coverage or cross-producer graph correspondence.

```python
from helia_estimator.api import load_result, viewer_artifacts, export_viewer_artifact

result = load_result("results/kws_bus_trace")
for item in viewer_artifacts(result):
    print(item.viewer, item.basis, item.artifact.relative_path)
    print(item.requires_original_model, item.metric)

# Select one catalogued file; create a new destination outside the bundle.
catalog = viewer_artifacts(result)
if catalog:
    export_viewer_artifact(result, catalog[0], "saved-viewer.json")
```

Discovery reads the captured native summary/manifest metadata. It recognizes
summary-named outputs and retained Model Explorer siblings from the existing
writer convention; arbitrary JSON is not a viewer artifact. Missing exports
remain absent. `summary_source` anchors the family in native JSON; the individual
`ArtifactRef` supplies its retained identity. Source files can change after a
snapshot: copying rechecks containment, completion, size and hash. It refuses
foreign metadata, an existing destination or a destination inside the bundle.
The parent destination directory must already exist.

These operations copy retained bytes unchanged; they do not compile, simulate,
capture, regenerate graphs or launch viewers. Existing units and missing-data
semantics stay in the files. Compiler overlay values are not new PMU counters;
fenced cumulative costs are not an observed unfenced timeline. A copied JSON is
a visualization file, not a new native verified bundle. See the
[viewer guide](https://ambiqai.github.io/helia-estimator/guides/view-results/) for loading it with the appropriate graph.

```sh
helx results artifacts results/kws_bus_trace --format json
helx results export-artifact results/kws_bus_trace \
  --artifact artifacts/bus_trace_perfetto.json --output saved-timeline.json
```

`artifacts` emits one HELX catalog document with native identity and typed viewer
metadata. It returns an empty `viewers` list when no supported files were retained.
`export-artifact` writes the selected file and reports its path on stderr.
Rejected native evidence or export errors return exit 1; usage errors return 2.
This catalog is separate from the portable result draft below.

## Draft portable mapping

`observations(result)` projects verified retained fields into the draft portable
view; `render_result(result)` emits its JSON, and `export_result(result, path)`
writes it to a new file outside the bundle. The authority is BENCH's versioned
JSON Schema, copied with source hashes under `results/contracts/v1/`. This draft
is not a released wire API. Generated candidate types are authoring aids only;
they do not parse or establish trust in arbitrary common JSON.

```python
from helia_estimator.api import observations, render_result, export_result

result = load_result(Path("results/my_case"))
view = observations(result)
print(render_result(result))
export_result(result, Path("portable-draft.json"))
```

The mapping retains NPU counters/traffic, compiler estimates, every recorded
energy mode and retained memory facts. It preserves integer counter precision
above `2**53` (including retained integer clock and compiler tokens), explicitly
converts total energy from mJ to uJ, and keeps the
native value/unit/basis alongside each energy observation. Modeled energy is
never relabeled rail measurement. CPU runner diagnostics remain native detail.
Recorded idle cycles retain a derived basis (`total - active`), simulator origin,
and pointers to retained operands; they are not labeled an independent counter.
If conversion overflows the supported finite numeric representation, the
observation is unavailable with a reason; its finite native value/unit/basis
remain in extensions. Empty native labels map to nullable draft fields while
their original values remain in native detail.
New namespaced metric IDs are not automatic cross-producer comparison contracts.

Unknown identities, engine versions, input/state protocol, acquisition windows
and sample counts are not inferred. Missing metrics have unavailable reasons.
Native JSON source pointers and manifest-bound artifact references remain
available. Configured NPU clock fields map to the explicit `npu` clock domain;
positive fractional Hz and exact integer clock tokens are retained without
rounding. Missing domains stay unknown for consumers of other producers; rate
or list position never establishes a domain. Nonpositive native clock facts
remain explicitly unrepresented in extensions with a limitation.
Native integrity, sanity/overflow, energy availability and correctness remain
separate. A retained nonzero FVP exit remains a failed outcome; an incomplete
marker cannot establish one.

## Draft result commands

```sh
helx results inspect results/my_case --format json
helx results show results/my_case --format json
helx results export results/my_case --format json --output portable-draft.json
```

`inspect` emits one draft inspection document. Verified evidence contains the
result view and returns exit 0, even when a retained native outcome is failed.
Incomplete, unverified, invalid or unsupported evidence contains issues and a
null result, returning exit 1. The Python equivalent is
`render_inspection(inspect_result(path))`.

`show` emits the common view only after native verification. `export` writes the
same JSON to a new file outside the bundle and reports the destination on stderr.
Both share the Python reader and mapping. They refuse rejected evidence with
exit 1; unknown CLI options/formats are usage errors, exit 2. JSON is the format
defined by this initial draft binding. Existing `helx show --json` remains native
schema 4, and existing `helx export` retains its table/CSV behavior.

Artifact IDs are relative and stable within the result; artifact locations are
explicit file URIs for the bundle read by the producer, independent of the export
directory. Locations are resolution context, not content identity or permission
to fetch files. Native digests remain unchanged if the bundle is moved and reread.

The exact adopted BENCH source is commit
`aa3ab6c791e36d4086f67dc663971e24ada2c8a5`, result-schema SHA
`f919b64c823d1c44df75b186cb3be890fcbf5cc6b3dbfae5b35f8e3d686acf41`;
`ORIGIN.json` records both result/inspection schema digests. The mapping version
is `helx-schema4-draft4`. The existing `1.0-draft` wire label does not identify
every draft revision; consumers should pin schema and mapping identities. Older
integer-Hz/unknown-domain draft documents remain valid under the additive schema.
The new `domain` property requires that updated consumer schema; an older closed
draft schema will reject it. Consumers should retain their current HELX pin until
they adopt this schema/mapping pair. Native JSON/CSV and verified-reader changes in 0.12.0 are documented in
[API migration](https://ambiqai.github.io/helia-estimator/guides/api-migration/).

The public `DraftResultView`, `DraftObservation`, `DraftClock` and
`DraftClockDomain` names refer to the existing typed authoring/output values.
`observations(result)` returns a new view each time; editing it does not edit
verified native bytes. These dataclasses/enums are not validators for arbitrary
common JSON and do not establish native trust or a released stable wire API.

The coordinated work was delivered through [#136](https://github.com/AmbiqAI/helia-estimator/issues/136),
against the contract coordinated in [BENCH #111](https://github.com/AmbiqAI/helia-benchmark/issues/111).
The portable mapping is independent of the execution API above; no shared package
or accepted generator is introduced.

HELX owns simulation and retained evidence; BENCH owns orchestration and
comparison eligibility.

## Run a retained build

```python
from helia_estimator.api import load_build, run_build

runner = load_build("local/kws-build")
outcome = run_build(runner, "results/kws-from-build")
# Equivalent: runner.run("results/another-run")
print(outcome.stage, outcome.results_dir)
```

```sh
helx run-build local/kws-build -o results/kws-from-build
helx validate bundle results/kws-from-build
```

This executes the retained image with the installed FVP. Vela, runtime code
preparation and native compilation are skipped; missing simulator/licence setup
is an actionable error, never an implicit rebuild. Install the appropriate
simulator with `helx setup` first. The original build environment, model/config
files and source checkouts are not required. Captured platform, SoC, energy and
target inputs determine the result; current installed platform definitions are
not substituted.

The destination must be new and outside the source record. Each invocation
writes an ordinary schema-4 result bundle, including PMU sanity, analysis,
modeled energy and debug evidence. A manifest-bound
`artifacts/compiled_build_reference.json` identifies the record checksum and
executed image/model hashes and original build software. The retained Vela INI
and compiler version describe the actual compilation, even on a newer host.
These are retained byte identities, not the future
semantic intent/build/observation hashes. The source record is verified before
and after execution; interrupted or failed outputs remain incomplete.

Retained-build execution uses the captured protocol, tensor inputs and comparison
policy. It retains numerical comparisons when references were supplied at build
time; it does not change the compiled model, input samples, warmup/repetition
counts or comparison policy at replay time. To compare captured outputs against
another reference without running again, use `verify_outputs`. A retained build
is an executable artifact: only run records from a trusted producer.

## Control acquisition lifecycle

LiteRT, heliaRT and heliaAOT support indexed measured invocations with typed state and warmup
controls. See [Acquisition lifecycle](https://ambiqai.github.io/helia-estimator/guides/acquisition/) for YAML, replay behavior,
unsupported routes and counter scope.

```python
from helia_estimator.api import AcquisitionPolicy, ExperimentFvp, StatePolicy

fvp = ExperimentFvp(
    enabled=True,
    runtime_engine="litert",
    acquisition=AcquisitionPolicy(state=StatePolicy.CARRY, warmup_invokes=2, measured_invokes=3),
)
```

`acquisition_facts(load_result(path))` returns verified `AcquisitionFacts`, or
`None` for non-executed stages. The frozen facts expose `state`,
`warmup_invokes`, `measured_invokes`, `call_once_present`, `initialization`, and
`timing_scope`. All runtimes use the same acquisition protocol. AOT records initialization before
measurement and null CALL_ONCE; interpreters record allocation/first-invoke
initialization with their observed CALL_ONCE boolean.

See [Tensor inputs and numerical verification](https://ambiqai.github.io/helia-estimator/guides/tensor-verification/) for fixed input bytes,
per-sample output artifacts and explicit reference/tolerance policies.

### Portable result identity and measurement meaning

`observations(result)` and `render_result(result)` project a verified native
bundle without probing installed runtimes or reopening its original model path.
The portable mapping version is `helx-schema4-draft4`.

| Portable field | Retained meaning |
|---|---|
| `workload.model_identity` | SHA-256 of the source-model bytes observed before compilation and checked again before publication; not the Vela/runner model or a semantic graph hash |
| `workload.state_policy` | Observed `carry` or `single` model-instance lifecycle; unavailable before execution |
| `engine.version` for heliaAOT | The provider release that generated the module, from `generator_version`; native linked-runtime version remains unavailable |
| `engine.version` for LiteRT/heliaRT | Captured linked-runtime release, when established by build evidence |
| `memory.arena_capacity.v1` | Configured capacity in bytes, scope `arena_capacity`, statistic `size`; distinct from allocator-reported use and model storage |

The `helia-estimator` extension retains `identity_facts` with native source
pointers, explicit version bases and reasons for unavailable identities.
`requested_acquisition` keeps the resolved build request separately from the
observed `acquisition`: an automatic warmup request remains null, not zero.
A conflicting explicit request and observed lifecycle/count makes reading fail.
Each indexed counter observation covers one measured inference; exact batch
statistics remain a separately labeled native extension. Energy observations
retain their modeled basis and mode, independently of simulated counters.

A model digest identifies bytes, not equivalent precision, placement, timing
scope or target behavior. The source digest alone does not authenticate the
model, and runner-model/command-stream identities are distinct. Unknown target identities stay unavailable. Retained tensor inputs have a
separate storage identity, described in [tensor verification](https://ambiqai.github.io/helia-estimator/guides/tensor-verification/). BENCH owns comparison eligibility;
matching names or hashes do not automatically qualify a comparison.

### Simulation target configuration identity

```python
from helia_estimator.api import load_result, simulation_target_identity

identity = simulation_target_identity(load_result("results/my_run"))
print(identity.value or identity.unavailable_reason)
```

The `helx.simulation-target-config/v1:sha256:` namespace identifies retained
configured simulation facts, never a physical target or HPX equivalent. The
portable draft4 mapping uses this value as `target.identity` and retains the
preimage, source pointers and unavailable reason in its namespaced extension.

`artifacts/simulation_target_build.json` captures explicit target settings from
the configured build and binds the runner image. Retained-build replay carries
that evidence forward. At launch, `artifacts/simulation_target.json` binds those
settings to the actual simulator executable digest and configured launch facts.
The reader only reads manifest-bound artifacts; it performs no host discovery.

The canonical hash object includes:

- `schema`, Corstone `subsystem`, positive whole-Hz configured `npu_clock_hz`,
  and `simulator_sha256`.
- `build_settings`: CPU/device and NPU family/architecture definitions, platform
  target directory, register/IRQ/UART/timing-adapter addresses, firmware
  `SYSTEM_CORE_CLOCK`, MAC count, effective memory mode and cache capacity,
  external-region routing, SRAM/EXT burst controls, and all 13 timing-adapter
  register settings for each of two ports. The exact closed key set is
  `BUILD_KEYS` in `results/simulation_target.py`.
- `launch_parameters`: configured NPU MAC count, explicit U85 SRAM hash,
  and the target profile's additional hardware parameters (U55/U65 TCM sizes).

The SHA-256 preimage is UTF-8 JSON, recursively sorted keys, compact separators,
`ensure_ascii=False`, no nonfinite numbers. It is available as `canonical_json`.
Build-setting strings preserve their recorded CMake spelling; different spellings
may conservatively differ. Paths, model/runtime/kernel identities, acquisition,
reference policy, output measurements and energy coefficients are excluded.
The image digest binds the receipt to evidence but is not in the target hash.

Effective C/C++/assembler flags from the CMake cache are retained and checked,
including global and selected-build-type flags. Only ordinary optimization/debug
flags and `-DNDEBUG` are admitted; unclassified flags (including target macro
overrides or response files) leave identity unavailable. Missing compiler-flag
evidence also leaves identity unavailable, after checking all available facts for
contradictions. Build arguments admit only no arguments or `--parallel N` with
positive N; persisted CMake project-include and make-rule override hooks also
leave identity unavailable. Receipts are read and written as UTF-8.
Launch MAC/TCM/mask settings must be
nonnegative integers, independent of optional summary cross-checks.

Missing receipts, implicit build defaults, unclassified custom CMake/FVP
arguments, missing image bindings and non-whole-Hz NPU rates leave identity
unavailable. Malformed or contradictory evidence used to establish an identity
invalidates the result. When launch evidence is absent or explicitly unavailable,
no target identity is asserted; the identity reader does not certify unused build
receipts.
Older captures are not hydrated from current defaults. Separate simulator
binaries conservatively produce separate identities, including across hosts.

`SYSTEM_CORE_CLOCK` is firmware configuration, not a measured CPU clock. Portable
NPU clocks retain configured origin/domain. This identity is not full executable
or environment equivalence (including simulator shared libraries), timing
qualification, silicon identity, numerical correctness or energy calibration.
BENCH owns comparison admission and any future cross-product target equivalence.

The identity is a description of HELX's supported build/launch configuration,
with safeguards against accidental misconfiguration. Cached toolchains that
differ from the active runtime environment leave identity unavailable. This is
not an attestation of arbitrary toolchains, modified receipts, or executable
semantics. HELX supplies facts and unknown reasons; BENCH owns comparison policy
and whether those facts suffice for a particular benchmark.
