Skip to content

Time Alignment

Instruments sample at different times. Stacking them in a figure works with each instrument's own times (Plotting), but comparing values or feeding statistics and machine learning requires aligning times explicitly. SOPRAN offers two ways.

Goal Method
Attach another instrument's values to one instrument's sample times resample_like
Build fixed-width time bins and a table of per-bin values from each instrument time_bins + SampleTable

Match another instrument's times

esa1 = spn.kaguya.esa1.counts.load(time)
conn = spn.kaguya.lmag.magnetic_connection.load(time)

conn_on_esa1 = conn.resample_like(esa1, method="nearest", tolerance="2s")
altitude_on_esa1 = spn.kaguya.orbit.altitude.load(time).resample_like(
    esa1, method="linear", tolerance="10s",
)
method Value used
nearest Sample closest to the target time
previous Last sample at or before the target time
next First sample at or after the target time
linear Linear interpolation between the neighboring samples

Targets with no sample within tolerance become missing. For linear the condition applies to both neighbors, so it never interpolates across data gaps, and it never extrapolates beyond the source.

The function form spn.resample_like(source, target, ...) also accepts pandas, Polars (DataFrame, LazyFrame), and xarray objects in addition to SopranArray. Times are converted to UTC datetime64[ns] before matching. Duplicate source times raise an error. linear on DataFrames interpolates numeric columns only, and pandas DataFrames with duplicate column names are rejected.

Build time bins

bins = spn.time_bins(time, cadence="10s", partial="drop")
bins.to_polars()     # index, start, stop, center, duration_seconds, is_partial

partial decides what happens when the range is not a multiple of cadence.

partial Behavior
"error" (default) Raise an error
"keep" Keep the short last bin
"drop" Drop the short last bin

Bins of unequal width, such as per-event intervals, come from explicit edges with spn.time_bins(edges=[...]). Every bin is half-open, [start, stop).

Build a table of per-bin values

features = (
    spn.SampleTable(bins)
    .add(sza, method="nearest", tolerance="5s")
    .add(wave_power, method="max")
    .add(density, method="median")
    .collect(join="inner")
)
method Value placed in the bin
nearest Sample closest to the bin center (may lie outside the bin; limit with tolerance)
center Sample inside the bin closest to its center
mean, median, max Aggregate inside the bin
first, last First or last sample inside the bin
collect argument Effect
join="outer" (default) Keep every bin; missing values stay missing
join="inner" Keep only bins where every variable has a value
fill=value Fill values left missing by outer
quality_mask=array Drop bins where the quality flag is 0, False, or missing; applied before join and fill

When every variable uses the same method, write spn.align(sza, density, grid=bins, method="nearest", tolerance="5s"). time × component vectors expand into columns such as magnetic_field_x and magnetic_field_y. Time series and vectors are supported; spectra cannot be binned directly yet.

Using the result

Call Returns
features.to_polars() Table with time and one column per variable; layout="long" for a long table
features.to_feature_frame() Variable columns only (include_time=True adds time)
features.to_feature_matrix() FeatureMatrix with a numeric matrix, column names, times, and provenance
features.metadata() Record of bins, per-variable methods, and join settings
features.write_dataset(store, "analysis.wake_context", context=case) Save in the Store's features layer

FeatureMatrix.select("sza", "wave_power") selects columns; write_parquet and write_npz save it, and FeatureMatrix.read_parquet and read_npz load it back.