DataStream Class

This tutorial covers single-trace analysis with the DataStream object on real gyrokinetic turbulence data (one GX run and one CGYRO run).

It demonstrates the core single-trace features:

  • Loading & plotting a raw time-series,

  • Stationarity testing (Augmented Dickey-Fuller),

  • Trimming to the steady-state portion (two equivalent calling styles),

  • Effective Sample Size (ESS) and autocorrelation-aware statistics,

  • saving / re-loading a trimmed stream and handling non-stationary inputs.

For analysing multiple runs together, see the Ensemble Analysis guide; for noisy signals where stationarity is hard to assess, see the RobustWorkflow guide.

The GX trim/statistics parameters (method="threshold", window_size=50, start_time=100, threshold=0.1, method="non-overlapping") and the CGYRO parameters (method="threshold", window_size=100, threshold=0.1) follow the QUENDS analysis notebooks.

Import QUENDS

import glob
import os
import tempfile
from pathlib import Path

import quends as qnds
from quends.base.trim import TrimDataStreamOperation, build_trim_strategy
from quends.postprocessing.loader import JsonLoader
from quends.postprocessing.writer import JsonWriter


def example_data_dir() -> Path:
    """Find the shared example data directory during script or gallery runs."""
    starts = []
    if "__file__" in globals():
        starts.append(Path(__file__).resolve())
    starts.append(Path.cwd().resolve())

    for start in starts:
        for parent in [start, *start.parents]:
            for candidate in (parent / "examples" / "data", parent / "data"):
                if candidate.is_dir():
                    return candidate
    raise FileNotFoundError("Could not locate examples/data")


COL = "HeatFlux_st"  # the heat-flux observable carried by the GX files
DATA_DIR = example_data_dir()
plotter = qnds.Plotter()

GX Data Analysis

The GX data ships in examples/data/gx and the CGYRO data in examples/data/cgyro.

Single Trace

Input case: a single simulation output. We analyse one GX run.

Data Loading

gx_files = sorted(glob.glob(str(DATA_DIR / "gx" / "ensemble" / "tprim_2_5_*.out.csv")))
single_path = gx_files[0]
ds = qnds.from_csv(single_path, COL)
print("loaded:", single_path, "| variables:", ds.variables(), "| rows:", len(ds))
ds.head()
loaded: /home/runner/work/quends/quends/examples/data/gx/ensemble/tprim_2_5_a.out.csv | variables: Index(['time', 'HeatFlux_st'], dtype='object') | rows: 201
time HeatFlux_st
0 0.020072 0.000003
1 2.027322 0.000154
2 4.034571 0.000196
3 6.041821 0.000236
4 8.049070 0.000298


Plotting the raw trace

The raw trace shows the initial transient followed by a noisy steady state.

plot = plotter.trace_plot(ds, [COL], show=True)
Time Series — Datastream, HeatFlux_st

Stationary Check

The Augmented Dickey-Fuller test reports whether the (raw) signal already looks stationary.

print("is_stationary (raw):", ds.is_stationary(COL))
is_stationary (raw): {'HeatFlux_st': True}

Trimming data to obtain the steady-state portion

QUENDS offers two equivalent ways to trim. First, the explicit strategy/operation pattern from quends.base.trim – useful when you want to build a strategy once and reuse it:

strat = build_trim_strategy(
    method="threshold", window_size=50, start_time=100, threshold=0.1
)
trimmed = TrimDataStreamOperation(strategy=strat)(ds, column_name=COL)
print("strategy/operation -> sss_start:", trimmed.trim_metadata.get("sss_start"))
strategy/operation -> sss_start: 240.88999613866895

Second, the convenience wrapper DataStream.trim – the same canonical path in one call. Both produce the identical steady-state start:

trimmed = ds.trim(method="threshold", threshold=0.1, window_size=50, start_time=100)
ss_start = trimmed.trim_metadata.get("sss_start")
print("ds.trim            -> sss_start:", ss_start)
trimmed.head()
ds.trim            -> sss_start: 240.88999613866895
time HeatFlux_st
0 240.889996 7.962383
1 242.897246 7.946047
2 244.904495 7.989000
3 246.911744 8.078041
4 248.918994 8.056113


Plot of the trace with the detected steady-state start

Plot using the exact steady-state start that the trim found (trimmed.trim_metadata["sss_start"]) via steady_state_plot – so the annotation matches the trim instead of re-detecting with other parameters. The post-steady-state mean is drawn over the steady region. (For a std / QuantileTrimStrategy trim you can pass show_std_bands=True to add the ±1/2/3 std bands; threshold-based trims are shown without them.)

plot = plotter.steady_state_plot(ds, [COL], steady_state_start=ss_start, show=True)
Steady-State (Manual) — Datastream, HeatFlux_st

Equivalently, steady_state_automatic_plot re-detects the start, but it must be given the same trim parameters to match:

plot = plotter.steady_state_automatic_plot(
    ds,
    [COL],
    method="threshold",
    threshold=0.1,
    batch_size=50,
    start_time=100,
    show=True,
)
Steady-State Detection — Datastream, HeatFlux_st

Effective Sample Size

Autocorrelation means the trimmed series holds fewer independent samples than rows; ESS quantifies that.

print("ESS (trimmed):", trimmed.effective_sample_size())
/home/runner/work/quends/quends/src/quends/base/utils.py:300: UserWarning: The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.
  warnings.warn(
ESS (trimmed): {'results': {'HeatFlux_st': 6}}

Statistical Analysis

compute_statistics returns the mean, an autocorrelation-corrected uncertainty, a confidence interval, and the block/window diagnostics.

stats = trimmed.compute_statistics(method="non-overlapping")
print(stats)
qnds.Exporter().display_dataframe(stats)
StatsResult({'HeatFlux_st': {'mean': 7.72589417051282, 'mean_uncertainty': 0.08566767204022449, 'variance': 0.09540635042628899, 'confidence_interval': (7.55798553331398, 7.89380280771166), 'standard_deviation': 0.3088791841906621, 'pm_std': (7.640226498472596, 7.811561842553044), 'effective_sample_size': 6, 'window_size': 6, 'n_short_averages': 13, 'ess_blocks': 4.701371580734517, 'se_effective_n': 13.0, 'se_method': 'iid_blocks', 'independence_status': 'independent', 'independent': True, 'ljungbox_lags': [5, 10], 'ljungbox_pvalues': [0.058791089981722916, 0.1174344029382242], 'ljungbox_pvalue': 0.058791089981722916, 'ci_method': 'normal', 'confidence_level': 0.95, 'metadata': {'warnings': ['The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.']}}}, metadata={'estimator': 'single', 'columns': ['HeatFlux_st'], 'total_samples': 81, 'schema_version': '1.0', 'warnings': [{'column': 'HeatFlux_st', 'message': 'The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.'}]})
                                                             HeatFlux_st
mean                                                            7.725894
mean_uncertainty                                                0.085668
variance                                                        0.095406
confidence_interval                 (7.55798553331398, 7.89380280771166)
standard_deviation                                              0.308879
pm_std                            (7.640226498472596, 7.811561842553044)
effective_sample_size                                                  6
window_size                                                            6
n_short_averages                                                      13
ess_blocks                                                      4.701372
se_effective_n                                                      13.0
se_method                                                     iid_blocks
independence_status                                          independent
independent                                                         True
ljungbox_lags                                                    [5, 10]
ljungbox_pvalues              [0.058791089981722916, 0.1174344029382242]
ljungbox_pvalue                                                 0.058791
ci_method                                                         normal
confidence_level                                                    0.95
metadata               {'warnings': ['The computed signal decorrelati...

Save the trimmed stream

The postprocessing layer can serialise a DataStream (data + operation history) to JSON with JsonWriter, and read it back with JsonLoader.

trimmed_path = os.path.join(tempfile.mkdtemp(), "trimmed_gx.json")
JsonWriter(trimmed_path).save(trimmed)
[JsonWriter] saved -> /tmp/tmpg2day21n/trimmed_gx.json

Other input scenarios

Already-trimmed data

Load the trimmed stream we just saved and re-run the pipeline on it. Because the data is already in steady state, re-trimming doubles as a check on how the different steady-state criteria behave.

reloaded = JsonLoader(trimmed_path).load()
print("reloaded rows:", len(reloaded), "| variables:", list(reloaded.data.columns))
reloaded rows: 81 | variables: ['time', 'HeatFlux_st']

It tests as stationary – there is no transient left to remove.

print("reloaded is_stationary:", reloaded.is_stationary(COL))
reloaded is_stationary: {'HeatFlux_st': False}

Idempotent re-trim (std / Quantile)

The std / QuantileTrimStrategy criterion is idempotent here: re-trimming returns the exact same data, so it recognises the whole reloaded series as steady state. We annotate it with steady_state_automatic_plot using the same std parameters as the re-trim – the detected start sits at the very beginning, and because the method is std the ±1/2/3 std bands are drawn over the steady region.

re_std = reloaded.trim(method="std", window_size=50)
identical = re_std.data.reset_index(drop=True).equals(
    reloaded.data.reset_index(drop=True)
)
print("std re-trim rows:", len(re_std), "| identical to reloaded:", identical)
plot = plotter.steady_state_automatic_plot(
    reloaded, [COL], method="std", batch_size=50, show=True
)
Steady-State Detection — Datastream, HeatFlux_st
std re-trim rows: 0 | identical to reloaded: False

Non-idempotent re-trim (threshold)

The threshold criterion is not idempotent: re-scanning the already-steady series (now with start_time=0) still shaves off some leading points, and how many depends on window_size and threshold – a larger window or smaller threshold is stricter and removes more.

for w, th in [(20, 0.1), (50, 0.1), (100, 0.1), (50, 0.2)]:
    rt = reloaded.trim(method="threshold", window_size=w, threshold=th, start_time=0)
    start = rt.trim_metadata.get("sss_start")
    start_text = f"{start:.1f}" if start is not None else "None"
    print(f"threshold w={w:3d} th={th}: rows={len(rt):4d} start={start_text}")
plot = plotter.steady_state_automatic_plot(
    reloaded,
    [COL],
    method="threshold",
    batch_size=50,
    threshold=0.1,
    start_time=0,
    show=True,
)
Steady-State Detection — Datastream, HeatFlux_st
threshold w= 20 th=0.1: rows=   0 start=None
threshold w= 50 th=0.1: rows=   0 start=None
threshold w=100 th=0.1: rows=   0 start=None
threshold w= 50 th=0.2: rows=   0 start=None

Sensitive re-trim (rolling variance)

The rolling_variance criterion is even more sensitive: a small threshold rejects the entire series (0 rows), so it needs a larger threshold on this already-steady data.

for w, th in [(50, 0.1), (50, 0.5), (50, 1.0)]:
    rt = reloaded.trim(
        method="rolling_variance", window_size=w, threshold=th, start_time=0
    )
    print(f"rolling_variance w={w} th={th}: rows={len(rt):4d}")
plot = plotter.steady_state_automatic_plot(
    reloaded,
    [COL],
    method="rolling_variance",
    batch_size=50,
    threshold=1.0,
    start_time=0,
    show=True,
)
Steady-State Detection — Datastream, HeatFlux_st
rolling_variance w=50 th=0.1: rows=   0
rolling_variance w=50 th=0.5: rows=   0
rolling_variance w=50 th=1.0: rows=   0

The statistics of the reloaded stream reproduce the original trim.

print("reloaded ESS  :", reloaded.effective_sample_size())
print("reloaded stats:", reloaded.compute_statistics(method="non-overlapping"))
/home/runner/work/quends/quends/src/quends/base/utils.py:300: UserWarning: The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.
  warnings.warn(
reloaded ESS  : {'results': {'HeatFlux_st': 6}}
reloaded stats: StatsResult({'HeatFlux_st': {'mean': 7.72589417051282, 'mean_uncertainty': 0.08566767204022449, 'variance': 0.09540635042628899, 'confidence_interval': (7.55798553331398, 7.89380280771166), 'standard_deviation': 0.3088791841906621, 'pm_std': (7.640226498472596, 7.811561842553044), 'effective_sample_size': 6, 'window_size': 6, 'n_short_averages': 13, 'ess_blocks': 4.701371580734517, 'se_effective_n': 13.0, 'se_method': 'iid_blocks', 'independence_status': 'independent', 'independent': True, 'ljungbox_lags': [5, 10], 'ljungbox_pvalues': [0.058791089981722916, 0.1174344029382242], 'ljungbox_pvalue': 0.058791089981722916, 'ci_method': 'normal', 'confidence_level': 0.95, 'metadata': {'warnings': ['The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.']}}}, metadata={'estimator': 'single', 'columns': ['HeatFlux_st'], 'total_samples': 81, 'schema_version': '1.0', 'warnings': [{'column': 'HeatFlux_st', 'message': 'The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.'}]})

Non-stationary / failed steady-state detection

Not every channel reaches steady state. Several GX observables in tprim_2_4 are non-stationary – here Phi2_t (the electrostatic potential energy), which keeps drifting. (Wg_st was suggested but tests as stationary in this run, so we use a genuinely non-stationary column.) The Augmented Dickey-Fuller test flags it, and a plain trim cannot find a clean steady state. For signals like this, use the RobustWorkflow guide.

ds_ns = qnds.from_csv(DATA_DIR / "gx" / "tprim_2_4.out.csv", "Phi2_t")
print("Phi2_t is_stationary:", ds_ns.is_stationary("Phi2_t"))
plot = plotter.trace_plot(ds_ns, ["Phi2_t"], show=True)
ns_trim = ds_ns.trim(method="threshold", threshold=0.1, window_size=50, start_time=100)
print("Phi2_t trim_metadata:", ns_trim.trim_metadata)
Time Series — Datastream, Phi2_t
Phi2_t is_stationary: {'Phi2_t': False}
Phi2_t trim_metadata: {'message': "Column 'Phi2_t' is not stationary. Steady-state trimming requires stationary data."}

UQ Analysis

Convenience accessors return just the piece you need from the trimmed data.

Other statistical methods

stats = trimmed.compute_statistics(column_name=COL, method="sliding")
col_stats = stats[COL]
print("mean:", col_stats["mean"])
print("mean uncertainty:", col_stats["mean_uncertainty"])
print("confidence interval:", col_stats["confidence_interval"])
/home/runner/work/quends/quends/src/quends/base/utils.py:300: UserWarning: The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.
  warnings.warn(
mean: 7.72589417051282
mean uncertainty: 0.08566767204022449
confidence interval: (7.55798553331398, 7.89380280771166)

Cumulative statistics track how the estimate stabilises as more samples are included.

cumulative = trimmed.cumulative_statistics()
qnds.Exporter().display_dataframe(cumulative)
                                                              HeatFlux_st
cumulative_mean         [8.0019601, 8.025555966666667, 8.0576797277777...
cumulative_uncertainty  [nan, 0.03336959465594852, 0.06043652047828679...
standard_error          [nan, 0.023595866666667575, 0.0348930413670232...
window_size                                                             6

additional_data exposes the underlying block diagnostics.

print(trimmed.additional_data(method="sliding"))
/home/runner/work/quends/quends/src/quends/base/utils.py:300: UserWarning: The computed signal decorrelation time is large compared to the max lag in the computation of the autocorrelation. Results may be inaccurate. Estimated tau_int=15.65, nlags=20.
  warnings.warn(
{'HeatFlux_st': {'A_est': 0.003585911287617663, 'p_est': 0.59836635833237, 'n_current': 76, 'current_sem': 0.00026864821628132983, 'target_sem': 0.00024178339465319687, 'n_target': 90.6325681086028, 'additional_samples': 15, 'window_size': 6}}

CGYRO Data Analysis

The same workflow applies to CGYRO output; here the observable is Q_D/Q_GBD and the trim parameters follow the CGYRO notebook (method="threshold", window_size=100, threshold=0.1).

CG = "Q_D/Q_GBD"
cg = qnds.from_csv(DATA_DIR / "cgyro" / "output_nu0_50.csv", CG)
print("cgyro rows:", len(cg))
cg.head()
cgyro rows: 1748
time Q_D/Q_GBD
0 0.5 0.003355
1 1.0 0.003314
2 1.5 0.003160
3 2.0 0.002480
4 2.5 0.002004


Raw trace and stationarity check.

plot = plotter.trace_plot(cg, [CG], show=True)
print("cgyro is_stationary (raw):", cg.is_stationary(CG))
Time Series — Datastream, Q_D/Q_GBD
cgyro is_stationary (raw): {'Q_D/Q_GBD': True}

Trim to the steady-state portion and plot it at the detected start.

cg_trimmed = cg.trim(method="threshold", window_size=100, start_time=0.0, threshold=0.1)
cg_ss = cg_trimmed.trim_metadata.get("sss_start")
print("cgyro sss_start:", cg_ss)
plot = plotter.steady_state_plot(cg, [CG], steady_state_start=cg_ss, show=True)
Steady-State (Manual) — Datastream, Q_D/Q_GBD
cgyro sss_start: 85.5

Effective sample size and statistics for the CGYRO run.

print("cgyro ESS:", cg_trimmed.effective_sample_size())
cg_stats = cg_trimmed.compute_statistics(method="non-overlapping")
print(cg_stats)
qnds.Exporter().display_dataframe(cg_stats)
cgyro ESS: {'results': {'Q_D/Q_GBD': 55}}
StatsResult({'Q_D/Q_GBD': {'mean': 26.116732499321174, 'mean_uncertainty': 0.8166885382015654, 'variance': 18.008464547604863, 'confidence_interval': (24.516022964446105, 27.717442034196242), 'standard_deviation': 4.243638126372802, 'pm_std': (25.30004396111961, 26.933421037522738), 'effective_sample_size': 55, 'window_size': 58, 'n_short_averages': 27, 'ess_blocks': 27.0, 'se_effective_n': 27.0, 'se_method': 'iid_blocks', 'independence_status': 'independent', 'independent': True, 'ljungbox_lags': [5, 10], 'ljungbox_pvalues': [0.7626440459670635, 0.36259692585423114], 'ljungbox_pvalue': 0.36259692585423114, 'ci_method': 'normal', 'confidence_level': 0.95}}, metadata={'estimator': 'single', 'columns': ['Q_D/Q_GBD'], 'total_samples': 1578, 'schema_version': '1.0', 'warnings': []})
                                                       Q_D/Q_GBD
mean                                                   26.116732
mean_uncertainty                                        0.816689
variance                                               18.008465
confidence_interval     (24.516022964446105, 27.717442034196242)
standard_deviation                                      4.243638
pm_std                   (25.30004396111961, 26.933421037522738)
effective_sample_size                                         55
window_size                                                   58
n_short_averages                                              27
ess_blocks                                                  27.0
se_effective_n                                              27.0
se_method                                             iid_blocks
independence_status                                  independent
independent                                                 True
ljungbox_lags                                            [5, 10]
ljungbox_pvalues       [0.7626440459670635, 0.36259692585423114]
ljungbox_pvalue                                         0.362597
ci_method                                                 normal
confidence_level                                            0.95

Total running time of the script: (0 minutes 2.212 seconds)

Gallery generated by Sphinx-Gallery


Last update: Aug 11, 2026