From 49b73b5796f40503eae08db52470802591a11639 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:01:48 +0200 Subject: [PATCH 01/12] build: configure ruff in pyproject.toml Use a line length of 88, which the code already follows, and take the target version (3.11) from requires-python. Select E, W, F, I, UP and B explicitly. ruff 0.17 enables about 400 rules when nothing is selected, and that default set changes between releases, so an explicit list keeps the result stable when ruff is updated. Ignore E501 only: ruff format wraps code at 88 columns, and the long lines it leaves are prose in strings and comments. --- pyproject.toml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c55850e..5ce7d95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,3 +49,24 @@ bugtracker = "https://github.com/JesperDramsch/keras-complex/issues" [tool.setuptools.packages.find] exclude = ["tests*", "docs*"] + +# ruff takes its target Python version from requires-python above. +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +# List the rules explicitly: newer ruff releases enable a much larger default set, +# and the result should not change when ruff is updated. +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear +] +ignore = [ + # ruff format already wraps code at 88 columns. The lines it leaves long are + # prose in strings and comments, which read better unbroken. + "E501", +] From 81f6cd8eafcc2c6a08df53b2967d0fd2d96736dd Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:04:33 +0200 Subject: [PATCH 02/12] test: pin the behaviour that the lint fixes touch These tests pass on the current code and must keep passing after the ruff clean-up: - the public names of the complexnn package and of `from complexnn import *`, including the fft and utils submodules - ComplexBN and ComplexLayerNorm reject an input of unknown rank with ValueError - ComplexDense output, computed with numpy; the layer multiplies by the conjugate of its kernel, unlike ComplexConv - ComplexConv1D/2D with spectral_parametrization=True raise NotImplementedError, from the ifft stubs in conv.py --- tests/test_api.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_bn.py | 12 ++++++++++- tests/test_conv.py | 10 +++++++++ tests/test_dense.py | 12 +++++++++++ tests/test_norm.py | 12 ++++++++++- 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/test_api.py diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..65c2586 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,52 @@ +import unittest + +import complexnn + +# Every public name that `import complexnn` provides, submodules included. +PUBLIC_NAMES = { + "bn", + "conv", + "dense", + "fft", + "init", + "norm", + "pool", + "utils", + "ComplexBN", + "ComplexConv", + "ComplexConv1D", + "ComplexConv2D", + "ComplexConv3D", + "WeightNorm_Conv", + "ComplexDense", + "ComplexIndependentFilters", + "ComplexInit", + "IndependentFilters", + "SqrtInit", + "ComplexLayerNorm", + "LayerNormalization", + "SpectralPooling1D", + "SpectralPooling2D", + "GetAbs", + "GetImag", + "GetReal", + "get_imagpart", + "get_realpart", + "getpart_output_shape", +} + + +class TestPublicNamespace(unittest.TestCase): + """The names the complexnn package exports""" + + def test_public_attributes(self): + """The package has exactly the public attributes listed above""" + public = {name for name in dir(complexnn) if not name.startswith("_")} + self.assertEqual(public, PUBLIC_NAMES) + + def test_star_import(self): + """`from complexnn import *` provides exactly the public names""" + namespace = {} + exec("from complexnn import *", namespace) + del namespace["__builtins__"] + self.assertEqual(set(namespace), PUBLIC_NAMES) diff --git a/tests/test_bn.py b/tests/test_bn.py index 67acb9f..2763202 100644 --- a/tests/test_bn.py +++ b/tests/test_bn.py @@ -1,9 +1,10 @@ import unittest -import complexnn as conn import numpy as np import tensorflow as tf +import complexnn as conn + class TestComplexBatchNormalization(unittest.TestCase): """Moving statistics and train/inference behaviour of ComplexBatchNormalization""" @@ -58,3 +59,12 @@ def test_compute_output_shape(self): for shape in [(None, 8), (None, 4, 4, 8)]: with self.subTest(shape=shape): self.assertEqual(tuple(layer.compute_output_shape(shape)), shape) + + +class TestComplexBatchNormalizationBuild(unittest.TestCase): + """Input checks in ComplexBatchNormalization.build""" + + def test_build_rejects_unknown_rank(self): + """Building for an input of unknown rank raises ValueError""" + with self.assertRaises(ValueError): + conn.ComplexBN().build(tf.TensorShape(None)) diff --git a/tests/test_conv.py b/tests/test_conv.py index 108998d..367682e 100644 --- a/tests/test_conv.py +++ b/tests/test_conv.py @@ -57,6 +57,16 @@ def test_conv2Dtranspose(self): calc = model.output_shape self.assertEqual(true, calc) + def test_spectral_parametrization_not_implemented(self): + """spectral_parametrization=True raises NotImplementedError when called""" + for layer, shape in ( + (conn.ComplexConv1D(2, 3, spectral_parametrization=True), (2, 8, 4)), + (conn.ComplexConv2D(2, 3, spectral_parametrization=True), (2, 8, 8, 4)), + ): + with self.subTest(layer=type(layer).__name__): + with self.assertRaises(NotImplementedError): + layer(np.zeros(shape, "float32")) + class TestConvConfig(unittest.TestCase): """Conv layer configs rebuild the same layer""" diff --git a/tests/test_dense.py b/tests/test_dense.py index 98721a9..0e4c9a2 100644 --- a/tests/test_dense.py +++ b/tests/test_dense.py @@ -19,6 +19,18 @@ def test_outputs_dense(self): calc = layer.compute_output_shape(input_shape) self.assertEqual(true, calc) + def test_call_multiplies_by_conjugate_kernel(self): + """The output is the complex product of the input and the conjugate kernel""" + # ComplexConv multiplies by its kernel, ComplexDense by the conjugate of + # its kernel. This test records the current behaviour. + rng = np.random.default_rng(0) + x = rng.standard_normal((4, 6)).astype("float32") # 3 real, then 3 imaginary + layer = conn.ComplexDense(units=2, use_bias=False) + y = layer(x).numpy() + kernel = layer.real_kernel.numpy() + 1j * layer.imag_kernel.numpy() + expected = (x[:, :3] + 1j * x[:, 3:]) @ np.conj(kernel) + np.testing.assert_allclose(y[:, :2] + 1j * y[:, 2:], expected, atol=1e-5) + class TestDenseConfig(unittest.TestCase): """ComplexDense configs rebuild the same kernel initialiser""" diff --git a/tests/test_norm.py b/tests/test_norm.py index 41544fa..e5cf01e 100644 --- a/tests/test_norm.py +++ b/tests/test_norm.py @@ -1,7 +1,8 @@ import unittest -import complexnn as conn import tensorflow as tf + +import complexnn as conn from complexnn.bn import sqrt_init @@ -53,3 +54,12 @@ def test_compute_output_shape(self): for shape in [(None, 8), (None, 4, 4, 8)]: with self.subTest(shape=shape): self.assertEqual(tuple(layer.compute_output_shape(shape)), shape) + + +class TestComplexLayerNormBuild(unittest.TestCase): + """Input checks in ComplexLayerNorm.build""" + + def test_build_rejects_unknown_rank(self): + """Building for an input of unknown rank raises ValueError""" + with self.assertRaises(ValueError): + conn.ComplexLayerNorm().build(tf.TensorShape(None)) From d1b9b12ceb4a9886afdfe13afcf53310ccfcd836 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:05:49 +0200 Subject: [PATCH 03/12] refactor: declare the package namespace in complexnn.__all__ ruff reported every re-export in complexnn/__init__.py as an unused import (F401). List them in __all__ instead, which also documents the public API. __all__ keeps `from complexnn import *` exactly as it was, so it includes the fft and utils submodules. fft was only loaded as a side effect of an import in conv.py that the next commit removes, so import both submodules explicitly here. --- complexnn/__init__.py | 36 +++++++++++++++++++++++++++++++++--- tests/test_api.py | 4 ++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/complexnn/__init__.py b/complexnn/__init__.py index 4d6c1b1..e002632 100644 --- a/complexnn/__init__.py +++ b/complexnn/__init__.py @@ -1,9 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from . import bn, conv, dense, init, norm, pool - -# from . import fft +from . import bn, conv, dense, fft, init, norm, pool, utils from .bn import ComplexBatchNormalization as ComplexBN from .conv import ( ComplexConv, @@ -31,3 +29,35 @@ get_realpart, getpart_output_shape, ) + +__all__ = [ + "bn", + "conv", + "dense", + "fft", + "init", + "norm", + "pool", + "utils", + "ComplexBN", + "ComplexConv", + "ComplexConv1D", + "ComplexConv2D", + "ComplexConv3D", + "WeightNorm_Conv", + "ComplexDense", + "ComplexIndependentFilters", + "ComplexInit", + "IndependentFilters", + "SqrtInit", + "ComplexLayerNorm", + "LayerNormalization", + "SpectralPooling1D", + "SpectralPooling2D", + "GetAbs", + "GetImag", + "GetReal", + "get_imagpart", + "get_realpart", + "getpart_output_shape", +] diff --git a/tests/test_api.py b/tests/test_api.py index 65c2586..f28a82e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -50,3 +50,7 @@ def test_star_import(self): exec("from complexnn import *", namespace) del namespace["__builtins__"] self.assertEqual(set(namespace), PUBLIC_NAMES) + + def test_all(self): + """__all__ lists each public name once""" + self.assertCountEqual(complexnn.__all__, PUBLIC_NAMES) From 290c34e7b67acaa385d73dcefb7cc57854b274d6 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:06:55 +0200 Subject: [PATCH 04/12] refactor: remove dead code that ruff cannot fix safely - conv.py imported fft, fft2, ifft and ifft2 from .fft but used none of them: the module defines its own ifft and ifft2 stubs, which shadow the imports (F401, F811). The stubs stay, so spectral parametrisation still raises NotImplementedError. - ComplexBatchNormalization.build and ComplexLayerNorm.build computed `ndim = len(input_shape)` and never used it (F841). For an input of unknown rank, len() raised ValueError; the layer's own check for an undefined channel dimension now raises ValueError instead. - ComplexDense.call sliced the input into real and imaginary parts and never used the slices (F841). InputSpec(ndim=2) already guarantees that the slicing could not fail, so the output is unchanged. The tests added earlier cover each of these paths. --- complexnn/bn.py | 2 -- complexnn/dense.py | 5 ----- complexnn/norm.py | 1 - 3 files changed, 8 deletions(-) diff --git a/complexnn/bn.py b/complexnn/bn.py index 0682517..1074cf0 100644 --- a/complexnn/bn.py +++ b/complexnn/bn.py @@ -359,8 +359,6 @@ def __init__( self.gamma_off_constraint = constraints.get(gamma_off_constraint) def build(self, input_shape): - ndim = len(input_shape) - dim = input_shape[self.axis] if dim is None: raise ValueError( diff --git a/complexnn/dense.py b/complexnn/dense.py index 1753037..9e0d05f 100644 --- a/complexnn/dense.py +++ b/complexnn/dense.py @@ -180,11 +180,6 @@ def init_w_imag(shape, dtype=None): self.built = True def call(self, inputs): - input_shape = K.shape(inputs) - input_dim = input_shape[-1] // 2 - real_input = inputs[:, :input_dim] - imag_input = inputs[:, input_dim:] - cat_kernels_4_real = K.concatenate( [self.real_kernel, -self.imag_kernel], axis=-1 ) diff --git a/complexnn/norm.py b/complexnn/norm.py index b5d3576..90ff994 100644 --- a/complexnn/norm.py +++ b/complexnn/norm.py @@ -146,7 +146,6 @@ def __init__( super(ComplexLayerNorm, self).__init__(**kwargs) def build(self, input_shape): - ndim = len(input_shape) dim = input_shape[self.axis] if dim is None: raise ValueError( From 72f2fec418d6a768ed017d55ba1a166905b02eca Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 20:48:34 +0200 Subject: [PATCH 05/12] refactor: list the conv_utils module in complexnn.__all__ conv_utils, the helper module that replaced TensorFlow's private copy, is a submodule like utils and fft, so it is imported and listed with them instead of appearing as a side effect of importing conv. --- complexnn/__init__.py | 4 ++-- tests/test_api.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/complexnn/__init__.py b/complexnn/__init__.py index e002632..9f398da 100644 --- a/complexnn/__init__.py +++ b/complexnn/__init__.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from . import bn, conv, dense, fft, init, norm, pool, utils +from . import bn, conv, conv_utils, dense, fft, init, norm, pool, utils from .bn import ComplexBatchNormalization as ComplexBN from .conv import ( ComplexConv, @@ -33,6 +32,7 @@ __all__ = [ "bn", "conv", + "conv_utils", "dense", "fft", "init", diff --git a/tests/test_api.py b/tests/test_api.py index f28a82e..4528b30 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,6 +6,7 @@ PUBLIC_NAMES = { "bn", "conv", + "conv_utils", "dense", "fft", "init", From 4f69349a50ea2ec3e3fcd06f475a3b44e05a882e Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 20:49:41 +0200 Subject: [PATCH 06/12] style: apply ruff check --fix --- complexnn/bn.py | 11 +++-------- complexnn/conv.py | 25 ++++++++++++------------- complexnn/conv_utils.py | 1 - complexnn/dense.py | 5 ++--- complexnn/fft.py | 1 - complexnn/init.py | 1 - complexnn/norm.py | 13 ++++++------- complexnn/pool.py | 5 ++--- complexnn/utils.py | 3 +-- tests/test_conv.py | 5 ++--- tests/test_dense.py | 2 -- tests/test_init.py | 3 ++- tests/test_readme.py | 4 +--- tests/test_saving.py | 3 ++- tests/test_sequential.py | 3 ++- tests/test_train.py | 3 +-- 16 files changed, 36 insertions(+), 52 deletions(-) diff --git a/complexnn/bn.py b/complexnn/bn.py index 1074cf0..713846b 100644 --- a/complexnn/bn.py +++ b/complexnn/bn.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Note: The implementation of complex Batchnorm is based on # the Keras implementation of batch Normalization @@ -12,11 +11,7 @@ from tensorflow.keras.layers import InputSpec, Layer from tensorflow.keras.utils import register_keras_serializable -from .utils import get_initializer - -from .utils import _ignore_input_shape_warning - -from .utils import _check_complex_axis +from .utils import _check_complex_axis, _ignore_input_shape_warning, get_initializer def sqrt_init(shape, dtype=None): @@ -336,7 +331,7 @@ def __init__( **kwargs, ): with _ignore_input_shape_warning(kwargs): - super(ComplexBatchNormalization, self).__init__(**kwargs) + super().__init__(**kwargs) self.supports_masking = True self.axis = axis self.momentum = momentum @@ -581,5 +576,5 @@ def get_config(self): "gamma_diag_constraint": constraints.serialize(self.gamma_diag_constraint), "gamma_off_constraint": constraints.serialize(self.gamma_off_constraint), } - base_config = super(ComplexBatchNormalization, self).get_config() + base_config = super().get_config() return dict(list(base_config.items()) + list(config.items())) diff --git a/complexnn/conv.py b/complexnn/conv.py index 90d2dbf..a0598cd 100644 --- a/complexnn/conv.py +++ b/complexnn/conv.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import numpy as np import tensorflow as tf @@ -155,7 +154,7 @@ def conv_transpose_output_length( ): """Rearrange arguments for compatibility with conv_output_length.""" if dilation != 1: - msg = f"Dilation must be 1 for transposed convolution. " + msg = "Dilation must be 1 for transposed convolution. " msg += f"Got dilation = {dilation}" raise ValueError(msg) # return conv_utils.deconv_length( @@ -298,7 +297,7 @@ def __init__( **kwargs, ): with _ignore_input_shape_warning(kwargs): - super(ComplexConv, self).__init__(**kwargs) + super().__init__(**kwargs) self.rank = rank self.filters = filters self.kernel_size = conv_utils.normalize_tuple(kernel_size, rank, "kernel_size") @@ -647,7 +646,7 @@ def get_config(self): "transposed": self.transposed, "epsilon": self.epsilon, } - base_config = super(ComplexConv, self).get_config() + base_config = super().get_config() return dict(list(base_config.items()) + list(config.items())) @@ -760,7 +759,7 @@ def __init__( transposed=False, **kwargs, ): - super(ComplexConv1D, self).__init__( + super().__init__( rank=1, filters=filters, kernel_size=kernel_size, @@ -784,7 +783,7 @@ def __init__( ) def get_config(self): - config = super(ComplexConv1D, self).get_config() + config = super().get_config() config.pop("rank") # __init__ always passes data_format="channels_last" to ComplexConv. config.pop("data_format") @@ -916,7 +915,7 @@ def __init__( transposed=False, **kwargs, ): - super(ComplexConv2D, self).__init__( + super().__init__( rank=2, filters=filters, kernel_size=kernel_size, @@ -940,7 +939,7 @@ def __init__( ) def get_config(self): - config = super(ComplexConv2D, self).get_config() + config = super().get_config() config.pop("rank") return config @@ -1073,7 +1072,7 @@ def __init__( transposed=False, **kwargs, ): - super(ComplexConv3D, self).__init__( + super().__init__( rank=3, filters=filters, kernel_size=kernel_size, @@ -1097,7 +1096,7 @@ def __init__( ) def get_config(self): - config = super(ComplexConv3D, self).get_config() + config = super().get_config() config.pop("rank") return config @@ -1122,7 +1121,7 @@ def __init__( epsilon=1e-07, **kwargs, ): - super(WeightNorm_Conv, self).__init__(**kwargs) + super().__init__(**kwargs) if self.rank == 1: self.data_format = "channels_last" self.gamma_initializer = sanitizedInitGet(gamma_initializer) @@ -1132,7 +1131,7 @@ def __init__( self.gamma = None def build(self, input_shape): - super(WeightNorm_Conv, self).build(input_shape) + super().build(input_shape) if self.data_format == "channels_first": channel_axis = 1 else: @@ -1201,7 +1200,7 @@ def get_config(self): "gamma_constraint": constraints.serialize(self.gamma_constraint), "epsilon": self.epsilon, } - base_config = super(WeightNorm_Conv, self).get_config() + base_config = super().get_config() return dict(list(base_config.items()) + list(config.items())) diff --git a/complexnn/conv_utils.py b/complexnn/conv_utils.py index d5e2c36..9094b69 100644 --- a/complexnn/conv_utils.py +++ b/complexnn/conv_utils.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2015 The TensorFlow Authors. All Rights Reserved. # diff --git a/complexnn/dense.py b/complexnn/dense.py index 9e0d05f..ee923a9 100644 --- a/complexnn/dense.py +++ b/complexnn/dense.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import numpy as np from numpy.random import RandomState @@ -87,7 +86,7 @@ def __init__( if "input_shape" not in kwargs and "input_dim" in kwargs: kwargs["input_shape"] = (kwargs.pop("input_dim"),) with _ignore_input_shape_warning(kwargs): - super(ComplexDense, self).__init__(**kwargs) + super().__init__(**kwargs) self.units = units self.activation = activations.get(activation) self.use_bias = use_bias @@ -225,5 +224,5 @@ def get_config(self): "bias_constraint": constraints.serialize(self.bias_constraint), "seed": self.seed, } - base_config = super(ComplexDense, self).get_config() + base_config = super().get_config() return dict(list(base_config.items()) + list(config.items())) diff --git a/complexnn/fft.py b/complexnn/fft.py index fe6638e..b42ffc9 100644 --- a/complexnn/fft.py +++ b/complexnn/fft.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # import tensorflow.keras.engine as KE import numpy as np diff --git a/complexnn/init.py b/complexnn/init.py index fb25351..fca8436 100644 --- a/complexnn/init.py +++ b/complexnn/init.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import numpy as np import tensorflow.keras.backend as K diff --git a/complexnn/norm.py b/complexnn/norm.py index 90ff994..1ebb11a 100644 --- a/complexnn/norm.py +++ b/complexnn/norm.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Implementation of Layer Normalization and Complex Layer Normalization @@ -67,7 +66,7 @@ def __init__( self.beta_regularizer = regularizers.get(beta_regularizer) with _ignore_input_shape_warning(kwargs): - super(LayerNormalization, self).__init__(**kwargs) + super().__init__(**kwargs) def build(self, input_shape): self.input_spec = InputSpec( @@ -79,13 +78,13 @@ def build(self, input_shape): shape=shape, initializer=self.gamma_init, regularizer=self.gamma_regularizer, - name="{}_gamma".format(self.name), + name=f"{self.name}_gamma", ) self.beta = self.add_weight( shape=shape, initializer=self.beta_init, regularizer=self.beta_regularizer, - name="{}_beta".format(self.name), + name=f"{self.name}_beta", ) self.built = True @@ -103,7 +102,7 @@ def get_config(self): "gamma_regularizer": regularizers.serialize(self.gamma_regularizer), "beta_regularizer": regularizers.serialize(self.beta_regularizer), } - base_config = super(LayerNormalization, self).get_config() + base_config = super().get_config() return dict(list(base_config.items()) + list(config.items())) @@ -143,7 +142,7 @@ def __init__( self.gamma_diag_constraint = constraints.get(gamma_diag_constraint) self.gamma_off_constraint = constraints.get(gamma_off_constraint) with _ignore_input_shape_warning(kwargs): - super(ComplexLayerNorm, self).__init__(**kwargs) + super().__init__(**kwargs) def build(self, input_shape): dim = input_shape[self.axis] @@ -300,5 +299,5 @@ def get_config(self): "gamma_diag_constraint": constraints.serialize(self.gamma_diag_constraint), "gamma_off_constraint": constraints.serialize(self.gamma_off_constraint), } - base_config = super(ComplexLayerNorm, self).get_config() + base_config = super().get_config() return dict(list(base_config.items()) + list(config.items())) diff --git a/complexnn/pool.py b/complexnn/pool.py index 0e638d6..198ce76 100644 --- a/complexnn/pool.py +++ b/complexnn/pool.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import numpy as np import tensorflow.keras as KE @@ -15,7 +14,7 @@ @register_keras_serializable(package="complexnn") class SpectralPooling1D(KL.Layer): def __init__(self, topf=(0,), **kwargs): - super(SpectralPooling1D, self).__init__() + super().__init__() if "topf" in kwargs: self.topf = (int(kwargs["topf"][0]),) self.topf = (self.topf[0] // 2,) @@ -60,7 +59,7 @@ def call(self, x, mask=None): @register_keras_serializable(package="complexnn") class SpectralPooling2D(KL.Layer): def __init__(self, **kwargs): - super(SpectralPooling2D, self).__init__() + super().__init__() if "topf" in kwargs: self.topf = (int(kwargs["topf"][0]), int(kwargs["topf"][1])) self.topf = (self.topf[0] // 2, self.topf[1] // 2) diff --git a/complexnn/utils.py b/complexnn/utils.py index f0cd07b..2a8027e 100644 --- a/complexnn/utils.py +++ b/complexnn/utils.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import contextlib import re @@ -236,7 +235,7 @@ class _ComplexPart(Layer): def build(self, input_shape): _check_part_shape(self, input_shape) - super(_ComplexPart, self).build(input_shape) + super().build(input_shape) def call(self, inputs): _check_part_shape(self, inputs.shape) diff --git a/tests/test_conv.py b/tests/test_conv.py index 367682e..85d945a 100644 --- a/tests/test_conv.py +++ b/tests/test_conv.py @@ -1,9 +1,8 @@ import unittest import numpy as np -import tensorflow as tf -from tensorflow.keras.layers import Dense, Input, MaxPooling2D -from tensorflow.keras.models import Model, Sequential +from tensorflow.keras.layers import Input +from tensorflow.keras.models import Model import complexnn as conn diff --git a/tests/test_dense.py b/tests/test_dense.py index 0e4c9a2..13de554 100644 --- a/tests/test_dense.py +++ b/tests/test_dense.py @@ -2,8 +2,6 @@ import numpy as np import tensorflow as tf -from tensorflow.keras.layers import Dense, Input, MaxPooling2D -from tensorflow.keras.models import Model, Sequential import complexnn as conn diff --git a/tests/test_init.py b/tests/test_init.py index 0695ab5..97ad44c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,8 +1,9 @@ import unittest -import complexnn as conn import numpy as np +import complexnn as conn + def make_initializers(): """One of each complex initialiser, each filling a (4, 6) dense kernel""" diff --git a/tests/test_readme.py b/tests/test_readme.py index 9f436cd..51c3c98 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -1,9 +1,7 @@ import unittest -import numpy as np import tensorflow as tf -from tensorflow.keras.layers import Dense, Input, MaxPooling2D -from tensorflow.keras.models import Model, Sequential +from tensorflow.keras.layers import MaxPooling2D import complexnn as conn diff --git a/tests/test_saving.py b/tests/test_saving.py index 627c6f3..6dabb9e 100644 --- a/tests/test_saving.py +++ b/tests/test_saving.py @@ -5,11 +5,12 @@ import unittest import warnings -import complexnn as conn import numpy as np import tensorflow as tf from tensorflow.keras.utils import get_registered_name, get_registered_object +import complexnn as conn + # Every working layer, with an input shape it accepts. Each entry builds a new # layer, so tests never share an instance. Layers that take arguments set some # of them away from their defaults: a value that from_config does not restore diff --git a/tests/test_sequential.py b/tests/test_sequential.py index 4e82738..1d80e3f 100644 --- a/tests/test_sequential.py +++ b/tests/test_sequential.py @@ -1,9 +1,10 @@ import unittest -import complexnn as conn import numpy as np import tensorflow as tf +import complexnn as conn + class TestLayersInSequential(unittest.TestCase): """Each layer builds, predicts and trains inside a Sequential model (issue #23)""" diff --git a/tests/test_train.py b/tests/test_train.py index 75452f0..e6630a7 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -1,8 +1,7 @@ import unittest import numpy as np -import tensorflow as tf -from tensorflow.keras.layers import Dense, Input, MaxPooling2D +from tensorflow.keras.layers import Dense, Input from tensorflow.keras.models import Model, Sequential import complexnn as conn From e96dff1da52c2fb7e61dacbf9f0684263fcfca60 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 20:49:42 +0200 Subject: [PATCH 07/12] style: apply ruff format --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8eecc9b..df4ce87 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ To turn a complex NumPy array into this format: ```python import numpy as np -z = np.random.randn(8, 3, 3, 2) + 1j * np.random.randn(8, 3, 3, 2) # two complex channels +z = np.random.randn(8, 3, 3, 2) + 1j * np.random.randn( + 8, 3, 3, 2 +) # two complex channels x = np.concatenate([z.real, z.imag], axis=-1) # shape (8, 3, 3, 4) ``` From 8da29e602bedfd71a0492e6ddd3bf42ac48d7038 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:07:46 +0200 Subject: [PATCH 08/12] build: run ruff check and ruff format from pre-commit Add the ruff-check (with --fix) and ruff-format hooks from astral-sh/ruff-pre-commit v0.17.0 after the existing read-only hooks, which stay as they were. Both hooks read the settings in pyproject.toml. The lint hook runs first, as the ruff-pre-commit README asks when fixes are enabled. pre-commit.ci already runs this file on the repository, so it will now also apply ruff fixes to pull requests. --- .pre-commit-config.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bc23a9..8ffcd06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,5 @@ -# Read-only checks for now; formatting and linting hooks come with the tooling update. +# Read-only checks first, then ruff lints (fixing what it safely can) and formats +# the Python files with the settings in pyproject.toml. repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -7,3 +8,10 @@ repos: - id: check-toml - id: check-merge-conflict - id: check-added-large-files + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.17.0 + hooks: + # Lint before formatting, as ruff recommends when fixes are on. + - id: ruff-check + args: [--fix] + - id: ruff-format From 9cbee498364b32bbbaeff5c8cdb622c249e97ce5 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:08:55 +0200 Subject: [PATCH 09/12] ci: lint with ruff check and ruff format instead of flake8 The lint job ran flake8 for syntax errors and undefined names only. Run `ruff check` and `ruff format --check --diff` instead. Both use the settings in pyproject.toml, and ruff check reports findings as GitHub annotations. The job reads the ruff version from the ruff-pre-commit rev in .pre-commit-config.yaml, so CI and the hooks cannot drift apart when pre-commit.ci updates the pin. A comment next to the rev says so. The step fails if it cannot find the version. --- .github/workflows/test_python.yml | 14 ++++++++++++-- .pre-commit-config.yaml | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index bc793c3..ac305d4 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -13,8 +13,18 @@ jobs: - uses: astral-sh/setup-uv@v10.3.0 with: python-version: "3.12" - # Stop on syntax errors and undefined names only. - - run: uvx flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # Run the ruff release that .pre-commit-config.yaml pins, so this job and the + # pre-commit hooks agree when pre-commit.ci updates the pin. + - name: Find the ruff version + id: ruff + run: | + version=$(grep -A1 "astral-sh/ruff-pre-commit" .pre-commit-config.yaml | sed -n "s/^ *rev: *v//p") + test -n "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: ruff check + run: uvx "ruff@${{ steps.ruff.outputs.version }}" check --output-format=github . + - name: ruff format + run: uvx "ruff@${{ steps.ruff.outputs.version }}" format --check --diff . # 0.2.3 shipped invalid metadata that pip 24.1 and later refuse to install. # Build the package and install it the way users do before any release. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ffcd06..989a0ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,8 @@ repos: - id: check-toml - id: check-merge-conflict - id: check-added-large-files + # The lint job in .github/workflows/test_python.yml reads this rev, so keep it + # on the line directly below the repo line. - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.17.0 hooks: From 859bcc99f174c2ee8eea78ac13621de810f2e6c9 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:09:34 +0200 Subject: [PATCH 10/12] ci: add Dependabot for GitHub Actions and pip Adapted from the github-files template that #25 would add. Both ecosystems update monthly instead of daily, with one grouped pull request per ecosystem, which suits a library that only needs to keep working. The pip entry watches pyproject.toml and docs/requirements.txt with versioning-strategy increase-if-necessary, so Dependabot never raises the tensorflow>=2.15 lower bound that Keras 2 users depend on. The GitHub Actions entry will also propose replacing the unsupported github/codeql-action@v2 in codeql.yml (only v3 and v4 are supported). --- .github/dependabot.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7214b58 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +# Monthly updates, grouped into one pull request per ecosystem. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + commit-message: + prefix: ci + groups: + github-actions: + patterns: ["*"] + + # pyproject.toml and docs/requirements.txt only set lower bounds. Keep them + # unless a new release falls outside them: tensorflow>=2.15 must not be raised, + # because TensorFlow 2.15 is how Keras 2 users run this package. + - package-ecosystem: pip + directories: + - / + - /docs + schedule: + interval: monthly + versioning-strategy: increase-if-necessary + commit-message: + prefix: build + groups: + python: + patterns: ["*"] From 80bf2178ea557e0e7e3d57a8b5807ceead41b685 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 17:12:56 +0200 Subject: [PATCH 11/12] ci: label pull requests by the files they change Take .github/labeler.yml and .github/workflows/labeler.yml unchanged from JesperDramsch/github-files, as #25 would. Keeping them identical to the template lets the github-files "label sync" group keep updating this repository without overwriting anything specific to it. The workflow runs actions/labeler v7.0.0 on pull_request_target with pull-requests: write and never checks out the pull request's code. actions/labeler v7 only understands changed-files, head-branch and base-branch, so the template's title and body rules for the "breaking" label are logged as unknown and that label is never applied. That needs fixing in the template, not here. --- .github/labeler.yml | 63 +++++++++++++++++++++++++++++++++++ .github/workflows/labeler.yml | 12 +++++++ 2 files changed, 75 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..aa9b60f --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,63 @@ +Tests: +- changed-files: + - any-glob-to-any-file: [ "tests/**/*" ] + +Dependencies: +- changed-files: + - any-glob-to-any-file: + - "requirements.txt" + - "setup.py" + - "pyproject.toml" + - "poetry.lock" + - "pixi.toml" + - "pixi.lock" + - "Pipfile" + - "Pipfile.lock" + - "requirements/*.txt" + - "requirements/*.in" + +Build: +- changed-files: + - any-glob-to-any-file: + - "Dockerfile*" + - "docker-compose*.yml" + - "Makefile" + +Documentation: +- changed-files: + - any-glob-to-any-file: + - "docs/**/*" + - "*.md" + - "*.rst" + +Config: +- changed-files: + - any-glob-to-any-file: + - ".pre-commit-config.yaml" + - "config/**/*" + - "settings/**/*" + - "*.ini" + - "*.cfg" + - "*.conf" + +CI/CD: +- changed-files: + - any-glob-to-any-file: + - ".github/**/*" + - "tox.ini" + - ".coveragerc" + +# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name +feature: +- head-branch: + - '^feature' + - 'feature' + +# Add 'bugfix' label to branches with bug/fix/hotfix prefixes +bugfix: +- head-branch: [ '^bug', '^fix', '^hotfix' ] + +# Add 'breaking' label for major version bumps or breaking change commits +breaking: +- title: [ '^BREAKING CHANGE', 'BREAKING-CHANGE' ] +- body: [ 'BREAKING CHANGE:', 'BREAKING-CHANGE:' ] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..bf2103b --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v7.0.0 From d4e818c25d27b491dd224625ac16534930b73520 Mon Sep 17 00:00:00 2001 From: Jesper Dramsch Date: Mon, 12 Oct 2026 20:02:31 +0200 Subject: [PATCH 12/12] docs(ci): explain why the pip Dependabot entry opens no pull requests yet --- .github/dependabot.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7214b58..1d48bc6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,4 +1,5 @@ -# Monthly updates, grouped into one pull request per ecosystem. +# Checked monthly, with at most one grouped pull request per ecosystem. +# For now only github-actions opens any: see the comment on the pip entry. version: 2 updates: - package-ecosystem: github-actions @@ -11,9 +12,13 @@ updates: github-actions: patterns: ["*"] - # pyproject.toml and docs/requirements.txt only set lower bounds. Keep them - # unless a new release falls outside them: tensorflow>=2.15 must not be raised, - # because TensorFlow 2.15 is how Keras 2 users run this package. + # pyproject.toml and docs/requirements.txt only set lower bounds, and there is + # no lockfile. increase-if-necessary leaves a requirement unchanged while it + # allows the new release, and a lower bound allows every release, so this + # entry opens no version-update pull requests for now. It takes effect once a + # pin or an upper bound is added, and such a pull request may raise the lower + # bound too: tensorflow>=2.15 must stay, because TensorFlow 2.15 is how + # Keras 2 users run this package. - package-ecosystem: pip directories: - /