ReferenceClientSeries and Frames

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:

Python

TypeScript

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"},
])

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

Python

TypeScript

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

Accessing Data Elements

Python

TypeScript

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 Time Range Property

Whenever you read a series from Synnax, it will have a time_range property that represents the time range occupied by the samples in the Series. This property can be useful for getting a high-level understanding of when the samples were recorded without needing to query an index channel.

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:

Python

TypeScript

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

Other Useful Properties

Python

TypeScript

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

Frames

Python

TypeScript

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

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:

Python

TypeScript

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)

Accessing Frame Data

Python

TypeScript

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]

Type-Safe Access (TypeScript Only)

TypeScript

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");