Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions aeon/transformations/collection/feature_based/_catch22.py
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,21 @@ def _quantile(X, quant):
_local_simple_mean3_stderr = Catch22._FC_LocalSimple_mean3_stderr


@njit(fastmath=True, cache=True)
def _zeros_promoted(series, n):
"""Allocate a length-n zero array typed to match catch22's dtype rule.

Applies the same dtype-promotion rule used internally by
z_normalise_series_with_mean / _as_normalised_float (float32
input stays float32, every other dtype -- including integer input --
promotes to float64). Used both for typed placeholders whose real
values are assigned later, and for output buffers that must keep the
input's precision instead of silently upcasting to float64.
"""
dtype = (series[:0] / 1).dtype
return np.zeros(n, dtype=dtype)


@njit(fastmath=True, cache=True)
def _transform_case_numba(
X, f_idx, keep, outlier_norm, fft_case, ac_case, ac_tw, ac_nfft, stds
Expand All @@ -1920,7 +1935,7 @@ def _transform_case_numba(
requested (numba's np.std can round differently from numpy's).
"""
n_feats = len(f_idx)
c22 = np.zeros(n_feats * len(X))
c22 = _zeros_promoted(X[0], n_feats * len(X))

f_count = -1
for i in range(len(X)):
Expand All @@ -1934,7 +1949,10 @@ def _transform_case_numba(
# typed placeholders; real values are assigned before first use. These
# must be dtype-fixed (not aliases of series) so the kernel also
# compiles for integer input arrays.
outlier_series = np.empty(0, np.float64)
# / 1 applies the same dtype promotion z_normalise_series_with_mean
# uses internally (float32 stays float32, everything else becomes
# float64), so this placeholder always matches the real value's dtype.
outlier_series = _zeros_promoted(series, 0)
have_outlier = False
fft = ac_tw[:0]
have_fft = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from numpy import testing

from aeon.datasets import load_basic_motions
from aeon.testing.data_generation import make_example_3d_numpy
from aeon.transformations.collection.feature_based import Catch22
from aeon.utils.validation._dependencies import _check_soft_dependencies

Expand Down Expand Up @@ -1521,3 +1522,26 @@ def track_n(s):
assert abs(np.std(passed_series["p"]) - 1.0) < 1e-10
assert abs(np.mean(passed_series["n"])) < 1e-10
assert abs(np.std(passed_series["n"]) - 1.0) < 1e-10


def test_catch22_float32_output_dtype():
"""Test Catch22 does not raise on float32 input with outlier features.

Regression test: the outlier_series placeholder in _transform_case_numba was
hardcoded to float64, which numba's nopython mode could not unify with a
float32 z-normalised series produced for features 13/14 (outlier_include /
outlier_include_n), raising a TypingError for float32 input.
"""
X = make_example_3d_numpy(
n_cases=6, n_channels=1, n_timepoints=40, random_state=0, return_y=False
).astype(np.float32)

# outlier_norm=True exercises the fixed code path (features 13/14)
c22 = Catch22(outlier_norm=True, replace_nans=True)
result = c22.fit_transform(X)
assert result.shape == (6, 22)

# outlier_norm=False takes the other branch -- should still work
c22_no_norm = Catch22(outlier_norm=False, replace_nans=True)
result_no_norm = c22_no_norm.fit_transform(X)
assert result_no_norm.shape == (6, 22)
Loading