def __init__(
        self,
        in_channels: int,
        out_channels: int,
        kernel_size: int | tuple[int, int],
        input_shape: tuple[int, ...],
        device: str | "torch.device",
        stride: int | tuple[int, int] = 1,
        padding: int | tuple[int, int] = 0,
        dilation: int | tuple[int, int] = 0,
        bias: bool = True,
        supplied_input_weights: "torch.Tensor" | None = None,
        supplied_input_bias: "torch.Tensor" | None = None,
        to_debug: bool = False,
    ):
        """
        Creates the equivalent dense weights for the specified convolutional layer.

        :param in_channels: Number of input channels
        :param out_channels: Number of output channels
        :param kernel_size: Shape of the convolutional kernel
        :param device: Device to put the weights onto
        :param stride: The convolution's stride
        :param padding: Size of padding to use
        :param dilation: Dilation to apply to the convolution
        :param bias: If to include a bias term
        :param supplied_input_weights: Load in a pre-defined set of convolutional weights with the correct specification
        :param supplied_input_bias: Load in a pre-defined set of convolutional bias with the correct specification
        :param to_debug: Helper parameter to help with debugging.
        """

        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.padding = padding
        self.dilation = dilation
        self.stride = stride
        self.device = device
        self.include_bias = bias
        self.cnn: "torch.nn.Conv2d" | None = None

        super().__init__()
        self.conv = torch.nn.Conv2d(
            in_channels=1,
            out_channels=out_channels * in_channels,
            kernel_size=kernel_size,
            padding=padding,
            dilation=dilation,
            bias=False,
            stride=stride,
        ).to(device)
        self.bias_to_grad = None

        if bias:
            self.bias_to_grad = torch.nn.Parameter(torch.rand(out_channels).to(device))

        if to_debug:
            self.conv_debug = torch.nn.Conv2d(
                in_channels=in_channels,
                out_channels=out_channels,
                kernel_size=kernel_size,
                bias=bias,
                stride=stride,
                padding=padding,
                dilation=dilation,
            ).to(device)

            if isinstance(kernel_size, tuple):
                self.conv.weight = torch.nn.Parameter(
                    torch.reshape(
                        torch.tensor(self.conv_debug.weight.data.cpu().detach().numpy()),
                        (out_channels * in_channels, 1, kernel_size[0], kernel_size[1]),
                    ).to(device)
                )
            else:
                self.conv.weight = torch.nn.Parameter(
                    torch.reshape(
                        torch.tensor(self.conv_debug.weight.data.cpu().detach().numpy()),
                        (out_channels * in_channels, 1, kernel_size, kernel_size),
                    ).to(device)
                )
            if bias and self.conv_debug.bias is not None:
                self.bias_to_grad = torch.nn.Parameter(
                    torch.tensor(self.conv_debug.bias.data.cpu().detach().numpy()).to(device)
                )

        if supplied_input_weights is not None:
            if isinstance(kernel_size, tuple):
                self.conv.weight = torch.nn.Parameter(
                    torch.reshape(
                        supplied_input_weights,
                        (out_channels * in_channels, 1, kernel_size[0], kernel_size[1]),
                    )
                )
            else:
                self.conv.weight = torch.nn.Parameter(
                    torch.reshape(supplied_input_weights, (out_channels * in_channels, 1, kernel_size, kernel_size))
                )

        if supplied_input_bias is not None:
            self.bias_to_grad = torch.nn.Parameter(supplied_input_bias.to(device))

        self.in_channels = in_channels
        self.out_channels = out_channels
        self.input_height = input_shape[2]
        self.input_width = input_shape[3]

        self.output_height: int = 0
        self.output_width: int = 0

        self.dense_weights, self.bias = self.convert_to_dense(device)

        self.dense_weights = self.dense_weights.to(device)
        if self.bias is not None:
            self.bias = self.bias.to(device)

        Creates the equivalent dense weights for the specified convolutional layer.

        :param in_channels: Number of input channels
        :param out_channels: Number of output channels
        :param kernel_size: Shape of the convolutional kernel
        :param device: Device to put the weights onto
        :param stride: The convolution's stride
        :param padding: Size of padding to use
        :param dilation: Dilation to apply to the convolution
        :param bias: If to include a bias term
        :param supplied_input_weights: Load in a pre-defined set of convolutional weights with the correct specification
        :param supplied_input_bias: Load in a pre-defined set of convolutional bias with the correct specification
        :param to_debug: Helper parameter to help with debugging.
        
art/estimators/certification/interval/interval.py