# Notebook Display

This notebook demonstrates the html display of Nominals core objects when used with Jupyter notebook, VS Code, or Colab.

A ficticous dataset of a helicopter spin up test is used for demonstration.

The example needs the [installed Nominal Python client](https://dev.nominal.io/python/guides/install.md) and a `default` [profile](https://dev.nominal.io/python/guides/api-keys-setup.md).

## Generate Sample Data

This examples demonstrate the typical lifecycle of piece of test equipment,
called an [asset](https://dev.nominal.io/python/guides/assets.md). Let’s imagine, for example, that we fire up the
rotor of a helicopter to measure its angular velocity.

The test records three minutes of data at 10 Hz: the rotor spins up, holds near 31 rad/s
(about 300 rpm), and spins down. Telemetry and vibration go in separate tables:

Show code cell source

Hide code cell source

```python
import numpy as np
import pandas as pd

t = np.arange(1800) / 10
speed = np.interp(t, [0, 30, 120, 150, 180], [0, 31.4, 31.4, 0, 0])
test_start = pd.Timestamp.now(tz="UTC").floor("s") - pd.Timedelta(minutes=10)
times = test_start + pd.to_timedelta(t, unit="s")
rng = np.random.default_rng(2)
tables = {
    "telemetry": pd.DataFrame(
        {
            "time": times,
            "rotor_speed": speed.round(3),
            # The gearbox warms with each rotor revolution and levels off above 70 °C.
            "gearbox_temp": (
                40 + 35 * (1 - np.exp(-np.cumsum(speed) / 15000))
            ).round(2),
        }
    ),
    "vibration": pd.DataFrame(
        {
            "time": times,
            "accel_rms": (0.02 * speed + rng.normal(0, 0.05, t.size)).round(4),
        }
    ),
}
tables["telemetry"].head()
```

Each table becomes a [dataset](https://dev.nominal.io/python/guides/datasets/overview.md) on the rig, under its own data scope:

Show code cell source

Hide code cell source

```python
from nominal.thirdparty.pandas import upload_dataframe

units = {"rotor_speed": "rad/s", "gearbox_temp": "°C", "accel_rms": "g"}
for scope, table in tables.items():
    # The upload writes the index as the first column, so the timestamps go there.
    dataset = upload_dataframe(
        client,
        table.set_index("time"),
        f"RTR-02 {scope}",
        "time",
        timestamp_type="iso_8601",
        labels=[scope],
        properties={"sample_rate_hz": "10"},
    )
    rig.add_dataset(scope, dataset)
    for channel in dataset.get_channels():
        channel.update(unit=units[channel.name])
```

Each phase of the test becomes a run on the rig, and the gearbox overheating late in the hold
becomes an event:

Show code cell source

Hide code cell source

```python
from nominal.core import EventType

seconds = pd.Timedelta(seconds=1)
for name, begin, end in [
    ("Spin-up", 0, 30),
    ("Hold at 300 rpm", 30, 120),
    ("Spin-down", 120, 150),
]:
    rig.create_run(
        f"RTR-02 {name}",
        start=test_start + begin * seconds,
        end=test_start + end * seconds,
        labels=["example"],
        properties={"test_bay": "Bay 3", "gearbox_sn": "GB-1187"},
        links=[
            {
                "url": "https://dev.nominal.io/platform/data-model/",
                "title": "Nominal data model",
            }
        ],
    )
rig.create_event(
    "Gearbox above 70 °C",
    EventType.FLAG,
    start=test_start + 108 * seconds,
    duration=72 * seconds,
    description="Gearbox temperature passed the 70 °C limit late in the hold.",
    labels=["example"],
);  # the cell shows no output
```

## Create the Nominal Client

A [`NominalClient`](https://dev.nominal.io/python/ref/generated/nominal.core.NominalClient.md#nominal.core.NominalClient "nominal.core.NominalClient") provides the link to the Nominal Platform.

```python
from nominal.core import NominalClient

client = NominalClient.from_profile("default")
rig = client.search_assets(
    properties={"rig_id": "RTR-02", "example": "notebook_display"}
)[0]
```

## An asset

The rig’s [`Asset`](https://dev.nominal.io/python/ref/generated/nominal.core.Asset.md#nominal.core.Asset "nominal.core.Asset") card lists its properties, its datasets, by [data scope](https://dev.nominal.io/python/guides/assets.md#add-data-with-data-scopes),
and its runs, newest first. Each section starts folded; click its title to open it.
Each name links to its page in Nominal, and its copy button copies its RID.
**Open in Nominal** opens the asset’s own page:

```python
rig
```

## Relationship diagrams

The card above has a **Relationships** section, after the properties, with a diagram of the rig and
the resources around it, drawn like the diagrams in
[Per-asset organization](https://dev.nominal.io/platform/data-model/organization/per-asset.md).
Data flows from the bottom up: each dataset feeds the rig under its data scope’s ref name, and the
rig’s runs sit above it. The rig’s own node has the thicker border.
Each node links to its page in Nominal, and hovering over a node shows its full name.

Run, dataset, workbook, event and data review cards draw diagrams too.
A diagram uses only what its card already loads, so it makes no requests of its own,
and it needs no JavaScript, so it also displays in untrusted and saved notebooks.

An asset with hundreds of runs would make a diagram too large to read, so a diagram draws at most
`graph_max_nodes` nodes, 20 by default. Past that, each group shows its first few nodes and a
node counting the rest. That node links to the section of the card that lists them all.
The card’s own node and one node of each group always show, so a cap smaller than that is exceeded.
For example, with room for five nodes, the rig’s diagram draws one run and counts the other two.
Here `collapse_threshold=10` also opens the sections with up to 10 items, so the diagram shows open:

```python
from IPython.display import display

from nominal.core import set_display_options

with set_display_options(graph_max_nodes=5, collapse_threshold=10):
    display(rig)
```

`show_graph=False` leaves diagrams out of the cards.

## Configuring the display

[`set_display_options`](https://dev.nominal.io/python/ref/generated/nominal.core.set_display_options.md#nominal.core.set_display_options "nominal.core.set_display_options") changes how every card looks,
from that cell on, and [`get_display_options`](https://dev.nominal.io/python/ref/generated/nominal.core.get_display_options.md#nominal.core.get_display_options "nominal.core.get_display_options") returns the current settings as [`DisplayOptions`](https://dev.nominal.io/python/ref/generated/nominal.core.DisplayOptions.md#nominal.core.DisplayOptions "nominal.core.DisplayOptions"):

```python
from nominal.core import get_display_options, set_display_options

get_display_options()
```

Timestamps show to the second by default. Hover over one to see it to the nanosecond.
For example, show timestamps in the test site’s time zone, to the millisecond, in a more
readable format:

```python
set_display_options(
    timezone="America/Denver", time_precision="ms", time_format="readable"
)
rig
```

Called on its own like this, a change lasts until the kernel restarts.
To use the same options in every notebook, put the call in an
[IPython startup file](https://ipython.readthedocs.io/en/stable/interactive/tutorial.html#startup-files),
such as `~/.ipython/profile_default/startup/nominal.py`.

Each card loads its related resources from Nominal, with a few requests, and waits at most
`fetch_timeout` seconds, 5 by default, for them. A section that hasn’t loaded by then says so.
To display cards faster, or offline, `fetch_details=False` shows only the fields
the resource already holds.
Used in a `with` block, the change lasts only until the block ends:

```python
from IPython.display import display

with set_display_options(fetch_details=False):
    display(rig)
```

`style="text"` shows the plain text form instead of cards:

```python
with set_display_options(style="text"):
    display(rig)
```

## A dataset

A [`Dataset`](https://dev.nominal.io/python/ref/generated/nominal.core.Dataset.md#nominal.core.Dataset "nominal.core.Dataset") card shows the time span of its data and lists its files and channels:

```python
telemetry = rig.get_dataset("telemetry")
telemetry
```

## A channel

A numeric [`Channel`](https://dev.nominal.io/python/ref/generated/nominal.core.Channel.md#nominal.core.Channel "nominal.core.Channel") card summarizes its data: the number of samples, the minimum,
maximum and mean, the first and last timestamps, and a chart of its range and mean over time:

```python
telemetry.get_channel("gearbox_temp")
```

## A run

A [`Run`](https://dev.nominal.io/python/ref/generated/nominal.core.Run.md#nominal.core.Run "nominal.core.Run") card shows its number, time span and duration, and lists its assets, datasets and links:

```python
hold = next(
    run
    for run in rig.list_runs()
    if run.name == "RTR-02 Hold at 300 rpm" and not run.is_archived
)
hold
```

## Files and events

Other resources have cards too, such as a dataset’s files ([`DatasetFile`](https://dev.nominal.io/python/ref/generated/nominal.core.DatasetFile.md#nominal.core.DatasetFile "nominal.core.DatasetFile"))
and an asset’s events ([`Event`](https://dev.nominal.io/python/ref/generated/nominal.core.Event.md#nominal.core.Event "nominal.core.Event")):

```python
next(iter(telemetry.list_files()))
```

```python
rig.search_events()[0]
```
