Skip to content

Aeon Periodograms for Unequal-Length Time Series

Published:
13 min read

Aeon’s PeriodogramTransformer converts each multivariate time series into spectral magnitudes. Equal-length cases fit naturally into one 3-D array; for unequal-length collections, a list of arrays is the natural representation. With padding enabled, the transformer pads each member before computing its periodogram, and that representation change matters because list and array inputs travel through different preprocessing branches.

That split once made PeriodogramTransformer(pad_series=True, pad_with="mean") reject a valid unequal-length collection with ValueError: unsupported keyword arguments for mode 'mean': {'constant_values'}. The transformer had selected mean; the unwanted argument belonged to constant. The useful lesson is broader than that exception: an estimator that accepts several collection representations needs a parity contract across both representation and parameter mode.

Why unequal length changes the representation

An unequal-length collection cannot be stacked into one ordinary 3-D array without first making its time axes agree. Aeon’s transformer accepts that collection as a list. With pad_series=True, each member is padded to its own next power-of-two length before the periodogram is taken. The running example uses two-channel arrays with 20 and 33 time points. Their padded lengths are 32 and 64, so their retained half-spectrum widths are 16 and 32.

The user-facing choice is pad_with="mean". NumPy recognizes mean as a padding mode. Nothing in that request asks for a constant fill value. Before the repair, however, the unequal-length list branch built a call equivalent to np.pad(..., mode="mean", constant_values=0). NumPy read the mode first, checked the keyword family allowed for that mode, and rejected constant_values.

That makes the error unusually diagnostic. The input values are not the trigger. Neither are the channel count or the eventual Fourier width. The call is internally contradictory before a padded sample exists: the mode says “use a statistic,” while an extra keyword says “use this constant.” The exact captured exception names the unwanted argument instead of pointing at a numerical failure downstream.

The default constant path happened to be compatible with the unconditional keyword. For that mode, constant_values=0 is meaningful, so the list branch completed. A green default case therefore supplied no evidence about mean, reflect, or any other parameter mode with a different keyword contract. The useful test axis is the pair (collection representation, padding mode), not either coordinate in isolation.

A complete unequal-length example

The following complete example is reduced to the public behavioral boundary: two deterministic unequal-length inputs, the non-default mean mode, two shape assertions, and one value comparison against the 3-D path for x0. It is runnable against the merged source revision identified in the case study below.

import numpy as np
from aeon.transformations.collection import PeriodogramTransformer

X = [
    np.random.RandomState(0).random((2, 20)),
    np.random.RandomState(1).random((2, 33)),
]
transformer = PeriodogramTransformer(pad_series=True, pad_with="mean")
Xt_list = transformer.fit_transform(X)
assert Xt_list[0].shape == (2, 16)
assert Xt_list[1].shape == (2, 32)
Xt_array = transformer.fit_transform(X[0][None, ...])
np.testing.assert_allclose(Xt_list[0], Xt_array[0])

On the pre-fix list path, execution ends at transformer.fit_transform(X) with ValueError: unsupported keyword arguments for mode 'mean': {'constant_values'}. On the repaired path, both list output shapes are (2, 16) and (2, 32), respectively. Only Xt_list[0] is compared numerically with Xt_array[0]; the second list element receives a shape assertion, not a second 3-D parity comparison.

The seeds matter less than their role: they freeze values so a value-level assertion can be repeated. The two lengths matter directly. Twenty pads to 32 and yields 16 retained magnitudes; 33 pads to 64 and yields 32. Those dimensions check the transform’s shape arithmetic. The final assertion checks something different: x0 should not change merely because it arrived beside an unequal-length neighbor in a list.

Calling this a minimal reproduction does not mean it runs on an arbitrary packaged Aeon version. The code boundary is the merged implementation identified below. The latest checked release at the research cutoff predates that merge, so the example should be evaluated against the pinned source rather than treated as a release-availability statement.

Two containers, two branches

The same logical series can enter through different collection representations. A 3-D array has one shared time length and a shape such as (cases, channels, time). A Python list can hold arrays whose time lengths differ. Supporting both means the transformer has separate routing and padding work before the common periodogram calculation.

The existing 3-D branch already built its padding kwargs conditionally. For constant, it forwarded mode and constant_values; for mean, it forwarded only mode. The pre-fix list branch did not preserve that condition. It sent constant_values for each mode, which made the two representations disagree before the Fourier operation began.

Shape assertions do detect the observed mean rejection because the failing branch produces no result for them to inspect. A value comparison is the stronger oracle for silent semantic divergence once both branches return arrays of the expected shape. The discriminating contract therefore combines a non-default mode with a value comparison against a representation that already handles the same mode correctly.

NumPy’s keyword contract

The official numpy.pad documentation gives the interface as numpy.pad(array, pad_width, mode='constant', **kwargs). The **kwargs spelling makes the call syntactically flexible, but the accepted names are conditional on mode. constant_values belongs only to constant mode. Statistical modes such as mean, median, maximum, and minimum use stat_length; linear_ramp uses end_values; reflect and symmetric use reflect_type.

That is a sum type hiding in an ordinary Python signature. Each mode selects a different legal payload. Treating the kwargs as one flat bag loses the relationship:

The repair does not add those other optional families. It performs the narrower operation demanded by the transformer API: always forward the selected mode, and attach the transformer’s constant value only when the selected mode is constant. The transformer can then delegate mode semantics to NumPy without sending an argument whose meaning belongs to another variant.

This is why the exception is about API structure rather than data quality. No statistic computed from X can make constant_values relevant to mean. Changing the arrays, tolerances, or Fourier assertion would leave the rejected call intact. The smallest useful repair sits where the kwargs are assembled.

The branch-parity invariant

Let T_list,m denote the transform reached through the unequal-length list branch under padding mode m, and let T_3D,m denote the 3-D branch. For the first series in a deterministic pair, the regression asks for:

T_list,m([x0,x1])[0] ≈ T_3D,m(x0[None,...])[0]

This is a behavioral invariant, not an identity of container mechanics. The list path also transforms x1; the 3-D witness contains only x0. The comparison isolates the same array, mode, and transform while changing the outer representation. If the delegated padding kwargs diverge, the left side can reject even though the right side is defined. If both finish, the numerical comparison checks that routing through the list has not changed x0’s periodogram.

The upstream regression uses two oracles. Both list output shapes are asserted: (2, 16) for x0 and (2, 32) for x1. Numerical parity is narrower and compares only Xt_list[0] with Xt_array[0]. Because the upstream np.testing.assert_allclose call passes no tolerance arguments, the official assert_allclose documentation supplies the defaults rtol=1e-7, atol=0.

A second, dependency-free fixture checks the reasoning without claiming to be another Aeon capture. It starts with one-channel x0 = [1,2,3,4,5] and x1 = [1,2,3,4,5,6]. Mean padding to length eight produces [1,2,3,4,5,3,3,3] and [1,2,3,4,5,6,3.5,3.5]. A direct DFT then retains the first floor(N/2) magnitudes. The x0 list spectrum and its 3-D wrapper both evaluate to [24, 5.414213562373097, 1.9999999999999998, 2.585786437626905] in that model.

The observed maximum absolute difference for that x0 calculation is zero. Zero here is a floating observation, not a byte-identity claim. The independent direct-DFT fixture uses the absolute-only threshold rtol=0, atol=1e-12. Keeping that tolerance next to its fixture avoids quietly borrowing the upstream relative tolerance or presenting two separate checks as one experiment.

The invariant generalizes more usefully than the particular exception. When one estimator accepts multiple containers, choose a common logical sample, hold its parameter mode fixed, and compare behavior across branches. Shapes remain worth checking because they catch padding-width mistakes. A value oracle covers the semantics that shape arithmetic cannot see.

A bounded Aeon case study

An Aeon contribution sharpened the conditional-keyword path and regression coverage; the maintainer-reviewed merge is a practical case study in this broader branch-parity contract.

The submitted head revision is 0050d4e7251ae9bbc8a2c57ebfef1ebbab502cf3; the distinct squash revision is 8ac71e463aa7314bd1bfc05edccf3d53de1aa3a1. The submitted source and submitted test are byte-identical to the corresponding squash source and squash test. Those byte identities support a source-derived merged model; that model is not a captured squash run.

A three-card branch diagram scoped to pad_series true and an unequal-length list in mean mode. The pre-fix unequal-length list branch forwards mode mean with constant_values zero and shows Reject. The existing 3-D branch omits constant_values and shows Pass. A source-derived merged model also omits constant_values and shows Pass, with submitted and squash revisions separated and an explicit note that it is not a captured squash run.

Figure 1. Conditional forwarding is the operative control: removing the irrelevant keyword from the shown mean call changes the list branch from reject to pass and restores the same delegated contract already used by the 3-D path.

The diagram separates three evidence classes. The first card is captured pre-fix list behavior. The second is the already-conditional 3-D reference. The third is read from the byte-matching submitted and squash source and is deliberately labelled as a source-derived merged model. That distinction prevents source inspection from being reported as a test run that was never captured.

The two-line repair

At the decision point, the repair is two lines of logic:

kwargs = {"mode": self.pad_with}

if self.pad_with == "constant": kwargs["constant_values"] = self.constant_value

The first line represents the common contract. The second adds the mode-specific field. The list branch then calls the same delegated API with the same conditional rule already present in the 3-D branch. The exact change and its regression are visible in the immutable squash diff.

The upstream test boundary is precise. It varies ten named strings, constructs the seeded (2, 20) and (2, 33) arrays, asserts both list shapes, and compares the x0 list output with the 3-D x0 output. It does not numerically compare the second list element with a second 3-D execution. That narrower oracle is enough to expose the branch mismatch while keeping the test’s claim honest.

Ten tested modes

The captured matrix covers ten tested named modes in this exact order: constant, mean, edge, reflect, median, linear_ramp, maximum, minimum, symmetric, and wrap. It is a bounded regression table, not an inventory of NumPy’s full padding interface.

A two-row matrix for ten tested named modes: constant, mean, edge, reflect, median, linear_ramp, maximum, minimum, symmetric, and wrap. Captured before at 6f5a24f, constant is Pass and the other nine cells are Reject. Captured after fix at 9525193, all ten cells are Pass. Every status is written and paired with a check-circle or cross-diamond, and the 3-D x0 result is identified as a parity reference rather than a third captured row.

Figure 2. The captured result changes from one pass and nine rejections to ten passes across the tested matrix; the denominator and branch-tip labels keep that inference narrower than the source-derived merged model.

Captured before at 6f5a24f, the focused run recorded one pass and nine rejections. constant passed; each of the other nine names rejected the irrelevant constant_values keyword. The 9525193 branch-tip capture recorded ten passes. Both records use the same ten-name denominator, so the focused one-of-ten versus ten-of-ten comparison is matched.

The after row is labelled “Captured after fix,” not “merged.” Revision 9525193 belongs to an unmerged post-fix branch tip. Separately, the submitted and squash bodies establish what code and test reached the merged revision. Agreement between that source-derived model and the branch-tip outcome is useful corroboration, but it does not manufacture a captured run at the squash SHA.

Limits

Operational checklist

  1. Vary the input container independently: exercise the list and 3-D representations with the same logical x0.
  2. Vary the parameter mode independently: keep the default, then add a valid non-default mode whose delegated kwargs differ.
  3. Pair shape checks with a behavioral oracle that compares the common sample across branches.
  4. Write the numerical tolerance beside the oracle that uses it; do not carry defaults from one fixture into another.
  5. Inspect kwargs at the delegation boundary and attach mode-specific fields only inside their valid branch.
  6. Label captured output, submitted code, squash code, and source-derived models as separate provenance layers.

The practical test grid is small: container × parameter mode × oracle. Its value comes from keeping the axes independent. A default-mode shape assertion occupies one cell. The mean list/3-D value comparison occupies another and fails for exactly the branch contract that had diverged.

Primary sources

Newsletter subscriptions are not currently available.