Skip to content

Commit c55a14e

Browse files
authored
ENH: enable complex dtype support unconditionally (#790)
* WIP: ENH: enable complex dtype support unconditionally This also gets rid of usage of the deprecated IF macro in Cython code, which resolves a bunch of build warnings. * BLD/ENH: support complex dtypes on MSVC, mirroring NumPy's approach MSVC has no C99 `_Complex`; it provides the struct-based `_Fcomplex`/`_Dcomplex` types, which have no arithmetic operators. Handle this the way NumPy does (see npy_common.h/npy_math.h and the complex check in numpy/_core/meson.build): - common.h: typedef switch (`_Fcomplex`/`_Dcomplex` on MSVC, C99 complex elsewhere) plus inline accessors that read via `creal` and write via pointer punning, valid for every representation. A PYWT_TEST_STRUCT_COMPLEX define forces NumPy's struct branch so the MSVC code path can be compiled and tested with any compiler. - convolution.template.c (the only complex arithmetic in the C core): replace operators with type-generic helpers - ZERO/ADD/SUB/MUL and ADD_PRODUCT(s, f, x) for `s += f * x`. The multiplier is always real, so real and imaginary parts stay independent and codegen on C99-complex compilers is unchanged. - Cython boundary: declare float_complex/double_complex as opaque extern typedefs in c_wt.pxd and cast explicitly at call sites, instead of forcing -DCYTHON_CCOMPLEX=1; Cython >=3.2.5 picks the right complex representation per compiler. - meson: replace the have_c99_complex probe with NumPy's complex.h/type checks, and drop the _c99_config machinery plus the remaining real/imag-splitting fallbacks in _swt.py/_multidim.py. The full test suite passes both normally and with the struct representation forced (emulating MSVC semantics on Linux). * CI: test the MSVC-style struct complex path on linux-aarch64 Build with -DPYWT_TEST_STRUCT_COMPLEX so the operator-free complex code path that MSVC relies on is compiled and tested on every PR, not only in the Windows wheel builds. * DOC: clarify the PYWT_TEST_STRUCT_COMPLEX comment in common.h * BLD: document why the Cython copy-tree must stay in meson.build The generated config.pxi is gone, but the copies still provide the rebuild dependency on .pxd files: Cython's -M depfiles (which Meson's native Cython support relies on) do not record module-namespace cimports such as 'from . cimport c_wt', so dropping the copy-tree would cause stale builds after .pxd edits.
2 parents d291c3d + 4026e04 commit c55a14e

22 files changed

Lines changed: 708 additions & 636 deletions

‎.github/workflows/tests.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ jobs:
4141
# Linux arm64
4242
- runs-on: ubuntu-22.04-arm
4343
python-version: "3.14"
44+
# Linux arm64, MSVC-style struct complex types forced, to exercise
45+
# the operator-free complex code path on a C99-complex compiler
46+
# (see PYWT_TEST_STRUCT_COMPLEX in pywt/_extensions/c/common.h)
47+
- runs-on: ubuntu-22.04-arm
48+
python-version: "3.14"
49+
OPTIONS_NAME: "struct-complex"
4450
# Linux amd64
4551
- runs-on: ubuntu-latest
4652
python-version: "3.12"
@@ -100,6 +106,8 @@ jobs:
100106
elif [ "${REFGUIDE_CHECK}" == "1" ]; then
101107
pip install sphinx numpydoc scipy-doctest
102108
pip install . -v
109+
elif [ "${OPTIONS_NAME}" == "struct-complex" ]; then
110+
pip install . -v -Csetup-args=-Dc_args=-DPYWT_TEST_STRUCT_COMPLEX
103111
else
104112
pip install . -v
105113
fi

‎.gitignore‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ doc/source/regression/*.ipynb
3636
# Project working files
3737
# Expanded Cython
3838
pywt/_extensions/*.[ch]
39-
pywt/_extensions/_c99_config.py
40-
pywt/_extensions/config.pxi
4139
cythonize.dat
4240
pywt/version.py
4341
build.log

‎meson.build‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ project(
1111
],
1212
)
1313

14-
cython = find_program('cython')
14+
cython = find_program('cython', version: '>=3.2.5')
1515

1616
py = import('python').find_installation(pure: false)
1717
py_dep = py.dependency()

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
build-backend = "mesonpy"
99
requires = [
1010
"meson-python>=0.18.0",
11-
"Cython>=3.1.3",
11+
"Cython>=3.2.5",
1212

1313
# numpy requirement for wheel builds for distribution on PyPI - building
1414
# against 2.x yields wheels that are also compatible with numpy 1.x at

‎pywt/_c99_config.py.in‎

Lines changed: 0 additions & 3 deletions
This file was deleted.

‎pywt/_dwt.py‎

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import numpy as np
44

5-
from ._c99_config import _have_c99_complex
65
from ._extensions._dwt import downcoef as _downcoef
76
from ._extensions._dwt import dwt_axis, dwt_single, idwt_axis, idwt_single
87
from ._extensions._dwt import dwt_coeff_len as _dwt_coeff_len
@@ -161,12 +160,6 @@ def dwt(data, wavelet, mode='symmetric', axis=-1):
161160
array([-0.70710678, -0.70710678, -0.70710678])
162161
163162
"""
164-
if not _have_c99_complex and np.iscomplexobj(data):
165-
data = np.asarray(data)
166-
cA_r, cD_r = dwt(data.real, wavelet, mode, axis)
167-
cA_i, cD_i = dwt(data.imag, wavelet, mode, axis)
168-
return (cA_r + 1j*cA_i, cD_r + 1j*cD_i)
169-
170163
# accept array_like input; make a copy to ensure a contiguous array
171164
dt = _check_dtype(data)
172165
data = np.asarray(data, dtype=dt, order='C')
@@ -241,17 +234,6 @@ def idwt(cA, cD, wavelet, mode='symmetric', axis=-1):
241234
raise ValueError("At least one coefficient parameter must be "
242235
"specified.")
243236

244-
# for complex inputs: compute real and imaginary separately then combine
245-
if not _have_c99_complex and (np.iscomplexobj(cA) or np.iscomplexobj(cD)):
246-
if cA is None:
247-
cD = np.asarray(cD)
248-
cA = np.zeros_like(cD)
249-
elif cD is None:
250-
cA = np.asarray(cA)
251-
cD = np.zeros_like(cA)
252-
return (idwt(cA.real, cD.real, wavelet, mode, axis) +
253-
1j*idwt(cA.imag, cD.imag, wavelet, mode, axis))
254-
255237
if cA is not None:
256238
dt = _check_dtype(cA)
257239
cA = np.asarray(cA, dtype=dt, order='C')
@@ -328,9 +310,6 @@ def downcoef(part, data, wavelet, mode='symmetric', level=1):
328310
upcoef
329311
330312
"""
331-
if not _have_c99_complex and np.iscomplexobj(data):
332-
return (downcoef(part, data.real, wavelet, mode, level) +
333-
1j*downcoef(part, data.imag, wavelet, mode, level))
334313
# accept array_like input; make a copy to ensure a contiguous array
335314
dt = _check_dtype(data)
336315
data = np.asarray(data, dtype=dt, order='C')
@@ -387,9 +366,6 @@ def upcoef(part, coeffs, wavelet, level=1, take=0):
387366
array([ 1., 2., 3., 4., 5., 6.])
388367
389368
"""
390-
if not _have_c99_complex and np.iscomplexobj(coeffs):
391-
return (upcoef(part, coeffs.real, wavelet, level, take) +
392-
1j*upcoef(part, coeffs.imag, wavelet, level, take))
393369
# accept array_like input; make a copy to ensure a contiguous array
394370
dt = _check_dtype(coeffs)
395371
coeffs = np.asarray(coeffs, dtype=dt, order='C')

0 commit comments

Comments
 (0)