Series and Frames
Understanding the core data structures for working with telemetry data in Synnax.
This guide covers the fundamental data structures used in Synnax: Series (typed arrays) and Frames (multi-channel containers). These are core types returned by read operations and used for write operations.
Series
A series is a strongly typed array of data samples. A series can contain any data you want it to, but in the context of Synnax, it represents a set of contiguous samples from a single channel.
Constructing a Series
Here are a few simple ways to construct a series:
import numpy as np
# Construct a series from a list of samples. The data type is inferred
# from the first element - in this case int64. All elements must be the
# same type; mixed types won't throw an error but will cause undefined behavior.
series = sy.Series([1, 2, 3, 4, 5])
# Manually specify the data type of the series
series = sy.Series([1, 2, 3, 4, 5], data_type=sy.DataType.INT32)
# Construct a series from a numpy array. The type of the series
# will be inferred from the numpy array's dtype.
series = sy.Series(np.array([1, 2, 3, 4, 5], dtype=np.int64))
# Construct a series from a list of strings. The data type is inferred
# from the first element - in this case string.
series = sy.Series(["apple", "banana", "cherry"])
# Construct a series from dictionaries. The data type is inferred
# from the first element - in this case json.
series = sy.Series([
{"red": "cherry"},
{"yellow": "banana"},
{"orange": "orange"},
]) import { Series } from "@synnaxlabs/client";
// Construct a series from an array of numbers. The data type is inferred
// from the first element - in this case float64. All elements must be the
// same type; mixed types won't throw an error but will cause undefined behavior.
const series = new Series([1, 2, 3, 4, 5]);
// Manually specify the data type of the series
const series = new Series({ data: [1, 2, 3, 4, 5], dataType: "float32" });
// Construct a series from a Float32Array. This is the most efficient way to
// construct a series from a large amount of data.
const series = new Series(new Float32Array([1, 2, 3, 4, 5]));
// Construct a series from an array of strings. The data type is inferred
// from the first element - in this case string.
const series = new Series(["apple", "banana", "cherry"]);
// Construct a series from an array of objects. The data type is inferred
// from the first element - in this case json.
const series = new Series([
{ red: "cherry" },
{ yellow: "banana" },
{ orange: "orange" },
]); The values passed to a series cannot have different data types. The constructor will not throw an error, as validating data types is an expensive operation, but the series will not behave as expected.
Native Array Interop
The Series class is directly compatible with numpy, and supports all of the operations
possible on a numpy array without needing to convert back and forth.
import matplotlib.pyplot as plt
import numpy as np
import synnax as sy
series = sy.Series([1, 2, 3, 4, 5])
# Use numpy functions directly on a series
print(np.mean(series))
# Convert the series to a numpy array
numpy_array = series.to_numpy()
# Convert the series to a numpy array using np.array
numpy_array = np.array(series)
# Pass directly into libraries like matplotlib
x_data = sy.Series([1, 2, 3, 4, 5])
y_data = sy.Series([1, 4, 9, 16, 25])
plt.plot(x_data, y_data)
plt.show() Series implement the Iterable interface, so you can use the spread operator to convert
a series to a Javascript array, or use the Array.from method:
import { Series } from "@synnaxlabs/client";
// Using spread operator
const series = new Series([1, 2, 3, 4, 5]);
const jsArray = [...series];
console.log(jsArray); // [ 1, 2, 3, 4, 5 ]
// Using Array.from
const jsArray2 = Array.from(series);
console.log(jsArray2); // [ 1, 2, 3, 4, 5 ]
// This method will also work for
// `json` and `string` encoded series
const series = new Series([
{
a: "one",
b: "two",
c: "three",
},
]);
const jsArray = [...series];
console.log(jsArray); // [ { a: 'one', b: 'two', c: 'three' } ] Accessing Data Elements
Series support standard Python indexing with bracket notation, including negative indices:
import synnax as sy
series = sy.Series([1, 2, 3, 4, 5])
print(series[0]) # 1
print(series[-1]) # 5 The at method provides element access with support for negative indexing:
import { Series } from "@synnaxlabs/client";
const series = new Series([1, 2, 3, 4, 5]);
console.log(series.at(0)); // 1
console.log(series.at(-1)); // 5 The Time Range Property
Whenever you read a series from Synnax, it will have a time_range
The start field represents the timestamp for the first sample, and the end field
represents a timestamp just after the last sample (start-inclusive, end-exclusive).
It’s also easy to define a time range when constructing a series:
import synnax as sy
start = sy.TimeStamp.now()
series = sy.Series(
[1, 2, 3, 4, 5],
time_range=sy.TimeRange(start=start, end=start + sy.TimeSpan.SECOND * 6),
) import { Series, TimeRange, TimeStamp, TimeSpan } from "@synnaxlabs/client";
const start = TimeStamp.now();
const series = new Series({
data: [1, 2, 3, 4, 5],
timeRange: new TimeRange({ start, end: start.add(TimeSpan.seconds(6)) }),
}); Other Useful Properties
Length
Use the built-in len() function to get the number of samples:
import synnax as sy
series = sy.Series([1, 2, 3, 4, 5])
print(len(series)) # 5 Data Type
The data_type property returns the data type of the series:
import synnax as sy
series = sy.Series([1, 2, 3, 4, 5])
print(series.data_type) # float64
print(series.data_type == sy.DataType.FLOAT64) # True Max and Min
Since Series is numpy-compatible, use numpy functions directly:
import numpy as np
import synnax as sy
series = sy.Series([1, 2, 3, 4, 5])
print(np.max(series)) # 5
print(np.min(series)) # 1 Length
The length property returns the number of samples in the series:
import { Series } from "@synnaxlabs/client";
const series = new Series([1, 2, 3, 4, 5]);
console.log(series.length); // 5 Data Type
The dataType property returns the data type of the series:
import { DataType, Series } from "@synnaxlabs/client";
const series = new Series([1, 2, 3, 4, 5]);
console.log(series.dataType.toString()); // "float64"
console.log(series.dataType.equals(DataType.FLOAT64)); // true Max, Min, and Bounds
The max, min, and bounds properties return the extrema of numeric series:
import { DataType, Series } from "@synnaxlabs/client";
const series = new Series([1, 2, 3, 4, 5]);
console.log(series.max); // 5
console.log(series.min); // 1
console.log(series.bounds); // { lower: 1, upper: 5 } Frames
A frame is a collection of series from multiple channels. Frames are returned by the
read method of the Synnax data client (client.read), the read method of a
Streamer instance (client.open_streamer), and the value property of an Iterator
instance (client.open_iterator).
A frame is a collection of series from multiple channels. Frames are returned by the
read method of the Synnax data client (client.read), the read method of a
Streamer instance (client.openStreamer), and the value property of an Iterator
instance (client.openIterator).
Constructing a Frame
A frame maps the key or name of a channel to one or more series. Here are a few examples of how to construct a frame:
import pandas as pd
import synnax as sy
# Construct a frame using channel names
frame = sy.Frame({
"channel1": sy.Series([1, 2, 3, 4, 5]),
"channel2": sy.Series([5, 4, 3, 2, 1]),
"channel3": sy.Series([1, 1, 1, 1, 1]),
})
# Construct a frame using channel keys (integers)
frame = sy.Frame({
1: sy.Series([1, 2, 3, 4, 5]),
2: sy.Series([5, 4, 3, 2, 1]),
3: sy.Series([1, 1, 1]), # Series don't need to be the same length
})
# Construct a frame from individual samples
frame = sy.Frame({"ch1": 1, "ch2": 2, "ch3": 3 })
# Construct a frame from a pandas DataFrame
df = pd.DataFrame({
"channel1": [1, 2, 3, 4, 5],
"channel2": [5, 4, 3, 2, 1],
})
frame = sy.Frame(df) import { Frame, Series } from "@synnaxlabs/client";
// Construct a frame using channel names
const frame = new Frame({
channel1: new Series([1, 2, 3, 4, 5]),
channel2: new Series([5, 4, 3, 2, 1]),
channel3: new Series([1, 1, 1, 1, 1]),
});
// Construct a frame using channel keys (integers)
const frame = new Frame({
1: new Series([1, 2, 3, 4, 5]),
2: new Series([5, 4, 3, 2, 1]),
3: new Series([1, 1, 1]), // Series don't need to be the same length
});
// Construct a frame with multiple series for a single channel
const frame = new Frame({
channel1: [new Series([1, 2, 3, 4, 5]), new Series([6, 7, 8, 9, 10])],
}); Accessing Frame Data
The get method
The easiest way to access data from a frame is using bracket notation or the get
method. Both return a MultiSeries object, which wraps multiple Series instances but
behaves like a single series:
import synnax as sy
frame = sy.Frame({
"channel1": [sy.Series([1, 2]), sy.Series([3, 4, 5])],
"channel2": sy.Series([5, 4, 3, 2, 1]),
"channel3": sy.Series([1, 1, 1, 1, 1]),
})
multi_series: sy.MultiSeries = frame["channel1"]
# Access a value
print(multi_series[0]) # 1
# Access a value from a specific series
print(multi_series.series[0][0]) # 1
# Convert to a Python list
py_list = list(multi_series)
print(py_list) # [1, 2, 3, 4, 5] The get method
The easiest way to access data from a frame is to use the get method. This method
returns a MultiSeries object, which wraps multiple Series instances but behaves like
a single series:
import { Frame, MultiSeries, Series } from "@synnaxlabs/client";
const frame = new Frame({
channel1: [new Series([1, 2]), new Series([3, 4, 5])],
channel2: new Series([5, 4, 3, 2, 1]),
channel3: new Series([1, 1, 1, 1, 1]),
});
const multiSeries: MultiSeries = frame.get("channel1");
// Access a value
console.log(multiSeries.at(0)); // 1
// Access a value from a specific series
console.log(multiSeries.series[0].at(0)); // 1
// Convert to a Javascript array
const jsArray = [...multiSeries];
console.log(jsArray); // [ 1, 2, 3, 4, 5 ] The at method
The at method can be used to access a JavaScript object containing a single value for
each channel in the frame:
const frame = new Frame({
channel1: new Series([1, 2, 3, 4, 5]),
channel2: new Series([5, 4, 3, 2, 1]),
channel3: new Series([1, 1]),
});
const obj = frame.at(3);
console.log(obj); // { channel1: 4, channel2: 2, channel3: undefined } If you set the required parameter to true, the method will throw an error if any of
the channels are missing a value at the given index:
const obj = frame.at(3, true); // Throws an error Type-Safe Access (TypeScript Only)
The at method returns a TelemValue type, which is a union of all possible data types
a series can contain. This can make type-safe code difficult:
import { Series, TelemValue } from "@synnaxlabs/client";
const series = new Series([1, 2, 3, 4, 5]);
const v: TelemValue = series.at(0); // Could be number, string, object, etc. If you know the series contains a specific JavaScript type (number, string,
object, bigint), use the as method to get a typed Series<T>:
const series = new Series([1, 2, 3, 4, 5]);
const typedSeries: Series<number> = series.as("number");
const v: number = typedSeries.at(0); // Guaranteed to be a number The as method validates that the series data type is compatible with the requested
JavaScript type. If the types are incompatible, it throws an error:
const stringSeries = new Series(["apple", "banana"]);
// Throws: "cannot convert series of type string to number"
stringSeries.as("number");
const floatSeries = new Series([1.5, 2.5, 3.5]);
// Throws: "cannot convert series of type float64 to bigint"
floatSeries.as("bigint");