> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Lab view

> A project-scoped dashboard of channel occupancy — see what is running, inspect a channel, watch a run, and query occupancy from the SDK

The **Lab view** is a project-scoped dashboard over the
[equipment tree](/operate/equipment) that shows every channel in your test
equipment and whether it is currently in use. Use it to answer "what is
running right now, and when does a channel free up?".

## Channel occupancy

The Lab view derives each channel's state from the measurements linked to it —
there's no live telemetry. A channel is in one of four states:

| State               | Meaning                                                                                                                                                                       |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `free`              | No linked measurement, or every linked measurement has an `end_time` set. The channel is available for a new test.                                                            |
| `occupied`          | A linked measurement has no `end_time` and was updated within the staleness window. A cell is on test now.                                                                    |
| `stale`             | A linked measurement has no `end_time` but hasn't been updated within the staleness window. Likely a stopped or silently failed test, or one someone forgot to mark complete. |
| `out_of_commission` | The channel is deliberately out of service (broken or under maintenance). Overrides the derived state above.                                                                  |

The staleness window is fixed at **48 hours**. This is comfortably larger than
a typical daily cycler upload cadence, so a channel only flips to `stale` after
it has skipped roughly two expected updates — the real "stopped or forgotten"
signal, not a briefly-late update.

To free a `stale` channel, set an `end_time` on the linked measurement (mark
the test complete) or delete the measurement.

## Viewing channels as a table

The Lab view can be switched between the default card layout (channels grouped
under their cycler) and a **table view** that renders **one row per channel**
across every cycler in the project. Use the layout toggle at the top of the
Lab view to switch modes. The choice is not saved — the view resets to cards
on refresh or navigation.

Use the table view when you want to:

* Scan every channel in the project on a single flat list rather than a per-cycler grid.
* Filter or compare channels across cyclers — for example, show all `stale`
  channels or compare their electrical ratings.
* Copy a channel-by-channel snapshot of what the lab is doing right now.

Each row shows the same information the channel card does — site, cycler,
channel name, occupancy state, electrical ratings, the active measurement (if
any) and its `estimated_end_time`, and the `out_of_commission` flag — so
nothing is hidden by switching layouts. Clicking a row opens the same
[channel page](#inspecting-a-channel) as clicking a card, and the same
`channel:read` permission gates access.

The table reads from the same aggregation `client.lab.status()` exposes, so
for the equivalent flat, one-row-per-channel view from Python, flatten the
tree yourself:

```python theme={null}
status = client.lab.status(client.project_id)

for site in status.sites:
    for cycler in site.cyclers:
        for channel in cycler.channels:
            print(site.name, cycler.name, channel.name, channel.state)
```

## Inspecting a channel

Click a channel in the Lab view to open its **channel page** — a dedicated
view of everything that's on (and has ever been on) that piece of hardware.
The page pulls together the same data other parts of Ionworks Studio already
show, so you don't need to jump between measurements to see what a channel is
doing.

The channel page shows:

* The channel's current state (`free`, `occupied`, `stale`, or `out_of_commission`) and its electrical ratings.
* The **active measurement**, if any — the cell, protocol, start time, and estimated end time — with a **Mark complete** action that sets `end_time` on the measurement and frees the channel.
* Recent data for the active measurement, split into two tabs:
  * **Time series** — a trailing window of raw signals (voltage by default; add current, temperature, or any other column via the plot's own selectors). The x-axis is a calendar-date axis anchored to the measurement's `start_time`, so the plot reads in real dates rather than elapsed seconds.
  * **Cycles** — per-cycle summary metrics across the whole measurement.
* The **history** of the 100 most recent measurements that have run on this channel, ordered by start time, with links back to each measurement's own detail page.

The time-series window is user-controlled — set the number of days (default 7)
and click **Refresh** to re-fetch. Nothing polls on its own, so the plot only
changes when you ask for it; the "as of" stamp shows when the plotted snapshot
was fetched. Only channels with an `occupied` or `stale` active measurement
have data to plot — a `free` or `out_of_commission` channel shows the state
badge and history but no chart.

Marking a measurement complete from the channel page is equivalent to patching
its `end_time` — the same operation you can do from the SDK — and moves the
channel back to `free` immediately:

```python theme={null}
from datetime import datetime, timezone

client.cell_measurement.update(
    measurement_id,
    {"end_time": datetime.now(timezone.utc).isoformat()},
)
```

Access to the channel page respects the `channel:read` permission — users
without it see the equipment tree in the Lab view but can't drill into a
channel.

## Querying occupancy from the SDK

`client.lab` wraps the same aggregation the Lab view renders from and answers
the common "what's my lab doing right now?" questions from Python. It's
read-only — use `client.site`, `client.cycler`, and `client.channel` to
mutate equipment.

| Method                                                                    | Answers                                                                                                                                              |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.lab.status(project_id)`                                           | The full sites → cyclers → channels tree with each channel's derived state and on-test measurement, plus project-level occupancy counts.             |
| `client.lab.utilization(project_id)`                                      | Headline busy percent (`occupied` + `stale`, out-of-commission included in the denominator) plus the raw counts. Matches the number on the Lab wall. |
| `client.lab.free_channels(project_id, *, min_amps, min_volts, max_volts)` | Free channels, optionally filtered by required electrical ratings. Answers "where can I run this cell?".                                             |
| `client.lab.stale_channels(project_id)`                                   | Channels whose linked test has gone quiet — likely stopped, silently failed, or forgotten tests tying up equipment.                                  |
| `client.lab.on_channel(project_id, channel_id)`                           | The measurement currently on test on a channel, or `None` when it's free, out of commission, or not in the project.                                  |

Only `status()` hits the network. The other methods are pure client-side views
over that payload — each fetches its own snapshot by default, so a single call
is always internally consistent.

```python theme={null}
util = client.lab.utilization(project_id)
print(f"{util.percent}% busy — {util.occupied}/{util.total} channels running")

# 4 A cell needing a 0–5 V window
candidates = client.lab.free_channels(
    project_id,
    min_amps=4.0,
    min_volts=0.0,
    max_volts=5.0,
)
for fc in candidates:
    print(f"{fc.site_name} / {fc.cycler_name} / {fc.channel.name}")

for fc in client.lab.stale_channels(project_id):
    m = fc.channel.measurement
    print(f"Stale: {fc.cycler_name}/{fc.channel.name} — last update {m.updated_at}")
```

Two separate calls fetch two snapshots and can disagree if equipment changes
in between. When you need several views of the **same** instant — for example
utilization alongside the free-channel list — fetch one snapshot and pass it in
via the `status` keyword:

```python theme={null}
snapshot = client.lab.status(project_id)
util = client.lab.utilization(project_id, status=snapshot)
free = client.lab.free_channels(project_id, status=snapshot)  # same instant
```

Channels missing a rating are excluded from `free_channels` when that rating
is used as a filter — an unrated channel can't be shown to satisfy the
requirement. Drop the filter to include unrated channels.

## Watching a channel

**Watch** a channel to follow the test that's currently running on it. Engineers
typically watch the handful of channels running their own cells; from the Lab
wall you can then flip a **Only watched** filter to hide everything else and
drill straight into the runs you care about.

A watch is tied to the specific [cell measurement](/data/measurements) currently
on the channel, not to the channel itself — so the moment that measurement
finishes (an `end_time` is set), the watch **clears itself automatically**.
Starting a new test on the same channel does not silently re-enter it into
your watchlist; opt in again when a new run begins.

Set watches from the channel page or the Lab wall — a **Watch** toggle appears
next to any channel that has a live `occupied` or `stale` measurement (a
`free` or `out_of_commission` channel has nothing to watch). Watches are
per-user: each teammate curates their own watchlist without affecting anyone
else.

## Scheduling invariants

The server enforces a small set of scheduling invariants so the Lab view and
your equipment records stay consistent. Requests that violate time-ordering or
type rules are rejected with HTTP 400, while channel scheduling conflicts
(overlap, out-of-commission) return HTTP 409. Nothing is written and the
channel state is unchanged.

| Invariant                                               | When it triggers                                                                                                                                                      | Status |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| One active measurement per channel                      | You try to link a measurement (create or update `channel_id`) to a channel that already has another linked measurement without an `end_time`.                         | 409    |
| No scheduling on `out_of_commission` channels           | You try to set `channel_id` on a measurement to a channel whose `out_of_commission` is `true`. Existing links on channels flipped out of service later are preserved. | 409    |
| Only `time_series` measurements can reference a channel | A `properties` or `file` measurement (no physical channel occupancy) supplies a `channel_id`.                                                                         | 400    |
| `end_time` must not precede `start_time`                | You patch a measurement so its `end_time` falls before its `start_time`. This check predates channel scheduling and applies to every measurement, linked or not.      | 400    |

The overlap check counts only measurements without an `end_time` — a completed
test never blocks a new one. To reschedule a busy channel, first close the
running measurement (patch its `end_time`) or move it to a different channel,
then link the new measurement.

```python theme={null}
from datetime import datetime, timezone

# Free the channel before starting the next test on it.
client.cell_measurement.update(
    previous_measurement_id,
    {"end_time": datetime.now(timezone.utc).isoformat()},
)

client.cell_measurement.update(new_measurement_id, {"channel_id": ch01.id})
```

If you need to bring a channel back into service, clear `out_of_commission`
before assigning a new measurement:

```python theme={null}
client.channel.update(channel_id, {"out_of_commission": False})
client.cell_measurement.update(measurement_id, {"channel_id": channel_id})
```

## Next steps

<CardGroup cols={2}>
  <Card title="Channel service" icon="wrench" href="/operate/channel-service">
    Mark a channel out of service and review its outage history.
  </Card>

  <Card title="Planned measurements" icon="calendar-check" href="/operate/planned-measurements">
    Request a test, schedule it onto a channel, and link the real run back.
  </Card>
</CardGroup>
