---
title: "Structured Input Spaces"
description: "Give each model input its own window length, sample rate, and interpolation instead of one model-wide history window"
source_url: https://ai-ops.com/docs/models/structured-input-spaces
---

# Structured Input Spaces

Most models read every input the same way: one window length, one sample rate, applied to all of them. A **structured input space** breaks that assumption. The model's flat input vector is carved into named inputs, and each one carries its own window length, sample rate, and interpolation method.

That means a flow reading sampled every second and a temperature sampled every hour can feed the same model, in one inference, without either one being resampled to the other's cadence.

A structured model's input tensor is rank-2 — `[1, flat_dim]` — where `flat_dim` is the sum of every input's window length. The per-input windows are declared in the model file (or carved during upload), not derived from the graph. See [Model Inference Requirements](https://ai-ops.com/docs/models/inference-requirements.md#structured-models-rank-2-carved-into-per-input-windows) for how this compares to Flat and Time-series shapes.

## When to Use One

| Use a structured space when | Use Flat or Time-series when |
|---|---|
| Your inputs are sampled at genuinely different rates (a 1s control signal alongside an hourly ambient reading) | Every input shares one cadence |
| Some inputs need a long lookback and others only the latest value | All inputs need the same lookback |
| The policy was trained against a gymnasium `Dict` or `Tuple` observation and you want the live vector packed the same way | The model was trained on a plain 2-D or 3-D array |
| You want each signal's staleness and history checks sized to that signal | One model-wide window is an accurate description of the data |

There is no benefit to a structured space when every input would end up with the same window and rate. A Flat or Time-series model is simpler to configure and supports [Memory Only](https://ai-ops.com/docs/models/configuring-a-model.md#memory-only), which a structured model does not.

---

## Anatomy of a Space

A structured space is a small tree. Containers group inputs; each leaf of the tree is one signal, and one input binding is created per leaf.

```text
  obs                          Dict
   ├── flow                    Box, window 5      → Input 1, slots [0–4]
   ├── temperature             Box, window 12     → Input 2, slots [5–16]
   └── ambient                 Box, window 1      → Input 3, slot  [17]

  Flat vector, leaf-major:
  ┌───────────────┬───────────────────────────────────┬────────┐
  │  flow ×5      │  temperature ×12                  │ amb ×1 │
  └───────────────┴───────────────────────────────────┴────────┘
   0             4  5                                16   17
                          flat dim 18
```

Each leaf occupies a contiguous run of slots, and the runs tile the vector exactly — no gaps, no overlap, and the total must equal the model graph's input width.

| Leaf property | Meaning |
|---|---|
| **Path** | Where the input sits in the tree, for example `obs/temperature`. A tuple child appends its index, `obs/sensors[0]` |
| **Binding order** | 1-based position in the flatten order. Input 1 is the first window in the vector |
| **Window** | How many samples this input contributes (its slice length) |
| **Sample rate** | Seconds between this input's history samples |
| **Interpolation** | How raw history is resampled onto this input's grid |

> [!NOTE] One leaf is one tag
> A single leaf maps to a single tag, whatever its window length. A leaf with a window of 12 is one tag read at 12 points in time — not 12 separate signals. To bind twelve distinct measurements, the space needs twelve leaves.

---

## Declaring a Space

There are two routes. Either the model file already declares its own space, or you carve one during upload.

### Route 1: Embedded in the model file

An ONNX file can carry its input space as embedded metadata under the `koios.observation_space` key, alongside the training and binding metadata described in [Managing Model Files](https://ai-ops.com/docs/models/model-files.md#embedded-metadata). The `koios-model-utils` library writes it:

```python
from koios_model_utils import embed_koios_metadata
from koios_model_utils.metadata import LeafSampling
from koios_model_utils.gym import observation_space_from_gymnasium

space = observation_space_from_gymnasium(
    env.observation_space,
    sampling={"obs/ambient": LeafSampling(sample_rate=3600.0)},
)

embed_koios_metadata(model, inputs=..., outputs=..., observation_space=space)
```

The flatten order is the observation space's own resolved child order, used verbatim, so the live vector Koios assembles matches the one the training script fed the model. Building a space from a live gymnasium space needs the library's optional `gymnasium` extra:

```bash
pip install "koios-model-utils[gymnasium]"
```

The library supports continuous `Box` leaves inside `Dict` and `Tuple` containers. Discrete, multi-binary, and multi-dimensional spaces are not supported.

A file that carries this metadata is recognized as **Structured** on upload, and the space is re-applied every time that file is activated.

### Route 2: Carve it in the Input Shape step

A plain rank-2 file with no embedded space can be carved by hand. Every upload continues to an **Input Shape** step after the file is accepted, offering four cards:

| Card | Shape | When it is selectable |
|---|---|---|
| **Flat** | `[1, N]` | Rank-2 files with no embedded input space |
| **Time-series** | `[1, D, F]` | Rank-3 files |
| **Structured** | `[1, flat_dim]` | Rank-2 files. Pre-selected and locked, with a **Detected from model file** badge, when the file declares its own space |
| **Multi-input** | `{a: …, b: …}` | Disabled — "Coming in a later release" |

Cards that contradict the file are disabled and give the reason on hover. A rank-3 file cannot be read as Structured, and a file that declares its own space cannot be read as anything else — for that file the step shows the space as a read-only tree and you click **Continue**.

Choosing **Structured** on a file with no embedded space opens the slice builder.

---

## The Slice Builder

The builder carves the model's flat input vector into named inputs. One row per input:

| Column | Notes |
|---|---|
| **Input name** | Required, unique, and cannot contain `/` — it becomes the last segment of the input's path, `obs/<name>` |
| **Window** | Whole number of samples, at least 1 |
| **Sample rate** | Starts on **Model rate**, meaning this input inherits the model's sample rate. Click it to set an explicit rate; the small **×** returns it to the model rate |
| **Interpolation** | **PCHIP (default)**, Linear, Previous, or Nearest |
| **Slots** | Read-only: the flat-vector positions this input occupies, e.g. `[5–16]` |

Use **Add input** to append a row — its window defaults to whatever is left unassigned. A progress bar and an **N / M slots assigned** badge track the carve: indigo while slots remain, red if you overshoot, teal when it tiles exactly.

**Save shape** stays disabled until every row is named and sized and the windows sum to exactly the model's input width. Its tooltip says what is missing: *Assign all 18 slots (12 assigned)*, or *Name every input (names must be unique)*.

> [!WARNING] Carving the active file rebuilds its input bindings
> Saving a shape on the model's active file replaces its input bindings with one per input, each with no tag assigned. Output bindings and their tags are untouched. Carve the shape first, then assign tags. If you save a shape on a file that is not active, only the file is changed — the carve takes effect when you activate it.

The carve is set during upload. To change it later, upload the file again and carve it differently.

---

## How Bindings Map to Inputs

One input binding is created per leaf, in flatten order:

- **Binding order is 1-based.** Input 1 is the first window in the flat vector, input 2 the next, and so on.
- **Binding orders must be unique.** Windows are keyed by binding order at assembly, so a duplicate would silently collapse two inputs into one. Duplicates are rejected and the model refuses to run — see [Model Configuration Errors](https://ai-ops.com/docs/models/troubleshoot-configuration-errors.md) for what that looks like and how to renumber.
- **Binding orders must be dense.** They run 1..N with no gaps, and the count must equal the number of leaves the space declares.
- **The binding's name** defaults to the last segment of its path — `obs/flow` becomes `flow`.

Some binding fields belong to the model file and some are yours to edit:

| Field | Owner |
|---|---|
| Path | The file. Re-stamped on every activation |
| Window length | The file. Read-only in the UI |
| Sample rate | You. An optional per-input override of the model sample rate |
| Interpolation | You. Defaults to PCHIP |
| Tag, scaling, calibration, failure detection, rate of change | You, exactly as on any other binding |

### On the Bindings tab

Input cards are grouped under the input they belong to, and each card reports the window in use — for example **12 samples × 1.0 h**. Hover that text for the full story: sample spacing, lookback, the slots the input occupies in the flat vector, and its interpolation method.

Open a card and the drawer gains a fourth section, **Sampling**:

| Control | Description |
|---|---|
| **Override model sample rate** | Off by default — the input uses the model's sample rate, shown beside the switch. Turn it on to give this input its own cadence |
| **Sample rate** | Time between history samples for this input, used only when the override is on |
| **Window** | Read-only: sample count and the span it covers — for a 12-sample window at one-hour spacing it reads **12 samples · 11.0 h**. The window length comes from the model's input space and cannot be changed here |
| **Advanced → Interpolation** | How raw history is resampled onto this input's grid. **Default (PCHIP)** unless you pin a method |

Full detail on the other sections is in [Assigning Bindings](https://ai-ops.com/docs/models/assigning-bindings.md#per-input-sampling-structured-models).

---

## Assembly at Scan Time

On every scan, Koios:

1. **Fixes one reference instant** for the whole model — the right edge every input's window ends at.
2. **Resamples each input** on its own cadence onto its own grid, ending at that instant.
3. **Normalizes and calibrates** each value using that binding's own settings.
4. **Lays the windows end to end** in ascending binding order, producing the rank-2 `[1, flat_dim]` vector the model was trained on.

Because the windows are concatenated whole rather than interleaved by time step, the result matches a gymnasium flatten of the same observation. Samples within each window run oldest first unless the model file declares that input as newest-first.

> [!NOTE] Fast inputs no longer wait on slow ones
> Staleness, history depth, warm-up backfill, and prediction-history retention are all sized per input. A one-second input is judged against one-second expectations even when another input in the same model looks back an hour.

---

## Warm-Up and History

A model cannot infer until every input has enough history to fill its window. For a structured model that happens per input, so one binding can be ready while another is still filling — the model waits for all of them.

- **On enable**, Koios makes a single attempt to backfill each input from long-term history, at that input's own rate and for that input's own window. Where no history exists, it falls back to holding the tag's current live value flat across the window.
- **Until a window fills**, that binding reports **Not Enough Historical Depth**. This is normal for a newly bound tag and clears on its own as data accumulates.
- **Initialize history** seeds each input over its own window rather than one model-wide window. Run it for the whole model from the three-dot menu in the model header, or for a single binding from that binding's configuration drawer.
- **Stale-data detection** measures the allowed missed samples against that input's own sample rate.

An input with a long window over a slow signal takes correspondingly longer to warm up. An hourly input with a 12-sample window needs 12 hours of history before it is ready, unless backfill or Initialize history can supply it.

---

## Restrictions

| Restriction | Detail |
|---|---|
| **Memory Only is not supported** | A structured model with Memory Only enabled saves cleanly and then refuses to run. It is left Stopped with *Model configuration is invalid* and the detail *Structured input spaces do not support in-memory history yet — disable Memory Only for this model* |
| **Rank-2 graphs only** | The structured shape applies to flat input tensors. A rank-3 (time-series) file cannot be carved |
| **Windows must tile exactly** | The per-input windows must sum to the declared flat dimension, and that must match the graph's input width |
| **Binding orders must be unique and dense** | Duplicates, gaps, or a count that disagrees with the space stop the model |
| **PCHIP is the only method applied** | An input can carry another interpolation selection, but this release resamples every input with PCHIP and records a warning in the model log when the configuration loads |
| **Multi-input models are not supported** | A model must present a single input tensor |

> [!WARNING] A misconfigured structured model fails closed
> Rather than inferring on a wrongly-assembled vector, a structured model that fails validation is stopped at load with a precise message on the model's status. If a model will not start, read the error detail first — it names the input and the mismatch. See [Model Configuration Errors](https://ai-ops.com/docs/models/troubleshoot-configuration-errors.md#structured-input-space-mismatches) for each message and its fix.

---

## Where to See the Space

| Location | What it shows |
|---|---|
| **Model detail → Overview** | The **Input** field carries the shape label, for example "3 inputs, flat dim 18 (Structured)". The model list has only Inputs, Outputs and Depth columns — no shape label |
| **File detail → Space tab** | The space as a tree, with each input's window, order, and path, plus badges for the input count, flat dimension, and element type. Only present when the file declares a space |
| **File detail → Overview** | Input and output shapes, including the flattened dimension |
| **Bindings tab** | Input cards grouped by input, each showing its window |
| **Export** | The `leaf_path`, `leaf_size`, `leaf_sample_rate`, and `leaf_interpolation` columns of a binding export. See [Importing and Exporting](https://ai-ops.com/docs/models/importing-exporting.md) |

---

## What's Next

- [Model Inference Requirements](https://ai-ops.com/docs/models/inference-requirements.md): the three input shapes and how tensors are built
- [Assigning Bindings](https://ai-ops.com/docs/models/assigning-bindings.md#per-input-sampling-structured-models): per-input sampling controls and every other binding setting
- [Managing Model Files](https://ai-ops.com/docs/models/model-files.md#input-space-koiosobservation_space): the embedded metadata a structured file carries
- [Configuring a Model](https://ai-ops.com/docs/models/configuring-a-model.md#memory-only): why Memory Only and a structured space are mutually exclusive
