# Using a single dataset for many identical assets

This approach is for testing a very large number of similar or identical units, as in production or [end-of-line test](https://dev.nominal.io/platform/reference/glossary.md#end-of-line-eol-test "The final test sequence a unit goes through before it ships, run on a test stand at the end of the production line to confirm the finished unit meets its specifications."), where every unit is tested against known criteria. It can also work for a fleet, but the fleet must be very large before this approach fits better than the [default approach](https://dev.nominal.io/platform/data-model/organization/per-asset.md).

The biggest advantage of this approach is that you don’t manage a dataset for every unit. Instead, you have a dataset per [tester](https://dev.nominal.io/platform/reference/glossary.md#tester "Another name for a test stand: the equipment that runs tests on a unit under test."), or even [one for a whole production line](https://dev.nominal.io/platform/data-model/organization/tagged-data.md#multi-tester-line). Organizing data this way also lets you review *all* of the data a tester gathered, as described in [production tester monitoring](https://dev.nominal.io/platform/data-model/organization/tagged-data.md#production-test). The drawback is that every point must be correctly tagged when it is loaded into Nominal.

## Implementing this approach

In this approach, the data itself is [tagged](https://dev.nominal.io/platform/reference/glossary.md#tag "A key-value pair on your data points, such as motor_id: 1, that lets you filter a subset of points from a single dataset.") with a physical asset name and users filter their data based on that asset identifier. The table below demonstrates how channel values are stored under the hood:

One channel

| Time | Measurement | Tag |
| --- | --- | --- |
| 12:00:00 | 100.0 | `UUT=A` |
| 12:00:00 | 100.0 | `UUT=B` |
| 12:00:01 | 200.0 | `UUT=A` |
| 12:00:01 | 200.0 | `UUT=B` |
| 12:00:02 | 300.0 | `UUT=A` |
| 12:00:02 | 300.0 | `UUT=B` |
| 12:00:03 | 200.0 | `UUT=A` |
| 12:00:03 | 200.0 | `UUT=B` |

In this simplified example, each time series point under a given channel is tagged with A or B, allowing the system to identify `Measurement[A]` and `Measurement[B]`. At a conceptual level, that looks like:

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef red fill:#fee2e2,stroke:#b91c1c,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937

    subgraph dataset1["Dataset"]
        rowA["12:00:00 · 100.0 · UUT=A<br>12:00:01 · 200.0 · UUT=A<br>..."]:::purple
        rowB["12:00:00 · 100.0 · UUT=B<br>12:00:01 · 200.0 · UUT=B<br>..."]:::purple
        rowC["12:00:00 · 100.0 · UUT=C<br>12:00:01 · 200.0 · UUT=C<br>..."]:::purple
    end
    style dataset1 fill:#f5f7ff,stroke:#4338ca,color:#1f2937

    asset1["Asset: UUT A"]:::green
    rowA --> asset1

    asset2["Asset: UUT B"]:::green
    rowB --> asset2

    asset3["Asset: UUT C"]:::green
    rowC --> asset3

    click asset1,asset2,asset3 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
```

Each [asset](https://dev.nominal.io/platform/reference/glossary.md#asset "A physical or simulated device or system you track in Nominal, such as a vehicle, a component, a unit under test, or a test stand.") attaches the [dataset](https://dev.nominal.io/platform/reference/glossary.md#dataset "A store in Nominal for time-series and log data, which grows as you upload or stream more into it.") through a [data scope](https://dev.nominal.io/platform/reference/glossary.md#data-scope "The link between an asset or run and a data source, named by its ref name. It can filter the data source by tags and shift its timestamps.") that filters the dataset on that unit’s tag, so each asset sees only its own unit’s points.

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef red fill:#fee2e2,stroke:#b91c1c,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937

    subgraph dataset1["Dataset"]
        raw["12:00:00 · 100.0 · UUT=A<br>12:00:00 · 100.0 · UUT=B<br>12:00:00 · 100.0 · UUT=C<br>...<br>12:29:59 · 123.4 · UUT=A"]:::purple
    end
    style dataset1 fill:#f5f7ff,stroke:#4338ca,color:#1f2937

    asset1["Asset: UUT A"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=A"| asset1

    asset2["Asset: UUT B"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=B"| asset2

    asset3["Asset: UUT C"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=C"| asset3

    click asset1,asset2,asset3 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
```

To set this up in code, create the shared dataset once, give each unit under test an asset which filters the shared dataset on that unit’s tag, and tag every point you write. The tag on a point is what decides which asset it belongs to.

**Python**

```python
from nominal.core import NominalClient

client = NominalClient.from_profile("default")

# One dataset per tester, shared by every unit it tests
dataset = client.create_dataset("Tester X1")

# One asset per unit. Each attaches the same dataset under the same ref name,
# filtered to the points tagged with that unit's ID.
for uut in ["A", "B", "C"]:
    asset = client.create_asset(f"UUT {uut}")
    asset.add_dataset("default", dataset, series_tags={"UUT": uut})

# Every point must carry the tag. The tag decides which asset sees the point:
# this one appears on UUT A only.
with dataset.get_write_stream() as stream:
    stream.enqueue("Measurement", "2026-01-01T12:00:00Z", 100.0, tags={"UUT": "A"})
```

**Rust**

```rust
use nominal::core::{AssetCreate, DatasetCreate};
use nominal::streaming::prelude::*;
use nominal::streaming::stream::NominalDatasetStreamBuilder;
use std::time::UNIX_EPOCH;

let client = nominal::NominalClient::from_profile("default")?;

// One dataset per tester, shared by every unit it tests
let dataset = client.catalog().create_dataset(DatasetCreate::new("Tester X1")).await?;

// One asset per unit. Each attaches the same dataset under the same ref name,
// filtered to the points tagged with that unit's ID.
for uut in ["A", "B", "C"] {
    let asset = client.assets().create(AssetCreate::new(format!("UUT {uut}"))).await?;
    client
        .assets()
        .add_dataset_with_tags(asset.rid(), "default", dataset.rid(), [("UUT", uut)])
        .await?;
}

// Every point must carry the tag. The tag decides which asset sees the point:
// this one appears on UUT A only.
let token = BearerToken::new(&std::env::var("NOMINAL_TOKEN")?)?;
let stream = NominalDatasetStreamBuilder::new()
    .stream_to_core(token, ResourceIdentifier::new(dataset.rid())?, tokio::runtime::Handle::current())
    .build();
stream
    .double_writer(ChannelDescriptor::with_tags("Measurement", [("UUT", "A")]))
    .push(UNIX_EPOCH.elapsed()?, 100.0);
```

**MATLAB**

```matlab
client = nominal.Client.fromProfile();

% One dataset per tester, shared by every unit it tests. Create it in the Nominal UI
% or with another client, then look it up by its RID.
dataset = client.datasetByRid("<ri.catalog.instance.dataset.identifier>");

% One asset per unit. Each attaches the same dataset under the same ref name,
% filtered to the points tagged with that unit's ID.
for uut = ["A" "B" "C"]
    asset = client.getOrCreateAsset("UUT " + uut);
    asset.addDataset(dataset, "default", Tags=struct(UUT=uut));
end

% Every point must carry the tag. The tag decides which asset sees the point:
% these appear on UUT A only.
stream = dataset.stream();
measurement = stream.channel("Measurement");
measurement.tag("UUT", "A");
measurement.push(datetime("now"), 100.0);
```

**Go**

```go
import (
	"time"

	"github.com/nominal-io/nominal-api-go/api/rids"
	nominalapi "github.com/nominal-io/nominal-api-go/io/nominal/api"
	assetapi "github.com/nominal-io/nominal-api-go/scout/asset/api"
	"github.com/nominal-io/nominal-api-go/scout/assets"
	"github.com/nominal-io/nominal-api-go/scout/catalog"
	runapi "github.com/nominal-io/nominal-api-go/scout/run/api"
	nominal "github.com/nominal-io/nominal-streaming-go"
)

// httpClient is a conjure-go-runtime httpclient.Client for your Nominal REST API URL,
// token is your API key as a bearertoken.Token, and apiKey is the same key as a string
catalogClient := catalog.NewCatalogServiceClient(httpClient)
assetClient := assets.NewAssetServiceClient(httpClient)

// One dataset per tester, shared by every unit it tests
dataset, _ := catalogClient.CreateDataset(ctx, token, catalog.CreateDataset{Name: "Tester X1"})

// One asset per unit. Each attaches the same dataset under the same ref name,
// filtered to the points tagged with that unit's ID.
for _, uut := range []string{"A", "B", "C"} {
	assetClient.CreateAsset(ctx, token, assetapi.CreateAssetRequest{
		Title: "UUT " + uut,
		DataScopes: []assetapi.CreateAssetDataScope{{
			DataScopeName: "default",
			DataSource:    runapi.NewDataSourceFromDataset(rids.DatasetRid(dataset.Rid)),
			SeriesTags:    map[nominalapi.TagName]nominalapi.TagValue{"UUT": nominalapi.TagValue(uut)},
		}},
	})
}

// Every point must carry the tag. The tag decides which asset sees the point:
// this one appears on UUT A only.
streamClient, _ := nominal.NewClient(apiKey)
stream, _, _ := streamClient.NewDatasetStream(ctx, dataset.Rid.String())
stream.FloatStream("Measurement", nominal.WithTags(nominal.Tags{"UUT": "A"})).Enqueue(time.Now().UnixNano(), 100.0)
stream.Close()
```

The native workbook fit for this storage approach is to compare results asset-to-asset. That is, users can produce a workbook which permits analysis of a single UUT’s results or one which compares all data across assets.

A workbook for a single UUT’s results points at one asset at a time:

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937
    classDef blue fill:#e0f2fe,stroke:#0369a1,color:#1f2937
    classDef hidden fill:none,stroke:none
    spacer[" "]:::hidden

    asset1["Asset: UUT A"]:::green
    asset2["Asset: UUT B"]:::green
    dataset1[("Dataset")]:::purple
    dataset1 --> asset1
    dataset1 --> asset2

    workbook1["Workbook for<br>Test Results<br><br>Points at one UUT at a time"]:::blue
    asset1 -.->|"pointing at A"| workbook1
    asset2 -.->|"pointing at B"| workbook1
    workbook1 ~~~ spacer

    click asset1,asset2 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
    click workbook1 href "../../../workbooks/" "Workbooks"
```

A workbook comparing UUTs points at all of their assets at once:

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937
    classDef blue fill:#e0f2fe,stroke:#0369a1,color:#1f2937
    classDef hidden fill:none,stroke:none
    spacer[" "]:::hidden

    asset1["Asset: UUT A"]:::green
    asset2["Asset: UUT B"]:::green
    dataset1[("Dataset")]:::purple
    dataset1 --> asset1
    dataset1 --> asset2

    workbook2["Workbook for<br>Test Comparison<br><br>Points at many assets"]:::blue
    asset1 --> workbook2
    asset2 --> workbook2
    workbook2 ~~~ spacer

    click asset1,asset2 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
    click workbook2 href "../../../workbooks/" "Workbooks"
```

You can also add a [run](https://dev.nominal.io/platform/data-model.md#data-model-runs) for each test on these assets, so workbooks can compare individual tests on one unit or across units.

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef red fill:#fee2e2,stroke:#b91c1c,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937
    classDef blue fill:#e0f2fe,stroke:#0369a1,color:#1f2937
    classDef hidden fill:none,stroke:none
    spacer[" "]:::hidden

    %% basic asset
    asset1["Asset: UUT A"]:::green
    asset2["Asset: UUT B"]:::green
    dataset1[("Dataset")]:::purple
    dataset1 --> asset1
    dataset1 --> asset2

    run1["Run: Test 1<br>on UUT A"]:::red
    asset1 --> run1
    run2["Run: Test 1<br>on UUT B"]:::red
    asset2 --> run2
    workbook2["Workbook for<br>Test Comparison<br><br>Points at many runs"]:::blue
    run1 --> workbook2
    run2 --> workbook2

    click asset1,asset2,asset3 href "../../#asset" "Asset"
    click dataset1,dataset2,dataset3 href "../../#time-series-data" "Time-series data"
    click run1,run2,run3 href "../../#runs" "Runs"
    click workbook1,workbook2 href "../../../workbooks/" "Workbooks"
```

## Extensions

### Units run through multiple testers

The approach above assumes each unit is tested by a single [tester](https://dev.nominal.io/platform/reference/glossary.md#tester "Another name for a test stand: the equipment that runs tests on a unit under test."). A [production line](https://dev.nominal.io/platform/reference/glossary.md#production-line "The sequence of stations a product moves through as it's built and tested. A production line can have many test stands, each running part of the test plan on every unit.") can instead split the test plan across testers. For example, testers X1, X2, and X3 run the tests in category X, testers Y1, Y2, and Y3 run category Y, and a unit tested by X1 might go through any of the Y testers.

In this case, use a single dataset for the entire line and tag every point with both its unit and its tester.

One channel

| Time | Measurement | Tags |
| --- | --- | --- |
| 12:00:00 | 100.0 | `UUT=A`, `tester=X1` |
| 12:00:00 | 100.0 | `UUT=B`, `tester=X2` |
| 12:00:01 | 200.0 | `UUT=A`, `tester=X1` |
| 12:00:01 | 200.0 | `UUT=B`, `tester=X2` |
| 12:00:02 | 300.0 | `UUT=A`, `tester=Y3` |
| 12:00:02 | 300.0 | `UUT=B`, `tester=Y1` |
| 12:00:03 | 200.0 | `UUT=A`, `tester=Y3` |
| 12:00:03 | 200.0 | `UUT=B`, `tester=Y1` |

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef darkgreen fill:#bbf7d0,stroke:#15803d,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937

    subgraph dataset1["Dataset: Production line"]
        rowX1["12:00:00 · 100.0 · UUT=A · tester=X1<br>..."]:::purple
        rowY3["12:00:02 · 300.0 · UUT=A · tester=Y3<br>..."]:::purple
        rowX2["12:00:00 · 100.0 · UUT=B · tester=X2<br>..."]:::purple
        rowY1["12:00:02 · 300.0 · UUT=B · tester=Y1<br>..."]:::purple
    end
    style dataset1 fill:#f5f7ff,stroke:#4338ca,color:#1f2937

    unitA["Asset: UUT A"]:::green
    rowX1 --> unitA
    rowY3 --> unitA

    unitB["Asset: UUT B"]:::green
    rowX2 --> unitB
    rowY1 --> unitB

    testerX1["Asset: Tester X1"]:::darkgreen
    rowX1 --> testerX1
    testerY3["Asset: Tester Y3"]:::darkgreen
    rowY3 --> testerY3
    testerX2["Asset: Tester X2"]:::darkgreen
    rowX2 --> testerX2
    testerY1["Asset: Tester Y1"]:::darkgreen
    rowY1 --> testerY1

    click unitA,unitB,testerX1,testerY3,testerX2,testerY1 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
```

#### Exception: Incompatible channel names

In some systems, testers may report the same [channels](https://dev.nominal.io/platform/reference/glossary.md#channel "A named signal: a series of measurements or computed values, such as voltage, pressure, or system state.") with different meaning. For example, imagine that tester category X reports `voltage` meaning supply voltage while tester category Y reports `voltage` meaning battery voltage. Nothing stops the data from being stored in a single dataset, but it may be challenging to distinguish between the two data values.

To keep the two apart, use one dataset per tester category, one for category X and another for category Y. The drawback is that each unit’s asset then attaches several datasets, each under a different [ref name](https://dev.nominal.io/platform/reference/glossary.md#ref-name "The name an asset or run gives each data source attached to it, such as default. Workbooks and checklists find channels by ref name and channel name.").

### Production tester monitoring

The sections above store and compare unit data. The same dataset can also show how the tester itself behaves over time.

An advantage of the tagged approach is that *all* physical measurements a tester produces, whatever the unit, are in one dataset. By default, give each tester its own dataset, because that makes filtering by tester easier. When testers share a dataset, tag each point with its tester instead, such as `tester=X1`.

A tester asset with the unfiltered dataset attached tracks the tester’s signals over time. Those signals mix tester and unit effects, so a drifting voltage could come from the tester’s calibration or from changes in the units.

To implement this extension, create a tester asset and attach the dataset with a common [ref name](https://dev.nominal.io/platform/reference/glossary.md#ref-name "The name an asset or run gives each data source attached to it, such as default. Workbooks and checklists find channels by ref name and channel name.") but no filter.

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef red fill:#fee2e2,stroke:#b91c1c,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937
    classDef blue fill:#e0f2fe,stroke:#0369a1,color:#1f2937
    classDef darkred fill:#fecaca,stroke:#b91c1c,color:#1f2937
    classDef darkgreen fill:#bbf7d0,stroke:#15803d,color:#1f2937

    subgraph dataset1["Dataset"]
        unitA["12:00:00 · 100.0 · UUT=A<br>...<br>12:29:59 · 123.4 · UUT=A"]:::purple
        unitB["14:00:00 · 100.0 · UUT=B<br>...<br>14:29:59 · 432.1 · UUT=B"]:::purple
    end
    style dataset1 fill:#f5f7ff,stroke:#4338ca,color:#1f2937

    asset1["Asset: UUT A"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=A"| asset1

    asset2["Asset: UUT B"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=B"| asset2

    asset3["Asset: Tester X1"]:::darkgreen
    dataset1 -->|"ref name=default<br>no filter"| asset3

    workbook1["Workbook for<br>Tester analysis"]:::blue
    asset3 --> workbook1

    click asset1,asset2,asset3 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
    click workbook1,workbook2 href "../../../workbooks/" "Workbooks"
```

#### Multi-tester monitoring

In the [multi-tester extension](https://dev.nominal.io/platform/data-model/organization/tagged-data.md#multi-tester-line), monitoring each tester can still be performed with tags.

```mermaid
---
config:
  layout: elk

---
flowchart BT
    classDef green fill:#dcfce7,stroke:#15803d,color:#1f2937
    classDef purple fill:#e0e7ff,stroke:#4338ca,color:#1f2937
    classDef blue fill:#e0f2fe,stroke:#0369a1,color:#1f2937
    classDef darkgreen fill:#bbf7d0,stroke:#15803d,color:#1f2937

    subgraph dataset1["Dataset: Production line"]
        rowA["12:00:00 · 100.0 · UUT=A · tester=X1<br>...<br>14:29:59 · 123.4 · UUT=A · tester=Y3"]:::purple
        rowB["13:00:00 · 100.0 · UUT=B · tester=X1<br>...<br>15:29:59 · 432.1 · UUT=B · tester=Y3"]:::purple
    end
    style dataset1 fill:#f5f7ff,stroke:#4338ca,color:#1f2937

    asset1["Asset: UUT A"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=A"| asset1

    asset2["Asset: UUT B"]:::green
    dataset1 -->|"ref name=default<br>filter UUT=B"| asset2

    tester1["Asset: Tester X1"]:::darkgreen
    dataset1 -->|"ref name=default<br>filter tester=X1"| tester1

    workbook1["Workbook for<br>Tester analysis<br><br>Points at one tester at a time"]:::blue
    tester1 -->|"pointing at X1"| workbook1

    click asset1,asset2,tester1,tester2 href "../../#asset" "Asset"
    click dataset1 href "../../#time-series-data" "Time-series data"
    click workbook1 href "../../../workbooks/" "Workbooks"
```

In this diagram, filtering a dataset on `tester=X1` still allows you to see every piece of data acquired/generated by the test system.

### Combining with the default approach

Use this extension when each unit also has data of its own, such as an onboard recorder or tests on other testers. Give each unit a dataset of its own under a second [ref name](https://dev.nominal.io/platform/reference/glossary.md#ref-name "The name an asset or run gives each data source attached to it, such as default. Workbooks and checklists find channels by ref name and channel name."), alongside its filtered link to the tester’s dataset. See [Combining the default and tagged approaches](https://dev.nominal.io/platform/data-model/organization/combined.md).

### Units built from changing components

Use this extension when components of a unit are tested on their own and move from one unit to another. Tag each point with both the component and the unit it’s in, and give each unit and each component an asset filtered on its own tag. See [Complex and nested assets](https://dev.nominal.io/platform/data-model/organization/assemblies.md).
