# A Typical Experimental Run

In this example, we demonstrate the typical lifecycle of an
experimental [run](https://dev.nominal.io/python/guides/runs.md). Such a run involves a 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.

Below, we show how to register the helicopter as an asset; this is only ever done once.

Then, we measure rotor data, and upload a [dataset](https://dev.nominal.io/python/guides/datasets/overview.md) to the helicopter asset.

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).

## Registering the asset

[`create_asset()`](https://dev.nominal.io/python/ref/generated/nominal.core.NominalClient.md#nominal.core.NominalClient.create_asset "nominal.core.NominalClient.create_asset") registers the helicopter as an [`Asset`](https://dev.nominal.io/python/ref/generated/nominal.core.Asset.md#nominal.core.Asset "nominal.core.Asset").
Properties identify the physical aircraft, such as its tail number:

```python
from nominal.core import NominalClient

client = NominalClient.from_profile("default")
helicopter = client.create_asset(
    "Helicopter N123HX",
    description="Light utility helicopter",
    properties={"tail_number": "N123HX"},
)
```

## Adding a dataset to the asset

The helicopter keeps its data in a [`Dataset`](https://dev.nominal.io/python/ref/generated/nominal.core.Dataset.md#nominal.core.Dataset "nominal.core.Dataset").
An asset holds each dataset under a name, its [data scope](https://dev.nominal.io/python/guides/assets.md#add-data-with-data-scopes).
Workbook templates and checklists refer to data by this name.
Give the same kind of data the same name on every asset, here `telemetry`, so one template works for every helicopter.
[`create_dataset()`](https://dev.nominal.io/python/ref/generated/nominal.core.NominalClient.md#nominal.core.NominalClient.create_dataset "nominal.core.NominalClient.create_dataset") creates an empty dataset, and [`add_dataset()`](https://dev.nominal.io/python/ref/generated/nominal.core.Asset.md#nominal.core.Asset.add_dataset "nominal.core.Asset.add_dataset") attaches it under that name.
This is also done only once:

```python
helicopter.add_dataset(
    "telemetry",
    client.create_dataset("Helicopter N123HX telemetry")
)
```

Each later test finds the registered helicopter by its properties with [`search_assets()`](https://dev.nominal.io/python/ref/generated/nominal.core.NominalClient.md#nominal.core.NominalClient.search_assets "nominal.core.NominalClient.search_assets"):

```python
from nominal.core import Asset, NominalClient

client = NominalClient.from_profile("default")
helicopter: Asset = client.search_assets(properties={"tail_number": "N123HX"})[0]
print(helicopter.nominal_url)
```

## Preparing the simulated data

Next, we generate an angular velocity curve for a helicopter
rotor: it spins up, holds near 31 rad/s (about 300 rpm), and spins
down at t = 45 s. This synthetic curve is for illustration only; in a
real experiment, the data would come from sensors.

```python
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd

t = np.arange(600) / 10  # one minute at 10 Hz, in seconds
speed = 31.4 * (1 - np.exp(-t / 5)) + 0.3 * np.sin(2 * np.pi * t / 4)
speed = np.where(t > 45, speed * np.exp(-(t - 45) / 4), speed).clip(min=0)

test_start = pd.Timestamp.now(tz="UTC").floor("s") - pd.Timedelta(minutes=5)
data = pd.DataFrame(
    {
        "time": test_start + pd.to_timedelta(t, unit="s"),
        "rotor_speed": speed.round(3),
    }
)

data_file = Path(tempfile.mkdtemp()) / "rotor_spin_up.csv"
data.to_csv(data_file, index=False, date_format="%Y-%m-%dT%H:%M:%S.%fZ")
```

## Start the run, upload some data

We now commence our test run. During the run, we “measure” the data simulated above, and upload it to the helicopter.

```python
dataset = helicopter.get_dataset("telemetry")
file = dataset.add_tabular_data(
    data_file, timestamp_column="time", timestamp_type="iso_8601"
)
file.poll_until_ingestion_completed()  # wait until done
```

The helicopter’s `telemetry` dataset collects the data of every test ever recorded on it.
A [`Run`](https://dev.nominal.io/python/ref/generated/nominal.core.Run.md#nominal.core.Run "nominal.core.Run") selects one test from that data: it is a time slice out of all data on the asset.

After ingestion, the file’s `bounds` hold its first and last timestamps, in nanoseconds since the Unix epoch.
[`create_run()`](https://dev.nominal.io/python/ref/generated/nominal.core.Asset.md#nominal.core.Asset.create_run "nominal.core.Asset.create_run") uses them to set the run’s start and end:

```python
run = helicopter.create_run(
    "Rotor spin-up",
    start=file.bounds.start,
    end=file.bounds.end,
    labels=["example"],
)
print(run.nominal_url)
```

## Inspect the data

We may now be curious to inspect the data we uploaded. This is perhaps easiest done using a [workbook](https://dev.nominal.io/python/guides/workbooks.md) in the Nominal platform, but we can also plot it using matplotlib.

An analysis usually starts from a run, retrieved by the RID on its page in Nominal.
[`list_assets()`](https://dev.nominal.io/python/ref/generated/nominal.core.Run.md#nominal.core.Run.list_assets "nominal.core.Run.list_assets") returns the run’s asset, and [`get_dataset()`](https://dev.nominal.io/python/ref/generated/nominal.core.Asset.md#nominal.core.Asset.get_dataset "nominal.core.Asset.get_dataset") returns its `telemetry` dataset.
The dataset holds every test on the helicopter, so read it between the run’s start and end to get this test only.
[`datasource_to_dataframe()`](https://dev.nominal.io/python/ref/generated/nominal.thirdparty.pandas.datasource_to_dataframe.md#nominal.thirdparty.pandas.datasource_to_dataframe "nominal.thirdparty.pandas.datasource_to_dataframe") downloads that slice of the `rotor_speed` channel into a pandas DataFrame:

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

run = client.get_run(run.rid)  # in practice, the RID from the run's page
helicopter: Asset = run.list_assets()[0]
telemetry = helicopter.get_dataset("telemetry")
df = datasource_to_dataframe(
    telemetry, channel_exact_match=["rotor_speed"], start=run.start, end=run.end
)
ax = df.plot(y="rotor_speed", legend=False, title=run.name)
ax.set_ylabel("Angular velocity (rad/s)");
```
