diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1d48bc6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +# 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 + directory: / + schedule: + interval: monthly + commit-message: + prefix: ci + groups: + github-actions: + patterns: ["*"] + + # 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: + - / + - /docs + schedule: + interval: monthly + versioning-strategy: increase-if-necessary + commit-message: + prefix: build + groups: + python: + patterns: ["*"] 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 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 1bc23a9..989a0ac 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,12 @@ 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: + # Lint before formatting, as ruff recommends when fixes are on. + - id: ruff-check + args: [--fix] + - id: ruff-format 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) ``` diff --git a/complexnn/__init__.py b/complexnn/__init__.py index 4d6c1b1..9f398da 100644 --- a/complexnn/__init__.py +++ b/complexnn/__init__.py @@ -1,9 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from . import bn, conv, dense, init, norm, pool - -# from . import fft +from . import bn, conv, conv_utils, dense, fft, init, norm, pool, utils from .bn import ComplexBatchNormalization as ComplexBN from .conv import ( ComplexConv, @@ -31,3 +28,36 @@ get_realpart, getpart_output_shape, ) + +__all__ = [ + "bn", + "conv", + "conv_utils", + "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/complexnn/bn.py b/complexnn/bn.py index 0682517..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 @@ -359,8 +354,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( @@ -583,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 1753037..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 @@ -180,11 +179,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 ) @@ -230,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 b5d3576..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,10 +142,9 @@ 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): - ndim = len(input_shape) dim = input_shape[self.axis] if dim is None: raise ValueError( @@ -301,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/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", +] diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..4528b30 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,57 @@ +import unittest + +import complexnn + +# Every public name that `import complexnn` provides, submodules included. +PUBLIC_NAMES = { + "bn", + "conv", + "conv_utils", + "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) + + def test_all(self): + """__all__ lists each public name once""" + self.assertCountEqual(complexnn.__all__, 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..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 @@ -57,6 +56,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..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 @@ -19,6 +17,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_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_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)) 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