import time

import numpy as np
import pytest

import aesara
import aesara.tensor as aet
from aesara.compile.mode import Mode
from aesara.tensor.exceptions import NotScalarConstantError
from aesara.tensor.math import _allclose, exp
from aesara.tensor.nnet import conv, conv2d
from aesara.tensor.type import dmatrix, dtensor3, dtensor4, dvector, scalar, tensor4
from tests import unittest_tools as utt


@pytest.mark.skipif(
    not conv.imported_scipy_signal and aesara.config.cxx == "",
    reason="conv2d tests need SciPy or a c++ compiler",
)
class TestConv2D(utt.InferShapeTester):
    # This class contains tests for the legacy 2d convolution,
    # but will also be inherited from for other implementations
    mode = None
    dtype = aesara.config.floatX
    # This will be set to the appropriate function in the inherited classes.
    # The call to `staticmethod` is necessary to prevent Python from passing
    # `self` as the first argument.
    conv2d = staticmethod(conv.conv2d)

    def setup_method(self):
        self.input = tensor4("input", dtype=self.dtype)
        self.input.name = "default_V"
        self.filters = tensor4("filters", dtype=self.dtype)
        self.filters.name = "default_filters"
        super().setup_method()

    def validate(
        self,
        image_shape,
        filter_shape,
        border_mode="valid",
        subsample=(1, 1),
        N_image_shape=None,
        N_filter_shape=None,
        input=None,
        filters=None,
        unroll_batch=None,
        unroll_kern=None,
        unroll_patch=None,
        verify_grad=True,
        should_raise=False,
    ):
        """
        :param image_shape: The constant shape info passed to conv2d.
        :param filter_shape: The constant shape info passed to conv2d.

        :param N_image_shape: None(default to image_shape) or tuple of
                              4 elements with the shape of the input image

        :param N_filter_shape: None(default to filter_shape) or tuple
                               of 4 elements with the shape of the
                               input filter

        """
        if N_image_shape is None:
            N_image_shape = [
                aet.get_scalar_constant_value(aet.as_tensor_variable(x))
                for x in image_shape
            ]
        if N_filter_shape is None:
            N_filter_shape = [
                aet.get_scalar_constant_value(aet.as_tensor_variable(x))
                for x in filter_shape
            ]

        if input is None:
            input = self.input
        if not filters:
            filters = self.filters

        # AESARA IMPLEMENTATION

        # we create a symbolic function so that verify_grad can work
        def sym_conv2d(input, filters):
            # define aesara graph and function
            input.name = "input"
            filters.name = "filters"
            rval = conv.conv2d(
                input,
                filters,
                image_shape,
                filter_shape,
                border_mode,
                subsample,
                unroll_batch=unroll_batch,
                unroll_kern=unroll_kern,
                unroll_patch=unroll_patch,
            )
            rval.name = "conv_output"
            return rval

        output = sym_conv2d(input, filters)
        output.name = f"conv2d({input.name},{filters.name})"
        aesara_conv = aesara.function([input, filters], output, mode=self.mode)

        # initialize input and compute result
        image_data = np.random.random(N_image_shape).astype(self.dtype)
        filter_data = np.random.random(N_filter_shape).astype(self.dtype)
        try:
            aesara_output = aesara_conv(image_data, filter_data)
        except ValueError:
            if not should_raise:
                raise
            return
        else:
            if should_raise:
                raise Exception("ConvOp should have generated an error")

        # REFERENCE IMPLEMENTATION
        s = 1.0
        orig_image_data = image_data
        if border_mode != "full":
            s = -1.0
        out_shape2d = (
            np.array(N_image_shape[-2:]) + s * np.array(N_filter_shape[-2:]) - s
        )
        out_shape2d = np.ceil(out_shape2d / np.array(subsample))
        # avoid numpy deprecation
        out_shape2d = out_shape2d.astype("int32")
        out_shape = (N_image_shape[0], N_filter_shape[0]) + tuple(out_shape2d)
        ref_output = np.zeros(out_shape)

        # loop over output feature maps
        ref_output.fill(0)
        if border_mode == "full":
            image_data2 = np.zeros(
                (
                    N_image_shape[0],
                    N_image_shape[1],
                    N_image_shape[2] + 2 * N_filter_shape[2] - 2,
                    N_image_shape[3] + 2 * N_filter_shape[3] - 2,
                )
            )
            image_data2[
                :,
                :,
                N_filter_shape[2] - 1 : N_filter_shape[2] - 1 + N_image_shape[2],
                N_filter_shape[3] - 1 : N_filter_shape[3] - 1 + N_image_shape[3],
            ] = image_data
            image_data = image_data2
            N_image_shape = image_data.shape
        for bb in range(N_image_shape[0]):
            for nn in range(N_filter_shape[0]):
                for im0 in range(N_image_shape[1]):
                    filter2d = filter_data[nn, im0, :, :]
                    image2d = image_data[bb, im0, :, :]
                    for row in range(ref_output.shape[2]):
                        irow = row * subsample[0]  # image row
                        for col in range(ref_output.shape[3]):
                            icol = col * subsample[1]  # image col
                            ref_output[bb, nn, row, col] += (
                                image2d[
                                    irow : irow + N_filter_shape[2],
                                    icol : icol + N_filter_shape[3],
                                ]
                                * filter2d[::-1, ::-1]
                            ).sum()

        assert _allclose(aesara_output, ref_output)

        # TEST GRADIENT
        if verify_grad:
            utt.verify_grad(sym_conv2d, [orig_image_data, filter_data])

    def test_basic1(self):
        # Tests that basic convolutions work for odd and even
        # dimensions of image and filter shapes, as well as rectangular
        # images and filters.

        self.validate((2, 2, 3, 3), (2, 2, 2, 2), "valid", verify_grad=False)

    def test_basic(self):
        # Tests that basic convolutions work for odd and even
        # dimensions of image and filter shapes, as well as rectangular
        # images and filters.

        self.validate((3, 2, 8, 8), (4, 2, 5, 5), "valid", verify_grad=False)
        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "valid")
        self.validate((3, 2, 7, 5), (5, 2, 3, 2), "valid", verify_grad=False)
        self.validate((3, 2, 8, 8), (4, 2, 5, 5), "full", verify_grad=False)
        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "full")
        # test filter same size as input

    def test_uint_image_shape_datatype(self):
        # Tests for uint datatype in image_shape.

        self.validate((2, 2, 3, np.uint8(3)), (3, 2, 3, 3), "valid", verify_grad=False)
        self.validate((np.uint16(2), 2, 3, 3), (3, 2, 3, 3), "valid", verify_grad=False)
        self.validate((2, np.uint32(2), 3, 3), (3, 2, 3, 3), "valid", verify_grad=False)

    def test_uint_filter_shape_datatype(self):
        # Tests for uint datatype in filter_shape

        self.validate((3, 2, 3, 3), (2, 2, 3, np.uint8(3)), "valid", verify_grad=False)
        self.validate((3, 2, 3, 3), (np.uint16(2), 2, 3, 3), "valid", verify_grad=False)
        self.validate((3, 2, 3, 3), (2, np.uint32(2), 3, 3), "valid", verify_grad=False)

    def test_img_kernel_same_shape(self):
        self.validate((3, 2, 3, 3), (4, 2, 3, 3), "full")
        self.validate((3, 2, 3, 3), (4, 2, 3, 3), "valid")

    def test_unroll_patch_true(self):
        # Test basic convs with True.

        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "valid", unroll_patch=True)
        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "full", unroll_patch=True)
        self.validate(
            (3, 2, 3, 3), (4, 2, 3, 3), "valid", unroll_patch=True, verify_grad=False
        )

    def test_unroll_patch_false(self):
        # Test basic convs with False.

        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "valid", unroll_patch=False)
        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "full", unroll_patch=False)
        self.validate(
            (3, 2, 3, 3), (4, 2, 3, 3), "valid", unroll_patch=False, verify_grad=False
        )

    def test_unroll_patch_true_fail(self):
        # Test basic convs with True.

        self.validate(
            (3, 2, 7, 5),
            (5, 2, 2, 3),
            "valid",
            unroll_patch=True,
            N_image_shape=(1, 3, 3, 3),
            N_filter_shape=(6, 3, 2, 2),
            should_raise=True,
        )
        self.validate(
            (3, 2, 7, 5),
            (5, 2, 2, 3),
            "full",
            unroll_patch=True,
            N_image_shape=(1, 3, 3, 3),
            N_filter_shape=(6, 3, 2, 2),
            should_raise=True,
        )
        self.validate(
            (3, 2, 3, 3),
            (4, 2, 3, 3),
            "valid",
            unroll_patch=True,
            N_image_shape=(1, 3, 3, 3),
            N_filter_shape=(6, 3, 2, 2),
            should_raise=True,
        )

    def test_unroll_special(self):
        # (unroll_kern, unroll_batch) in (0,1),(1,0) is special case.

        self.validate((6, 2, 3, 3), (3, 2, 2, 2), "valid", unroll_batch=1)

    def test_unroll_batch(self):
        # Test mini-batch unrolling for various legal values.

        # mini-batch of size 6 is multiple of 2 and 3. Should work.
        self.validate(
            (6, 2, 3, 3), (3, 2, 2, 2), "valid", unroll_batch=2, verify_grad=False
        )
        self.validate(
            (6, 2, 3, 3), (3, 2, 2, 2), "valid", unroll_batch=3, verify_grad=False
        )

    def test_unroll_kern(self):
        # Test kernel unrolling for various legal values.

        # 6 filters is a multiple of 2 and 3. Should work.
        self.validate(
            (2, 3, 3, 3), (6, 3, 2, 2), "valid", unroll_kern=2, verify_grad=False
        )
        self.validate(
            (2, 3, 3, 3), (6, 3, 2, 2), "valid", unroll_kern=3, verify_grad=False
        )

    def test_unroll_batch_kern(self):
        # Test mini-batch unrolling with kernel unrolling for various
        # legal values.

        # mini-batch of size 6 is multiple of 2 and 3. Should work.
        self.validate(
            (6, 2, 3, 3),
            (3, 2, 2, 2),
            "valid",
            unroll_batch=2,
            unroll_kern=3,
            verify_grad=False,
        )
        self.validate(
            (6, 2, 3, 3),
            (3, 2, 2, 2),
            "valid",
            unroll_batch=3,
            unroll_kern=3,
            verify_grad=False,
        )
        # 6 filters is a multiple of 2 and 3. Should work.
        self.validate(
            (2, 3, 3, 3),
            (6, 3, 2, 2),
            "valid",
            unroll_batch=2,
            unroll_kern=2,
            verify_grad=False,
        )
        self.validate(
            (2, 3, 3, 3),
            (6, 3, 2, 2),
            "valid",
            unroll_batch=2,
            unroll_kern=3,
            verify_grad=False,
        )

    def test_unroll_batch_kern_fail(self):
        # Test mini-batch unrolling with kernel unrolling for various
        # legal values, but pass bad input.  All those test must
        # generate errors

        # mini-batch of size 6 is multiple of 2 and 3. Should work.
        self.validate(
            (6, 2, 3, 3),
            (3, 2, 2, 2),
            "valid",
            unroll_batch=2,
            unroll_kern=3,
            N_image_shape=(7, 2, 3, 3),
            N_filter_shape=(3, 2, 2, 2),
            should_raise=True,
        )
        self.validate(
            (6, 2, 3, 3),
            (3, 2, 2, 2),
            "valid",
            unroll_batch=3,
            unroll_kern=3,
            N_image_shape=(6, 2, 3, 3),
            N_filter_shape=(4, 2, 2, 2),
            should_raise=True,
        )
        self.validate(
            (2, 3, 3, 3),
            (6, 3, 2, 2),
            "valid",
            unroll_batch=2,
            unroll_kern=2,
            N_image_shape=(1, 3, 3, 3),
            N_filter_shape=(6, 3, 2, 2),
            should_raise=True,
        )
        self.validate(
            (2, 3, 3, 3),
            (6, 3, 2, 2),
            "valid",
            unroll_batch=2,
            unroll_kern=3,
            N_image_shape=(2, 3, 3, 3),
            N_filter_shape=(5, 3, 2, 2),
            should_raise=True,
        )

    @pytest.mark.slow
    def test_subsample(self):
        # Tests convolution where subsampling != (1,1)
        self.validate((3, 2, 7, 5), (5, 2, 2, 3), "full", subsample=(2, 2))

        # Fails as of 2012-07-11
        with pytest.raises(NotImplementedError):
            self.validate((1, 1, 6, 6), (1, 1, 3, 3), "full", subsample=(3, 3))

        # Fails as of 2017-08-10
        with pytest.raises(NotImplementedError):
            self.validate((3, 2, 7, 5), (5, 2, 2, 3), "valid", subsample=(2, 2))
        with pytest.raises(NotImplementedError):
            self.validate((3, 2, 7, 5), (5, 2, 2, 3), "valid", subsample=(2, 1))
        with pytest.raises(NotImplementedError):
            self.validate((1, 1, 6, 6), (1, 1, 3, 3), "valid", subsample=(3, 3))

    def test_shape_Constant_tensor(self):
        # Tests convolution where the {image,filter}_shape is a Constant tensor.

        as_t = aet.as_tensor_variable
        self.validate((as_t(3), as_t(2), as_t(7), as_t(5)), (5, 2, 2, 3), "valid")
        self.validate(as_t([3, 2, 7, 5]), (5, 2, 2, 3), "valid")
        self.validate(as_t((3, 2, 7, 5)), (5, 2, 2, 3), "valid")
        self.validate((3, 2, 7, 5), (as_t(5), as_t(2), as_t(2), as_t(3)), "valid")
        self.validate((3, 2, 7, 5), as_t([5, 2, 2, 3]), "valid")
        self.validate((3, 2, 7, 5), as_t((5, 2, 2, 3)), "valid")
        self.validate(as_t([3, 2, 7, 5]), as_t([5, 2, 2, 3]), "full")

    def test_invalid_filter_shape(self):
        # Tests scenario where filter_shape[1] != input_shape[1]

        with pytest.raises(AssertionError):
            self.validate((3, 2, 8, 8), (4, 3, 5, 5), "valid")

    @pytest.mark.slow
    def test_invalid_input_shape(self):
        # Tests that when the shape gived at build time is not the same as
        # run time we raise an error

        for unroll_batch in [None, 1, 3]:
            for unroll_kern in [None, 2, 4]:
                for unroll_patch in [None, True, False]:
                    for mode in ["valid", "full"]:
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_image_shape=(2, 2, 8, 8),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_image_shape=(3, 1, 8, 8),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_image_shape=(3, 2, 7, 8),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_image_shape=(3, 2, 8, 7),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )

                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_filter_shape=(3, 2, 5, 5),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_filter_shape=(4, 1, 5, 5),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_filter_shape=(4, 2, 6, 5),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )
                        with pytest.raises(ValueError):
                            self.validate(
                                (3, 2, 8, 8),
                                (4, 2, 5, 5),
                                mode,
                                N_filter_shape=(4, 2, 5, 6),
                                unroll_batch=unroll_batch,
                                unroll_kern=unroll_kern,
                                unroll_patch=unroll_patch,
                            )

    def test_missing_info(self):
        # Test convolutions for various pieces of missing info.

        self.validate(
            None, None, N_image_shape=(3, 2, 8, 8), N_filter_shape=(4, 2, 5, 5)
        )
        self.validate(
            (3, 2, None, None),
            None,
            N_image_shape=(3, 2, 8, 8),
            N_filter_shape=(4, 2, 5, 5),
        )
        self.validate(
            (None, 2, None, None),
            (None, 2, 5, 5),
            N_image_shape=(3, 2, 8, 8),
            N_filter_shape=(4, 2, 5, 5),
        )
        self.validate(
            (3, 2, 8, 8),
            (4, 2, None, 5),
            N_image_shape=(3, 2, 8, 8),
            N_filter_shape=(4, 2, 5, 5),
        )
        self.validate(
            (3, 2, 8, 8),
            (4, 2, 5, None),
            N_image_shape=(3, 2, 8, 8),
            N_filter_shape=(4, 2, 5, 5),
        )

    def test_wrong_info(self):
        # Test convolutions when we don't give a constant as shape information

        i = aesara.scalar.basic.int32()
        with pytest.raises(NotScalarConstantError):
            self.validate(
                (3, 2, 8, i),
                (4, 2, 5, 5),
                N_image_shape=(3, 2, 8, 8),
                N_filter_shape=(4, 2, 5, 5),
            )
        with pytest.raises(NotScalarConstantError):
            self.validate(
                (3, 2, 8, 8),
                (4, 2, 5, i),
                N_image_shape=(3, 2, 8, 8),
                N_filter_shape=(4, 2, 5, 5),
            )

    def test_full_mode(self):
        # Tests basic convolution in full mode and case where filter
        # is larger than the input image.

        self.validate((3, 2, 5, 5), (4, 2, 8, 8), "full")

        def f():
            self.validate((3, 2, 5, 5), (4, 2, 8, 8), "valid")

        with pytest.raises(Exception):
            f()

    def test_wrong_input(self):
        # Make sure errors are raised when image and kernel are not 4D tensors

        with pytest.raises(Exception):
            self.validate((3, 2, 8, 8), (4, 2, 5, 5), "valid", input=dmatrix())
        with pytest.raises(Exception):
            self.validate((3, 2, 8, 8), (4, 2, 5, 5), "valid", filters=dvector())
        with pytest.raises(Exception):
            self.validate((3, 2, 8, 8), (4, 2, 5, 5), "valid", input=dtensor3())

    def test_gcc_crash(self):
        # gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
        #
        # crashed in this following case. I changed the c code to don't hit
        # gcc bug. So it should not crash anymore

        self.validate((1, 10, 213, 129), (46, 10, 212, 1), "valid", verify_grad=False)

    def speed(self):
        n_calls = 20000
        print("n_calls", n_calls)
        for border_mode in ["valid", "full"]:
            print()
            print(border_mode)
            for openmp in [False, True]:
                print("OpenMP", openmp)
                image_shapes = [
                    (1, 5, 6, 6),
                    (10, 5, 6, 6)
                    # (10, 10, 16, 16),
                    # (10, 10, 32, 32)]
                ]
                print("image_shape", image_shapes)
                for image_shape in image_shapes:
                    filter_shapes = [(1, 5, 4, 4), (2, 5, 4, 4), (5, 5, 4, 4)]
                    print("filter_shapes", filter_shapes)
                    for filter_shape in filter_shapes:

                        input = aesara.shared(np.random.random(image_shape))
                        filters = aesara.shared(np.random.random(filter_shape))

                        output = self.conv2d(
                            input,
                            filters,
                            image_shape,
                            filter_shape,
                            border_mode,
                            unroll_patch=True,
                            openmp=openmp,
                        )
                        mode = Mode(
                            linker=aesara.link.vm.VMLinker(
                                allow_gc=False, use_cloop=True
                            )
                        )
                        aesara_conv = aesara.function([], output, mode=mode)
                        t1 = time.time()
                        aesara_conv.fn(n_calls=n_calls)
                        t2 = time.time()
                        print(t2 - t1, end=" ")
                    print()

    def test_infer_shape(self):
        # Note: infer_shape is incomplete and thus input and filter shapes
        # must be provided explicitly

        def rand(*shape):
            r = np.asarray(np.random.rand(*shape), dtype="float64")
            return r * 2 - 1

        adtens = dtensor4()
        bdtens = dtensor4()
        aivec_val = [4, 5, 6, 3]
        bivec_val = [7, 5, 3, 2]
        adtens_val = rand(*aivec_val)
        bdtens_val = rand(*bivec_val)
        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="valid")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="full")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        aivec_val = [6, 2, 8, 3]
        bivec_val = [4, 2, 5, 3]
        adtens_val = rand(*aivec_val)
        bdtens_val = rand(*bivec_val)
        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="valid")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="full")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        aivec_val = [3, 6, 7, 5]
        bivec_val = [5, 6, 3, 2]
        adtens_val = rand(*aivec_val)
        bdtens_val = rand(*bivec_val)
        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="valid")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="full")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        aivec_val = [3, 6, 7, 5]
        bivec_val = [5, 6, 2, 3]
        adtens_val = rand(*aivec_val)
        bdtens_val = rand(*bivec_val)
        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="valid")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="full")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        aivec_val = [5, 2, 4, 3]
        bivec_val = [6, 2, 4, 3]
        adtens_val = rand(*aivec_val)
        bdtens_val = rand(*bivec_val)
        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="valid")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )

        self._compile_and_check(
            [adtens, bdtens],
            [self.conv2d(adtens, bdtens, aivec_val, bivec_val, border_mode="full")],
            [adtens_val, bdtens_val],
            conv.ConvOp,
            excluding=["conv_gemm"],
        )


# Test that broadcasting of gradients works correctly when using the
# nnet.conv2d() interface. This was reported in #3763, and uses the example
# code from that ticket.
def test_broadcast_grad():
    # rng = numpy.random.RandomState(utt.fetch_seed())
    x1 = tensor4("x")
    # x1_data = rng.randn(1, 1, 300, 300)
    sigma = scalar("sigma")
    # sigma_data = 20
    window_radius = 3

    filter_1d = aet.arange(-window_radius, window_radius + 1)
    filter_1d = filter_1d.astype(aesara.config.floatX)
    filter_1d = exp(-0.5 * filter_1d ** 2 / sigma ** 2)
    filter_1d = filter_1d / filter_1d.sum()

    filter_W = filter_1d.dimshuffle(["x", "x", 0, "x"])

    y = conv2d(x1, filter_W, border_mode="full", filter_shape=[1, 1, None, None])
    aesara.grad(y.sum(), sigma)
