From 5bfaa14fb54c71347c948fc432102bf2df37f396 Mon Sep 17 00:00:00 2001 From: AC Date: Wed, 14 Oct 2026 18:17:07 +0800 Subject: [PATCH] Expose continuous wavelet support bounds at construction --- doc/source/ref/wavelets.rst | 17 ++++++++++++-- pywt/_extensions/_pywt.pyx | 40 ++++++++++++++++++++++++++------ pywt/tests/test_cwt_wavelets.py | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/doc/source/ref/wavelets.rst b/doc/source/ref/wavelets.rst index 1ddc4cc1c..371e95d7f 100644 --- a/doc/source/ref/wavelets.rst +++ b/doc/source/ref/wavelets.rst @@ -262,7 +262,7 @@ from plain Python lists of filter coefficients and a *filter bank-like* object. ``ContinuousWavelet`` object ---------------------------- -.. class:: ContinuousWavelet(name, dtype=np.float64) +.. class:: ContinuousWavelet(name, dtype=np.float64, lower_bound=None, upper_bound=None) Describes properties of a continuous wavelet identified by the specified wavelet ``name``. In order to use a built-in wavelet the ``name`` parameter must be a valid @@ -270,13 +270,22 @@ from plain Python lists of filter coefficients and a *filter bank-like* object. :param name: Wavelet name :param dtype: numpy.dtype to use for the wavelet. Can be numpy.float64 or numpy.float32. + :param lower_bound: Lower bound of the effective support. ``None`` keeps the family's default. + :param upper_bound: Upper bound of the effective support. ``None`` keeps the family's default. + + The bounds determine the interval sampled by :meth:`wavefun` and used in + :func:`pywt.cwt`. Defaults depend on the wavelet family: for example, ``gaus1`` + uses [-5, 5], while ``morl`` uses [-8, 8]. They remain writable after construction. **Example:** .. try_examples:: >>> import pywt - >>> wavelet = pywt.ContinuousWavelet('gaus1') + >>> wavelet = pywt.ContinuousWavelet('gaus1', lower_bound=-4, upper_bound=4) + >>> psi, x = wavelet.wavefun() + >>> float(x[0]), float(x[-1]) + (-4.0, 4.0) .. attribute:: name @@ -340,6 +349,8 @@ from plain Python lists of filter coefficients and a *filter bank-like* object. DWT: False CWT: True Complex CWT: False + Lower bound: -5.0 + Upper bound: 5.0 Approximating wavelet functions - ``ContinuousWavelet.wavefun()`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -398,3 +409,5 @@ Approximating wavelet functions - ``ContinuousWavelet.wavefun()`` DWT: False CWT: True Complex CWT: False + Lower bound: -5.0 + Upper bound: 5.0 diff --git a/pywt/_extensions/_pywt.pyx b/pywt/_extensions/_pywt.pyx index ddef57500..d664e6e54 100644 --- a/pywt/_extensions/_pywt.pyx +++ b/pywt/_extensions/_pywt.pyx @@ -680,15 +680,34 @@ cdef public class Wavelet [type WaveletType, object WaveletObject]: cdef public class ContinuousWavelet [type ContinuousWaveletType, object ContinuousWaveletObject]: """ - ContinuousWavelet(name, dtype) object describe properties of - a continuous wavelet identified by name. + ContinuousWavelet(name, dtype=np.float64, lower_bound=None, upper_bound=None) - In order to use a built-in wavelet the parameter name must be - a valid name from the wavelist() list. + Describe the properties of a continuous wavelet identified by name. + + Parameters + ---------- + name : str + Built-in continuous wavelet name from ``wavelist(kind='continuous')``. + dtype : numpy.dtype, optional + Data type for wavelet evaluation: ``numpy.float64`` (default) or + ``numpy.float32``. + lower_bound, upper_bound : float or None, optional + Bounds of the effective support used by ``wavefun`` and the continuous + wavelet transform. Each ``None`` retains the wavelet family's default + bound. For example, ``morl`` uses [-8, 8] and ``gaus1`` uses [-5, 5]. + The bounds can also be changed after construction using the attributes + of the same names. + Examples + -------- + >>> import pywt + >>> wavelet = pywt.ContinuousWavelet('morl', lower_bound=-6, upper_bound=6) + >>> wavelet.lower_bound, wavelet.upper_bound + (-6.0, 6.0) """ #cdef readonly properties - def __cinit__(self, name=u"", dtype=np.float64): + def __cinit__(self, name=u"", dtype=np.float64, lower_bound=None, + upper_bound=None): cdef object family_code, family_number # builtin wavelet @@ -771,6 +790,10 @@ cdef public class ContinuousWavelet [type ContinuousWaveletType, object Continuo raise ValueError( "Invalid continuous wavelet name '%s'." % self.name) + if lower_bound is not None: + self.lower_bound = lower_bound + if upper_bound is not None: + self.upper_bound = upper_bound def __dealloc__(self): if self.w is not NULL: @@ -778,7 +801,8 @@ cdef public class ContinuousWavelet [type ContinuousWaveletType, object Continuo self.w = NULL def __reduce__(self): - return (ContinuousWavelet, (self.name, self.dt)) + return (ContinuousWavelet, (self.name, self.dt, + self.w.lower_bound, self.w.upper_bound)) property family_number: "Wavelet family number" @@ -987,7 +1011,9 @@ cdef public class ContinuousWavelet [type ContinuousWaveletType, object Continuo u" Symmetry: %s" % self.symmetry, u" DWT: False", u" CWT: True", - u" Complex CWT: %s" % self.complex_cwt + u" Complex CWT: %s" % self.complex_cwt, + u" Lower bound: %s" % self.lower_bound, + u" Upper bound: %s" % self.upper_bound ]: s.append(x.rstrip()) return u'\n'.join(s) diff --git a/pywt/tests/test_cwt_wavelets.py b/pywt/tests/test_cwt_wavelets.py index 62b468853..d7bfd5b0d 100644 --- a/pywt/tests/test_cwt_wavelets.py +++ b/pywt/tests/test_cwt_wavelets.py @@ -561,3 +561,44 @@ def test_cwt_discrete_wavelet_raises(): # a continuous wavelet still works out, _ = pywt.cwt(data, [1, 2], 'morl') assert out.shape == (2, 100) + + +@pytest.mark.parametrize('name', ['gaus1', 'morl', 'cmor1.5-1.0', 'shan1-1']) +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_continuous_wavelet_constructor_bounds(name, dtype): + wavelet = pywt.ContinuousWavelet(name, dtype, lower_bound=-4, upper_bound=6) + assigned = pywt.ContinuousWavelet(name, dtype) + assigned.lower_bound = -4 + assigned.upper_bound = 6 + for actual, expected in zip(wavelet.wavefun(), assigned.wavefun()): + assert_equal(actual, expected) + data = np.sin(np.arange(64)) + for actual, expected in zip(pywt.cwt(data, [1, 2], wavelet), + pywt.cwt(data, [1, 2], assigned)): + assert_equal(actual, expected) + assert 'Lower bound: -4.0' in str(wavelet) + assert 'Upper bound: 6.0' in str(wavelet) + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + restored = pickle.loads(pickle.dumps(wavelet, protocol=protocol)) + assert restored.lower_bound == -4 + assert restored.upper_bound == 6 + for actual, expected in zip(restored.wavefun(), wavelet.wavefun()): + assert_equal(actual, expected) + + +@pytest.mark.parametrize('name', ['gaus1', 'morl', 'shan1-1']) +def test_continuous_wavelet_constructor_default_bounds(name): + default = pywt.ContinuousWavelet(name) + lower = pywt.ContinuousWavelet(name, lower_bound=-3) + upper = pywt.ContinuousWavelet(name, upper_bound=3) + assert (lower.lower_bound, lower.upper_bound) == (-3, default.upper_bound) + assert (upper.lower_bound, upper.upper_bound) == (default.lower_bound, 3) + explicit_none = pywt.ContinuousWavelet(name, lower_bound=None, upper_bound=None) + assert explicit_none.lower_bound == default.lower_bound + assert explicit_none.upper_bound == default.upper_bound + + +@pytest.mark.parametrize('bound', ['lower_bound', 'upper_bound']) +def test_continuous_wavelet_invalid_constructor_bound(bound): + with pytest.raises(TypeError): + pywt.ContinuousWavelet('morl', **{bound: object()})