from lets_plot import LetsPlot
LetsPlot.setup_html()
import numpy as np
from xaitimesynth import (
TimeSeriesBuilder,
manual,
gaussian_noise,
plot_component,
plot_components,
generate_cylinder_bell_funnel,
)
Custom Data Generation¶
xaitimesynth contains a fixed set of data generating methods (gaussian_noise, peak, seasonal, etc.). However, sometimes you need to generate data with a generator that's not included in the package.
The manual() component solves this. You write a plain Python function that produces a NumPy array, wrap it in manual(generator=...), and pass it to the builder just like any built-in component.
Generator function signature¶
Custom generators must follow this signature:
def my_generator(
n_timesteps: int, # total series length (context)
rng, # np.random.RandomState for reproducibility
length: int, # actual output length (may differ from n_timesteps for features)
**kwargs, # any extra parameters you define
) -> np.ndarray: # 1-D array of shape (length,)
...
All three standard parameters are passed as keyword arguments by the builder, so their order in your function definition is flexible.
n_timesteps— the full series length. Use this to scale frequencies or amplitudes relative to the whole series.rng— the builder's random state. Draw from it (e.g.rng.randn()) so the dataset is reproducible whenrandom_stateis set.length— for features placed in a window, this is the window length, not the full series. Your array must have exactlylengthelements.- **
**kwargs** — any extra parameters you pass tomanual()are forwarded here.
Previewing a generator with plot_component()¶
Before wiring a generator into a full dataset, use plot_component() to inspect its shape in isolation.
manual() works in two modes: pass a pre-computed array with values=, or a callable with generator=.
# Mode 1: pre-computed values
cosine_values = np.cos(np.linspace(0, 2 * np.pi, 100))
plot_component(component_type="manual", values=cosine_values, n_timesteps=100)
# Mode 2: generator function — output length is passed as `length`
def cosine_generator(n_timesteps, rng, length, **kwargs):
return np.cos(np.linspace(0, 2 * np.pi, length))
plot_component(component_type="manual", generator=cosine_generator, n_timesteps=100)
The same generator function drops straight into manual() for use in a dataset:
dataset_cosine = (
TimeSeriesBuilder(
n_timesteps=100, n_samples=50, normalization="none", random_state=0
)
.for_class(0)
.add_signal(gaussian_noise(sigma=0.2))
.for_class(1)
.add_signal(gaussian_noise(sigma=0.2))
.add_feature(manual(generator=cosine_generator), start_pct=0.2, end_pct=0.6)
.build()
)
plot_components(dataset_cosine)
Example: Cylinder-Bell-Funnel¶
The Cylinder-Bell-Funnel (CBF) benchmark (Saito, 2000) is a three-class dataset where each class shares the same Gaussian noise background but has a different pattern added inside a random window [a, b]:
| Class | Pattern inside [a, b] |
|---|---|
| Cylinder (0) | Constant level shift of amplitude (6 + η) |
| Bell (1) | Linearly increasing ramp: 0 → (6 + η) |
| Funnel (2) | Linearly decreasing ramp: (6 + η) → 0 |
Here η ~ N(0, 1) is drawn per sample. The window length is sampled from Uniform[32, 96] timesteps and placed at a random start position.
None of these shapes are built into xaitimesynth, so manual() is the right tool. The generators use rng to draw η so amplitude varies per sample while remaining reproducible.
# Per-sample generators — eta is drawn from rng so amplitude varies per sample
def cylinder_generator(n_timesteps, rng, length, **kwargs):
eta = rng.randn()
return np.full(length, 6.0 + eta) # constant plateau
def bell_generator(n_timesteps, rng, length, **kwargs):
eta = rng.randn()
return np.linspace(0, 6.0 + eta, length) # ramp up
def funnel_generator(n_timesteps, rng, length, **kwargs):
eta = rng.randn()
return np.linspace(6.0 + eta, 0, length) # ramp down
dataset_manual = (
TimeSeriesBuilder(
n_timesteps=128, n_samples=300, normalization="none", random_state=42
)
.for_class(0) # Cylinder
.add_signal(gaussian_noise(mu=0, sigma=1))
.add_feature(
manual(generator=cylinder_generator),
random_location=True,
length_pct=(0.25, 0.75), # window length ~ Uniform[32, 96] timesteps
)
.for_class(1) # Bell
.add_signal(gaussian_noise(mu=0, sigma=1))
.add_feature(
manual(generator=bell_generator), random_location=True, length_pct=(0.25, 0.75)
)
.for_class(2) # Funnel
.add_signal(gaussian_noise(mu=0, sigma=1))
.add_feature(
manual(generator=funnel_generator),
random_location=True,
length_pct=(0.25, 0.75),
)
.build()
)
plot_components(dataset_manual)
Convenience wrapper¶
The builder setup above is wrapped in generate_cylinder_bell_funnel(), a ready-made function that produces the same dataset with one call.
Note, however, that this implementation differs slightly from the original implementation and this is just an approximation of the original. In this implementation, the "features" can start at time point 0, whereas in the original implementation, the window never starts before timestep 16. So if you want a 100% faithful implementation, you should use another package or generate the data yourself.
dataset = generate_cylinder_bell_funnel(random_state=42)
plot_components(dataset)