def zonotope_relu(self, x: "torch.Tensor") -> "torch.Tensor":
        """
        Implements "DeepZ" for ReLU.

        :param x: input zonotope.
        :return x: zonotope after application of the relu. May have grown in dimension if crossing relus occur.
        """
        original_shape = x.shape
        x = x.reshape((x.shape[0], -1))

        # compute lower bounds
        lbs = self.compute_lb(cent=x[0], eps=x[1:])

        # compute upper bounds
        ubs = self.compute_ub(cent=x[0], eps=x[1:])
        slope = torch.div(ubs, (ubs - lbs))

        index_cent_vector = torch.zeros((x.shape[0], 1)).to(self.device)
        index_cent_vector[0] = 1

        cent_update = (slope * lbs) / 2
        cent_update = torch.tile(cent_update, (x.shape[0], 1))

        # find where we have a crossing relu
        bools = torch.logical_and(lbs < 0, ubs > 0)

        # where we have a crossing relu, update the terms. Else, do not change input.
        x = torch.where(bools, x * slope - cent_update * index_cent_vector, x)

        # where we have a feature that is < 0, relu always returns 0
        zeros = torch.from_numpy(np.zeros(1).astype("float32")).to(self.device)
        x = torch.where(ubs < 0, zeros, x)

        # vector containing all the (potential) new error terms. We will need to 1) select the ones
        # we need and zero out the rest. And 2) Shape the errors into the correct matrix.
        new_vector = torch.unsqueeze(-1 * ((slope * lbs) / 2), dim=0)

        # indexing_matrix is the shape we want the error terms to be.
        indexing_matrix = np.zeros((torch.sum(bools), x.shape[1]))

        tmp_crossing_relu = torch.logical_and(lbs < 0, ubs > 0)
        crossing_relu_index = 0
        for j, crossing_relu in enumerate(tmp_crossing_relu):
            if crossing_relu:
                indexing_matrix[crossing_relu_index, j] = 1
                crossing_relu_index += 1

        indexing_matrix_tensor = torch.from_numpy(indexing_matrix.astype("float32")).to(self.device)

        # where there is a crossing ReLU, select the error terms, else zero the vector.
        new_vector = torch.where(bools, new_vector, zeros)
        # tile the error vector to the correct shape and select the terms we want.
        # crossing_relu_index at this point is also the same as the number of crossing relus
        new_vector = torch.tile(new_vector, (crossing_relu_index, 1))
        new_vector = new_vector * indexing_matrix_tensor

        # add the new error terms to the zonotope.
        x = torch.cat((x, new_vector))

        if len(original_shape) > 2:
            x = x.reshape((-1, original_shape[1], original_shape[2], original_shape[3]))
        return x

        Implements "DeepZ" for ReLU.

        :param x: input zonotope.
        :return x: zonotope after application of the relu. May have grown in dimension if crossing relus occur.
        
art/estimators/certification/deep_z/deep_z.py