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, 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.
The biggest advantage of this approach is that you don’t manage a dataset for every unit. Instead, you have a dataset per tester, or even one for a whole production line. Organizing data this way also lets you review all of the data a tester gathered, as described in production tester monitoring. 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 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:
Time |
Measurement |
Tag |
|---|---|---|
12:00:00 |
100.0 |
|
12:00:00 |
100.0 |
|
12:00:01 |
200.0 |
|
12:00:01 |
200.0 |
|
12:00:02 |
300.0 |
|
12:00:02 |
300.0 |
|
12:00:03 |
200.0 |
|
12:00:03 |
200.0 |
|
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:
---
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 attaches the dataset through a data scope that filters the dataset on that unit’s tag, so each asset sees only its own unit’s points.
---
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.
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"})
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);
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);
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:
---
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:
---
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 for each test on these assets, so workbooks can compare individual tests on one unit or across units.
---
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. A production line 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.
Time |
Measurement |
Tags |
|---|---|---|
12:00:00 |
100.0 |
|
12:00:00 |
100.0 |
|
12:00:01 |
200.0 |
|
12:00:01 |
200.0 |
|
12:00:02 |
300.0 |
|
12:00:02 |
300.0 |
|
12:00:03 |
200.0 |
|
12:00:03 |
200.0 |
|
---
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 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.
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 but no filter.
---
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, monitoring each tester can still be performed with tags.
---
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, alongside its filtered link to the tester’s dataset. See Combining the default and tagged approaches.
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.