//  [python]
def forward(self, x1, x2, diag=False, **params):
        """
        Computes the Gaussian kernel matrix between inputs x1 and x2 belonging to a SPD manifold.

        Parameters
        ----------
        :param x1: input points on the SPD manifold
        :param x2: input points on the SPD manifold

        Optional parameters
        -------------------
        :param diag: Should we return the whole distance matrix, or just the diagonal? If True, we must have `x1 == x2`
        :param params: additional parameters

        Returns
        -------
        :return: kernel matrix between x1 and x2
        """
        # Transform input vector to matrix
        x1 = vector_to_symmetric_matrix_mandel_torch(x1)
        x2 = vector_to_symmetric_matrix_mandel_torch(x2)

        # Expand dimensions to compute all matrix-matrix distances
        x1 = x1.unsqueeze(-3)
        x2 = x2.unsqueeze(-4)
        # Repeat x and y data along 1- and 0- dimensions to have ndata_x x ndata_y x dim x dim arrays
        x1 = torch.cat(x2.shape[-3] * [x1], dim=-3)
        x2 = torch.cat(x1.shape[-4] * [x2], dim=-4)

        # Compute singular values
        x1_inv_x2 = torch.bmm(x1.view(-1, self.dim, self.dim), torch.linalg.inv(x2).view(-1, self.dim, self.dim))
        _, singular_values, _ = torch.linalg.svd(x1_inv_x2)
        singular_values = singular_values.to(device)

        # Reshape the singular values
        shape = list(x2.shape)[:-2]
        shape.append(singular_values.shape[-1])
        singular_values_rshp = singular_values.view(shape)

        # Compute difference and sum of squared singular values
        # Note: singular values that torch.linalg.svd outputs are sorted, the following code relies on this fact.
        H1, H2 = torch.log(singular_values_rshp[..., 0]), torch.log(singular_values_rshp[..., 1])
        # assert (H1 >= H2)
        r_H_sq = H1 ** 2 + H2 ** 2
        singular_values_diff = H1 - H2

        # # Evaluate non-integral part
        # nonintegral_part = torch.exp(-r_H_sq / (4 * t))
        #
        # Evaluate integral part
        shift = torch.log10(self.lengthscale).item()
        t_vals = torch.logspace(-2 + shift, 1.5 + shift, self.nb_points_integral_t).to(device)
        b_vals = torch.logspace(-5., 1., self.nb_points_integral_b).to(device)
        integral_vals = torch.zeros([self.nb_points_integral_t, self.nb_points_integral_b] +
                                    list(singular_values_diff.shape)).to(device)
        for i in range(self.nb_points_integral_t):
            for j in range(self.nb_points_integral_b):
                integral_vals[i, j] = self.link_function(singular_values_diff, r_H_sq, b_vals[j], t_vals[i])
        # Integral to obtain the heat kernel values
        heat_kernel_integral_vals = torch.trapz(integral_vals, b_vals, dim=1)
        # Integral over heat kernel to obtain the Matérn kernel values
        kernel = torch.trapz(heat_kernel_integral_vals, t_vals, dim=0)

        # Compute normalization factor
        # Evaluate only the integral part, as the non-integral part is equal to 1
        integral_vals_norm = torch.zeros([self.nb_points_integral_t, self.nb_points_integral_b]).to(device)
        for i in range(self.nb_points_integral_t):
            for j in range(self.nb_points_integral_b):
                integral_vals_norm[i, j] = self.link_function(torch.zeros((1, 1), dtype=kernel.dtype).to(device),
                                                              torch.zeros((1, 1), dtype=kernel.dtype).to(device),
                                                              b_vals[j], t_vals[i])
        # Integral to obtain the heat kernel values
        norm_factor_integral_vals = torch.trapz(integral_vals_norm, b_vals, dim=1)
        # Integral over heat kernel to obtain the Matérn kernel values
        norm_factor = torch.trapz(norm_factor_integral_vals, t_vals, dim=0)

        # Kernel
        return kernel / norm_factor