# 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**

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

**TypeScript**

```typescript
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

**Python**

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.

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

**TypeScript**

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:

```typescript
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

**Python**

Series support standard Python indexing with bracket notation, including negative indices:

```python
import synnax as sy

series = sy.Series([1, 2, 3, 4, 5])

print(series[0])   # 1
print(series[-1])  # 5
```

**TypeScript**

The `at` method provides element access with support for negative indexing:

```typescript
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` 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**

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

**TypeScript**

```typescript
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

**Python**

#### Length

Use the built-in `len()` function to get the number of samples:

```python
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:

```python
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:

```python
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
```

**TypeScript**

#### Length

The `length` property returns the number of samples in the series:

```typescript
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:

```typescript
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:

```typescript
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

**Python**

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

**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.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:

**Python**

```python
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)
```

**TypeScript**

```typescript
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

**Python**

#### 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:

```python
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]
```

**TypeScript**

#### 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:

```typescript
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:

```typescript
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:

```typescript
const obj = frame.at(3, true); // Throws an error
```

### 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:

```typescript
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>`:

```typescript
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:

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