diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index abe5acd..3bb325a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -6,6 +6,7 @@ * Added a new module `circular_ensembles` allowing the calculation of the circular orthogonal ensembles and circular symplectic ensembles Weingarten functions [(#32)](https://github.com/polyquantique/haarpy/pull/32). * Added a new module `permutation` allowing the calculation of the permutation matrices and centered permuation matrices' Weingarten functions as well as their moments [(#36)](https://github.com/polyquantique/haarpy/pull/36). * Added a new module `partition` allowing to generate partitions of a set as well as implementing some operations on them such as the meet and the join operations [(#36)](https://github.com/polyquantique/haarpy/pull/36). +* Added moment calculation for Haar random symplectic matrices and circular symplectic ensemble [(#43)](https://github.com/polyquantique/haarpy/pull/43). ### Breaking changes diff --git a/haarpy/__init__.py b/haarpy/__init__.py index b89b28f..8c1ed55 100644 --- a/haarpy/__init__.py +++ b/haarpy/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2024 Polyquantique +# Copyright 2025 Polyquantique # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,7 +34,9 @@ weingarten_centered_permutation haar_integral_unitary haar_integral_orthogonal + haar_integral_symplectic haar_integral_circular_orthogonal + haar_integral_circular_symplectic haar_integral_permutation haar_integral_centered_permutation get_conjugacy_class @@ -104,12 +106,14 @@ from .symplectic import ( twisted_spherical_function, weingarten_symplectic, + haar_integral_symplectic, ) from .circular_ensembles import ( weingarten_circular_orthogonal, weingarten_circular_symplectic, haar_integral_circular_orthogonal, + haar_integral_circular_symplectic, ) from .permutation import ( @@ -132,7 +136,9 @@ "weingarten_centered_permutation", "haar_integral_unitary", "haar_integral_orthogonal", + "haar_integral_symplectic", "haar_integral_circular_orthogonal", + "haar_integral_circular_symplectic", "haar_integral_permutation", "haar_integral_centered_permutation", "get_conjugacy_class", diff --git a/haarpy/circular_ensembles.py b/haarpy/circular_ensembles.py index c54f459..08f17af 100644 --- a/haarpy/circular_ensembles.py +++ b/haarpy/circular_ensembles.py @@ -15,12 +15,14 @@ Circular ensembles Python interface """ +from math import prod from fractions import Fraction from functools import lru_cache from typing import Union from collections import Counter from sympy import Symbol, Expr, fraction, factor, simplify -from sympy.combinatorics import Permutation +from sympy.combinatorics import Permutation, SymmetricGroup +from sympy.core.numbers import Integer from haarpy import ( weingarten_orthogonal, weingarten_symplectic, @@ -107,3 +109,130 @@ def haar_integral_circular_orthogonal( integral = factor(numerator) / factor(denominator) return integral + + +@lru_cache +def haar_integral_circular_symplectic( + sequences: tuple[tuple[Expr]], half_dimension: Expr +) -> Expr: + """Returns integral over circular symplectic ensemble polynomial + sampled at random from the Haar measure + + Args: + sequences (tuple[tuple[int]]) : Indices of matrix elements + half_dimension (Symbol) : Half the dimension of the unitary group + + Returns: + Expr : Integral under the Haar measure + + Raise: + ValueError : If sequences doesn't contain 2 tuples + ValueError : If tuples i and j are of odd size + TypeError: If dimension is int and sequence is not + TypeError: If the half_dimension is not int nor Symbol + ValueError: If all sequence indices are not between 0 and 2*dimension - 1 + TypeError: If sequence containt something else than Expr + TypeError: If symbolic sequences have the wrong format + """ + if len(sequences) != 2: + raise ValueError("Wrong sequence format") + + seq_i, seq_j = sequences + + degree = len(seq_i) + + if degree % 2 or len(seq_j) % 2: + raise ValueError("Wrong sequence format") + + if isinstance(half_dimension, int): + if not all(isinstance(i, int) for i in seq_i + seq_j): + raise TypeError + if not all(0 <= i <= 2 * half_dimension - 1 for i in seq_i + seq_j): + raise ValueError("The matrix indices are outside the dimension range") + if degree != len(seq_j): + return 0 + coefficient = prod( + -1 if i < half_dimension else 1 for i in (seq_i + seq_j)[::2] + ) + shifted_i = [ + ( + i + half_dimension + if i < half_dimension and index % 2 == 0 + else i - half_dimension if index % 2 == 0 else i + ) + for index, i in enumerate(seq_i) + ] + shifted_j = [ + ( + i + half_dimension + if i < half_dimension and index % 2 == 0 + else i - half_dimension if index % 2 == 0 else i + ) + for index, i in enumerate(seq_j) + ] + + elif isinstance(half_dimension, Symbol): + if not all(isinstance(i, (int, Expr)) for i in seq_i + seq_j): + raise TypeError + + if not all( + ( + len(xpr.as_ordered_terms()) == 2 + and xpr.as_ordered_terms()[0] == half_dimension + and isinstance(xpr.as_ordered_terms()[1], Integer) + and xpr.as_ordered_terms()[1] > 0 + ) + or xpr == half_dimension + for xpr in seq_i + seq_j + if isinstance(xpr, Expr) + ): + raise TypeError + if degree != len(seq_j): + return 0 + coefficient = prod( + -1 if isinstance(i, int) else 1 for i in (seq_i + seq_j)[::2] + ) + shifted_i = [ + ( + i + half_dimension + if isinstance(i, int) and index % 2 == 0 + else ( + 0 + if i == half_dimension and index % 2 == 0 + else i.as_ordered_terms()[1] if index % 2 == 0 else i + ) + ) + for index, i in enumerate(seq_i) + ] + shifted_j = [ + ( + i + half_dimension + if isinstance(i, int) and index % 2 == 0 + else ( + 0 + if i == half_dimension and index % 2 == 0 + else i.as_ordered_terms()[1] if index % 2 == 0 else i + ) + ) + for index, i in enumerate(seq_j) + ] + + else: + raise TypeError + + permutation_tuple = ( + permutation + for permutation in SymmetricGroup(degree).generate() + if permutation(shifted_i) == shifted_j + ) + + integral = coefficient * sum( + weingarten_circular_symplectic(permutation, half_dimension) + for permutation in permutation_tuple + ) + + if isinstance(half_dimension, Expr): + numerator, denominator = fraction(simplify(integral)) + integral = factor(numerator) / factor(denominator) + + return integral diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index cc98544..33c1590 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -18,14 +18,17 @@ from math import prod from fractions import Fraction from functools import lru_cache -from sympy import Symbol, factorial, factor, fraction, simplify +from itertools import product +from sympy import Symbol, factorial, factor, fraction, simplify, Expr from sympy.combinatorics import Permutation from sympy.utilities.iterables import partitions +from sympy.core.numbers import Integer from haarpy import ( get_conjugacy_class, murn_naka_rule, HyperoctahedralGroup, irrep_dimension, + hyperoctahedral_transversal, ) @@ -75,17 +78,15 @@ def twisted_spherical_function( @lru_cache -def weingarten_symplectic( - permutation: Permutation, symplectic_dimension: Symbol -) -> Symbol: +def weingarten_symplectic(permutation: Permutation, half_dimension: Symbol) -> Expr: """Returns the symplectic Weingarten function Args: permutation (Permutation): A permutation of the symmetric group S_2k - symplectic_dimension (int): The dimension of the symplectic group + half_dimension (Symbol): Half the dimension of the symplectic group Returns: - Symbol : The Weingarten function + Expr : The Weingarten function Raise: ValueError : If the degree 2k of the symmetric group S_2k is not a factor of 2 @@ -112,14 +113,14 @@ def weingarten_symplectic( ) coefficient_gen = ( prod( - 2 * symplectic_dimension - 2 * i + j + 2 * half_dimension - 2 * i + j for i in range(len(partition)) for j in range(partition[i]) ) for partition in partition_tuple ) - if isinstance(symplectic_dimension, int): + if isinstance(half_dimension, int): weingarten = sum( Fraction( irrep_dim * zonal_spherical, @@ -150,3 +151,125 @@ def weingarten_symplectic( weingarten = simplify(factor(numerator) / factor(denominator)) return weingarten + + +@lru_cache +def haar_integral_symplectic( + sequences: tuple[tuple[Expr]], + half_dimension: Symbol, +) -> Expr: + """Returns integral over symplectic group polynomial sampled at random from the Haar measure + + Args: + sequences (tuple[tuple[Expr]]): Indices of matrix elements + half_dimension (Symbol): Half the dimension of the symplectic group + + Returns: + Expr: Integral under the Haar measure + + Raise: + ValueError: If sequences don't contain 2 tuples + ValueError: If tuples i and j are of different length + TypeError: If the half_dimension is not int nor Symbol + TypeError: If dimension is int and sequence is not + ValueError: If all sequence indices are not between 0 and 2*dimension - 1 + TypeError: If sequence containt something else than Expr + TypeError: If symbolic sequences have the wrong format + """ + if len(sequences) != 2: + raise ValueError("Wrong sequence format") + + seq_i, seq_j = sequences + + degree = len(seq_i) + + if degree != len(seq_j): + raise ValueError("Wrong sequence format") + + if isinstance(half_dimension, int): + if not all(isinstance(i, int) for i in seq_i + seq_j): + raise TypeError + if not all(0 <= i <= 2 * half_dimension - 1 for i in seq_i + seq_j): + raise ValueError("The matrix indices are outside the dimension range") + if degree % 2: + return 0 + seq_i_position = tuple(0 if i < half_dimension else 1 for i in seq_i) + seq_j_position = tuple(0 if j < half_dimension else 1 for j in seq_j) + seq_i_value = tuple( + i if i < half_dimension else i - half_dimension for i in seq_i + ) + seq_j_value = tuple( + j if j < half_dimension else j - half_dimension for j in seq_j + ) + elif isinstance(half_dimension, Symbol): + if not all(isinstance(i, (int, Expr)) for i in seq_i + seq_j): + raise TypeError + + if not all( + ( + len(xpr.as_ordered_terms()) == 2 + and xpr.as_ordered_terms()[0] == half_dimension + and isinstance(xpr.as_ordered_terms()[1], Integer) + ) + or xpr == half_dimension + for xpr in seq_i + seq_j + if isinstance(xpr, Expr) + ): + raise TypeError + if degree % 2: + return 0 + seq_i_position = tuple(0 if isinstance(i, int) else 1 for i in seq_i) + seq_j_position = tuple(0 if isinstance(j, int) else 1 for j in seq_j) + + seq_i_value = tuple( + ( + i + if isinstance(i, int) + else 0 if i == half_dimension else i.as_ordered_terms()[1] + ) + for i in seq_i + ) + seq_j_value = tuple( + ( + j + if isinstance(j, int) + else 0 if j == half_dimension else j.as_ordered_terms()[1] + ) + for j in seq_j + ) + else: + raise TypeError + + def twisted_delta(seq_value, seq_pos, perm): + return ( + 0 + if not all( + i1 == i2 for i1, i2 in zip(perm(seq_value)[::2], perm(seq_value)[1::2]) + ) + else prod( + i2 - i1 for i1, i2 in zip(perm(seq_pos)[::2], perm(seq_pos)[1::2]) + ) + ) + + permutation_i_tuple = tuple( + (perm, twisted_delta(seq_i_value, seq_i_position, perm)) + for perm in hyperoctahedral_transversal(degree) + ) + permutation_j_tuple = tuple( + (perm, twisted_delta(seq_j_value, seq_j_position, perm)) + for perm in hyperoctahedral_transversal(degree) + ) + + integral = sum( + perm_i[1] + * perm_j[1] + * weingarten_symplectic(perm_j[0] * ~perm_i[0], half_dimension) + for perm_i, perm_j in product(permutation_i_tuple, permutation_j_tuple) + if perm_i[1] * perm_j[1] + ) + + if isinstance(half_dimension, Expr): + numerator, denominator = fraction(simplify(integral)) + integral = factor(numerator) / factor(denominator) + + return integral diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index 4b26483..5f557d5 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -16,181 +16,345 @@ """ from math import prod +from random import randint from fractions import Fraction from sympy import Symbol, simplify, factorial, factorial2 from sympy.combinatorics import SymmetricGroup import pytest import haarpy as ap -d = Symbol('d') +d = Symbol("d") +# The values in the dictionary below were verified against Monte Carlo simulations +cse_dict = { + ((0, 0), (0, 0), 1): 1, + ((0, 1), (0, 1), 1): 0, + ((0, 1, 2, 3), (0, 1, 2, 3), 2): 1 / 6, + ((0, 0, 0, 0), (0, 0, 0, 0), 2): 1 / 6, + ((0, 3, 0, 3), (0, 3, 0, 3), 2): 1 / 6, + ((0, 0, 0, 3), (0, 3, 0, 0), 2): 1 / 12, + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3): 1 / 35, + ((5, 5, 5, 5, 5, 5), (5, 5, 5, 5, 5, 5), 3): 1 / 35, + ((2, 2, 5, 5, 5, 5), (2, 2, 5, 5, 5, 5), 3): 1 / 35, + ((2, 2, 5, 5, 5, 5), (2, 5, 5, 2, 5, 5), 3): 0, + ((2, 2, 5, 5, 3, 3), (2, 2, 5, 5, 3, 3), 3): 1 / 63, + ((7, 7, 0, 0, 0, 0, 0, 0), (7, 0, 0, 7, 0, 0, 0, 0), 4): -1 / 4200, +} -@pytest.mark.parametrize("half_degree", range(1,3)) + +@pytest.mark.parametrize("half_degree", range(1, 3)) def test_weingarten_circular_orthogonal_hyperoctahedral_symbolic(half_degree): """Symbolic validation of COE Weingarten function against results shown in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ if half_degree == 1: - for permutation in SymmetricGroup(2*half_degree).generate(): - assert ap.weingarten_circular_orthogonal(permutation, d) == 1/(d+1) + for permutation in SymmetricGroup(2 * half_degree).generate(): + assert ap.weingarten_circular_orthogonal(permutation, d) == 1 / (d + 1) else: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) - coefficient = 1/(d*(d+1)*(d+3)) + coefficient = 1 / (d * (d + 1) * (d + 3)) assert ap.weingarten_circular_orthogonal(permutation, d) == ( - simplify((d+2)*coefficient) if permutation in hyperoctahedral - else -coefficient + simplify((d + 2) * coefficient) if permutation in hyperoctahedral else -coefficient ) -@pytest.mark.parametrize("half_degree", range(1,3)) +@pytest.mark.parametrize("half_degree", range(1, 3)) def test_weingarten_circular_orthogonal_hyperoctahedral_numeric(half_degree): """Symbolic validation of COE Weingarten function against results shown in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ if half_degree == 1: - for permutation in SymmetricGroup(2*half_degree).generate(): - assert ap.weingarten_circular_orthogonal(permutation, 7) == 1/(7+1) + for permutation in SymmetricGroup(2 * half_degree).generate(): + assert ap.weingarten_circular_orthogonal(permutation, 7) == 1 / (7 + 1) else: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) - coefficient = Fraction(1,(7*(7+1)*(7+3))) + coefficient = Fraction(1, (7 * (7 + 1) * (7 + 3))) assert ap.weingarten_circular_orthogonal(permutation, 7) == ( - simplify((7+2)*coefficient) if permutation in hyperoctahedral - else -coefficient + simplify((7 + 2) * coefficient) if permutation in hyperoctahedral else -coefficient ) -@pytest.mark.parametrize("half_degree", range(1,3)) +@pytest.mark.parametrize("half_degree", range(1, 3)) def test_weingarten_circular_symplectic_hyperoctahedral_symbolic(half_degree): """Symbolic validation of CSE Weingarten function against results shown in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ if half_degree == 1: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): assert ap.weingarten_circular_symplectic(permutation, d) == ( - permutation.signature()/(2*d-1) + permutation.signature() / (2 * d - 1) ) else: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) - coefficient = permutation.signature()/(d*(2*d-1)*(2*d-3)) + coefficient = permutation.signature() / (d * (2 * d - 1) * (2 * d - 3)) assert ap.weingarten_circular_symplectic(permutation, d) == ( - simplify((d-1)*coefficient) if permutation in hyperoctahedral - else coefficient/2 + simplify((d - 1) * coefficient) + if permutation in hyperoctahedral + else coefficient / 2 ) -@pytest.mark.parametrize("half_degree", range(1,3)) +@pytest.mark.parametrize("half_degree", range(1, 3)) def test_weingarten_circular_symplectic_hyperoctahedral_numeric(half_degree): """Symbolic validation of CSE Weingarten function against results shown in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ if half_degree == 1: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): assert ap.weingarten_circular_symplectic(permutation, 7) == Fraction( permutation.signature(), - (2*7-1), + (2 * 7 - 1), ) else: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) - coefficient = Fraction(permutation.signature(), (7*(2*7-1)*(2*7-3))) + coefficient = Fraction(permutation.signature(), (7 * (2 * 7 - 1) * (2 * 7 - 3))) assert ap.weingarten_circular_symplectic(permutation, 7) == ( - simplify((7-1)*coefficient) if permutation in hyperoctahedral - else coefficient*Fraction(1,2) + simplify((7 - 1) * coefficient) + if permutation in hyperoctahedral + else coefficient * Fraction(1, 2) ) @pytest.mark.parametrize( "sequences", [ - ((0,1,2,3), (0,1)), - ((0,1,2,3,4,5), (0,1,2,3)), - ((0,1,2,3), (0,1,2,2)), - ((0,0,0,0), (0,0,0,1)), - ] + ((0, 1, 2, 3), (0, 1)), + ((0, 1, 2, 3, 4, 5), (0, 1, 2, 3)), + ((0, 1, 2, 3), (0, 1, 2, 2)), + ((0, 0, 0, 0), (0, 0, 0, 1)), + ], ) def test_haar_integral_coe_zero(sequences): "test cases that return 0" assert not ap.haar_integral_circular_orthogonal(sequences, d) -@pytest.mark.parametrize("power", range(2,8,2)) +@pytest.mark.parametrize("power", range(2, 8, 2)) def test_haar_integral_coe_diagonal_entry(power): """Test Thm. 4.2 as seen in `Matsumoto. General moments of matrix elements from circular orthogonal ensembles `_ """ - sequences = (power*(0,), power*(0,)) - assert ( - ap.haar_integral_circular_orthogonal(sequences, d) - == factorial2(power)/prod((d+i) for i in range(1,power,2)) + sequences = (power * (0,), power * (0,)) + assert ap.haar_integral_circular_orthogonal(sequences, d) == factorial2(power) / prod( + (d + i) for i in range(1, power, 2) ) -@pytest.mark.parametrize("power", range(2,8,2)) +@pytest.mark.parametrize("power", range(2, 8, 2)) def test_haar_integral_coe_off_diagonal_entry(power): """Test Thm. 4.3 as seen in `Matsumoto. General moments of matrix elements from circular orthogonal ensembles `_ """ - half_power = int(power/2) - sequences = (half_power*(0,1), half_power*(0,1)) - assert ( - ap.haar_integral_circular_orthogonal(sequences, d) - == factorial(half_power)/((d+(power-1))*prod((d+i) for i in range(half_power-1))) + half_power = int(power / 2) + sequences = (half_power * (0, 1), half_power * (0, 1)) + assert ap.haar_integral_circular_orthogonal(sequences, d) == factorial(half_power) / ( + (d + (power - 1)) * prod((d + i) for i in range(half_power - 1)) ) -@pytest.mark.parametrize("power", range(2,6,2)) +@pytest.mark.parametrize("power", range(2, 6, 2)) def test_haar_integral_coe_diagonal_entry_numeric(power): """Test Thm. 4.2 as seen in `Matsumoto. General moments of matrix elements from circular orthogonal ensembles `_ """ dimension = 17 - sequences = (power*(0,), power*(0,)) - assert ( - ap.haar_integral_circular_orthogonal(sequences, dimension) - == Fraction(factorial2(power),prod((dimension+i) for i in range(1,power,2))) + sequences = (power * (0,), power * (0,)) + assert ap.haar_integral_circular_orthogonal(sequences, dimension) == Fraction( + factorial2(power), prod((dimension + i) for i in range(1, power, 2)) ) -@pytest.mark.parametrize("power", range(2,6,2)) +@pytest.mark.parametrize("power", range(2, 6, 2)) def test_haar_integral_coe_off_diagonal_entry_numeric(power): """Test Thm. 4.3 as seen in `Matsumoto. General moments of matrix elements from circular orthogonal ensembles `_ """ dimension = 17 - half_power = int(power/2) - sequences = (half_power*(0,1), half_power*(0,1)) - assert ( - ap.haar_integral_circular_orthogonal(sequences, dimension) - == Fraction( - factorial(half_power), - ((dimension+(power-1))*prod((dimension+i) for i in range(half_power-1))), - ) + half_power = int(power / 2) + sequences = (half_power * (0, 1), half_power * (0, 1)) + assert ap.haar_integral_circular_orthogonal(sequences, dimension) == Fraction( + factorial(half_power), + ((dimension + (power - 1)) * prod((dimension + i) for i in range(half_power - 1))), ) + @pytest.mark.parametrize( "sequences", [ ((1,),), - ((1,2,3),), - ((1,2),(3,4),(4,5)), + ((1, 2, 3),), + ((1, 2), (3, 4), (4, 5)), "str", - ((1,2),(3,4,5)), - ((1,2,3),(3,4,5,6)), - ((1,2,3),(1,2,3)), - ] + ((1, 2), (3, 4, 5)), + ((1, 2, 3), (3, 4, 5, 6)), + ((1, 2, 3), (1, 2, 3)), + ], ) def test_haar_integral_coe_value_error(sequences): "Test haar integral value error" with pytest.raises(ValueError, match="Wrong tuple format"): ap.haar_integral_circular_orthogonal(sequences, d) + + +@pytest.mark.parametrize( + "seq_i, seq_j, half_dim", + [ + ((0, 0), (0, 0), 1), + ((0, 1), (0, 1), 1), + ((0, 1, 2, 3), (0, 1, 2, 3), 2), + ((0, 0, 0, 0), (0, 0, 0, 0), 2), + ((0, 3, 0, 3), (0, 3, 0, 3), 2), + ((0, 0, 0, 3), (0, 3, 0, 0), 2), + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3), + ((5, 5, 5, 5, 5, 5), (5, 5, 5, 5, 5, 5), 3), + ((2, 2, 5, 5, 5, 5), (2, 2, 5, 5, 5, 5), 3), + ((2, 2, 5, 5, 5, 5), (2, 5, 5, 2, 5, 5), 3), + ((2, 2, 5, 5, 3, 3), (2, 2, 5, 5, 3, 3), 3), + ((7, 7, 0, 0, 0, 0, 0, 0), (7, 0, 0, 7, 0, 0, 0, 0), 4), + ], +) +def test_haar_integral_circular_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dim): + "Test haar integral circular symplectic moments against Monte Carlo simulation numeric" + integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), half_dim) + + mc_integral = cse_dict[(seq_i, seq_j, half_dim)] + + assert float(integral) == mc_integral + + +@pytest.mark.parametrize( + "seq_i, seq_j, half_dim", + [ + ((0, 0), (0, 0), 1), + ((0, d), (0, d), 1), + ((0, 1, d, d + 1), (0, 1, d, d + 1), 2), + ((0, 0, 0, 0), (0, 0, 0, 0), 2), + ((0, 1 + d, 0, 1 + d), (0, 1 + d, 0, 1 + d), 2), + ((0, 0, 0, 1 + d), (0, 1 + d, 0, 0), 2), + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3), + ((d + 2, d + 2, d + 2, d + 2, d + 2, d + 2), (d + 2, d + 2, d + 2, d + 2, d + 2, d + 2), 3), + ((2, 2, d + 2, d + 2, d + 2, d + 2), (2, 2, d + 2, d + 2, d + 2, d + 2), 3), + ((2, 2, d + 2, d + 2, d + 2, d + 2), (2, d + 2, d + 2, 2, d + 2, d + 2), 3), + ((2, 2, d + 2, d + 2, d, d), (2, 2, d + 2, d + 2, d, d), 3), + ((d + 3, d + 3, 0, 0, 0, 0, 0, 0), (d + 3, 0, 0, d + 3, 0, 0, 0, 0), 4), + ], +) +def test_haar_integral_circular_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dim): + "Test haar integral circular symplectic moments against Monte Carlo simulation symbolic" + integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), d).subs(d, half_dim) + + seq_i = tuple(i if isinstance(i, int) else i.subs(d, half_dim) for i in seq_i) + seq_j = tuple(i if isinstance(i, int) else i.subs(d, half_dim) for i in seq_j) + + mc_integral = cse_dict[((seq_i, seq_j, half_dim))] + + assert float(integral) == mc_integral + + + +@pytest.mark.parametrize( + "sequences", + [ + ((1, 2, 3, 4), (1, 2, 3, 4), (1, 2, 3, 4)), + ((1, 2, 3, 4),), + ((1, 2, 3, 4, 5), (1, 2, 3, 4, 5)), + ((1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 5, 6, 7)), + ], +) +def test_haar_integral_circular_symplectic_value_error_wrong_tuple(sequences): + "Value error for wrong sequence format" + with pytest.raises(ValueError, match="Wrong sequence format"): + ap.haar_integral_circular_symplectic(sequences, d) + + +@pytest.mark.parametrize( + "sequences", + [ + (("a", "b", "c", "d"), (1, 2, 3, 4)), + ((1, 1, d + 1, d + 1), (1, 1, 1, 1)), + ], +) +def test_haar_integral_circular_symplectic_type_error_integer_dimension(sequences): + "Type error for integer dimension with not integer sequences" + dimension = randint(1, 99) + with pytest.raises(TypeError): + ap.haar_integral_circular_symplectic(sequences, dimension) + + +@pytest.mark.parametrize( + "sequences, dimension", + [ + (((1, 3), (1, 3)), 1), + (((1, 2, 3, 5), (1, 2, 3, 4)), 2), + (((1, 2, 3, 41), (1, 2, 3, 41)), 20), + ], +) +def test_haar_integral_circular_symplectic_value_error_outside_dimension_range( + sequences, dimension +): + "Value error for sequences values outside dimension range" + with pytest.raises( + ValueError, + match="The matrix indices are outside the dimension range", + ): + ap.haar_integral_circular_symplectic(sequences, dimension) + + +@pytest.mark.parametrize( + "sequences", + [ + ((1, 2, 3, 4), (1, 2, 3, "a")), + ((1, 2, 3, 4), (1, 2, 3, {1, 2})), + ((1, 2, 3, 4), (1, 2, 3, 4 * d)), + ((1, 2, 3, 2 * d + 1), (1, 2, 3, 4)), + ((1, 2, 3, d + 1), (1, 2, 3, 4.0)), + ((1, 2, 3, d - 1), (1, 2, 3, 4)), + ((1, 2, 3, 4), (1, 2, 3, d**2)), + ((1, 2, 3, 4), (1, 2, 3, 1 + d**2 + d)), + ((1, 2, 3, 4), (1, 2, 3, d + Symbol("s"))), + ], +) +def test_haar_integral_circular_symplectic_type_error_wrong_format(sequences): + "Type error for symbolic dimension with wrong sequence format" + with pytest.raises(TypeError): + ap.haar_integral_circular_symplectic(sequences, d) + + +@pytest.mark.parametrize( + "dimension", + [ + "a", + [1, 2], + {1, 2}, + 3.0, + ], +) +def test_haar_integral_circular_symplectic_wrong_dimension_format(dimension): + "Type error if the symplectic dimension is not an int nor a symbol" + with pytest.raises(TypeError): + ap.haar_integral_circular_symplectic(((1, 2, 3, 4), (1, 2, 3, 4)), dimension) + + +@pytest.mark.parametrize( + "sequences, dimension", + [ + (((1, 1), (1, 1, 1, 1)), d), + (((1, 1, d + 1, d + 2), (1, 1)), d), + (((0, 0, 0, 0), (0, 0, 0, 0, 2, 2)), 2), + ], +) +def test_haar_integral_circular_symplectic_zero_cases(sequences, dimension): + "Test cases that yield zero" + assert not ap.haar_integral_circular_symplectic(sequences, dimension) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index ea8c3b4..a55fde1 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -16,6 +16,7 @@ """ from math import factorial +from random import randint from fractions import Fraction from sympy import Symbol, simplify from sympy.combinatorics import Permutation, SymmetricGroup @@ -23,21 +24,36 @@ import pytest import haarpy as ap -d = Symbol('d') +d = Symbol("d") + + +# The values in the dictionary below were verified against Monte Carlo simulations +# Using the code in here +# https://github.com/XanaduAI/unicirc/blob/740cd16a2e392bdbcd47466684a58d9341532d42/unicirc/optimization.py#L300 +symplectic_dict = { + ((0, 0, 0, 0), (0, 0, 0, 0), 2): 1 / 10, + ((0, 1, 0, 1), (0, 0, 0, 0), 2): 1 / 20, + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3): 1 / 56, + ((0, 1, 2, 0, 1, 2), (0, 0, 0, 0, 0, 0), 3): 1 / 336, + ((0, 0, 1, 0, 0, 1), (0, 2, 2, 0, 2, 2), 3): 5 / 1344, + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0), 4): 1 / 330, + ((0, 1, 2, 3, 0, 1, 2, 3), (0, 0, 0, 0, 0, 0, 0, 0), 4): 1 / 7920, + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4): 1 / 1980, +} @pytest.mark.parametrize( - "partition", - [ - ((1,1)), - ((2,)), - ((1,1,1)), - ((2,1)), - ((3,)), - ((1,1,1,1)), - ((2,2)), - ((3,1,1)), - ] + "partition", + [ + ((1, 1)), + ((2,)), + ((1, 1, 1)), + ((2, 1)), + ((3,)), + ((1, 1, 1, 1)), + ((2, 2)), + ((3, 1, 1)), + ], ) def test_twisted_spherical_image(partition): """Validates that the twisted spherical function is the image of the zonal spherical function @@ -45,9 +61,7 @@ def test_twisted_spherical_image(partition): symmetric spaces: `_ """ half_degree = sum(partition) - conjugate_partition = tuple( - sum(1 for i in partition if i > j) for j in range(partition[0]) - ) + conjugate_partition = tuple(sum(1 for i in partition if i > j) for j in range(partition[0])) for coset_type in partitions(half_degree): coset_type = tuple(key for key, value in coset_type.items() for _ in range(value)) coset_type_permutation = ap.coset_type_representative(coset_type) @@ -63,21 +77,21 @@ def test_twisted_spherical_image(partition): @pytest.mark.parametrize( "permutation, partition1, partition2", [ - (Permutation(3), (1,1), (2,)), - (Permutation(3)(0,1), (1,1), (2,)), - (Permutation(0,1,2,3), (1,1), (2,)), - (Permutation(3)(0,1,2), (1,1), (2,)), - (Permutation(0,1,2,3,4,5), (3,), (2,1)), - (Permutation(5)(0,4), (3,), (1,1,1)), - (Permutation(5)(0,4), (3,), (2,1)), - (Permutation(5)(0,1,2,3,4), (3,), (1,1,1)), - (Permutation(5)(0,1,2,3,4), (1,1,1), (2,1)), - (Permutation(5)(0,1,2,3,4), (3,), (2,1)), - (Permutation(5), (3,), (1,1,1)), - (Permutation(5), (3,), (2,1)), - (Permutation(5), (2,1), (1,1,1)), - (Permutation(7), (2,1,1), (2,2)), - (Permutation(7)(0,1), (2,1,1), (2,2)), + (Permutation(3), (1, 1), (2,)), + (Permutation(3)(0, 1), (1, 1), (2,)), + (Permutation(0, 1, 2, 3), (1, 1), (2,)), + (Permutation(3)(0, 1, 2), (1, 1), (2,)), + (Permutation(0, 1, 2, 3, 4, 5), (3,), (2, 1)), + (Permutation(5)(0, 4), (3,), (1, 1, 1)), + (Permutation(5)(0, 4), (3,), (2, 1)), + (Permutation(5)(0, 1, 2, 3, 4), (3,), (1, 1, 1)), + (Permutation(5)(0, 1, 2, 3, 4), (1, 1, 1), (2, 1)), + (Permutation(5)(0, 1, 2, 3, 4), (3,), (2, 1)), + (Permutation(5), (3,), (1, 1, 1)), + (Permutation(5), (3,), (2, 1)), + (Permutation(5), (2, 1), (1, 1, 1)), + (Permutation(7), (2, 1, 1), (2, 2)), + (Permutation(7)(0, 1), (2, 1, 1), (2, 2)), ], ) def test_twisted_spherical_orthogonality_transversal_zero(permutation, partition1, partition2): @@ -113,7 +127,7 @@ def test_twisted_spherical_orthogonality_transversal_zero(permutation, partition ) def test_twisted_spherical_orthogonality_transversal_none_zero(permutation, partition): """Orthogonality relation for the twisted spherical function - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ degree = permutation.size @@ -125,52 +139,82 @@ def test_twisted_spherical_orthogonality_transversal_none_zero(permutation, part ) duplicate_partition = tuple(part for part in partition for _ in range(2)) orthogonality = ( - Fraction(factorial(degree), 2**half_degree*factorial(half_degree)) + Fraction(factorial(degree), 2**half_degree * factorial(half_degree)) * ap.twisted_spherical_function(permutation, partition) / ap.irrep_dimension(duplicate_partition) ) assert convolution == orthogonality -@pytest.mark.parametrize("half_degree", range(1,3)) +@pytest.mark.parametrize( + "permutation, partition", + [ + (Permutation(3,), [2,]), + ((3,1), (1,1)), + (Permutation(5,)(0,1), 'a'), + ('a', (3,)), + (Permutation(5,)(0,3,4), 7), + (7, (1,1,1)), + ], +) +def test_twisted_spherical_function_type_error(permutation, partition): + "Test type error for for wrong input formats" + with pytest.raises(TypeError): + ap.twisted_spherical_function(permutation, partition) + + +@pytest.mark.parametrize( + "permutation, partition", + [ + (Permutation(3,), (2,2)), + (Permutation(3,), (1,1,1)), + (Permutation(4,), (2,1)), + (Permutation(4,), (1,1,1)), + ], +) +def test_twisted_spherical_function_degree_value_error(permutation, partition): + "Test value error for for wrong input formats" + with pytest.raises(ValueError): + ap.twisted_spherical_function(permutation, partition) + + +@pytest.mark.parametrize("half_degree", range(1, 3)) def test_weingarten_symplectic_hyperoctahedral_symbolic(half_degree): """Symbolic validation of symplectic Weingarten function against results shown in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ if half_degree == 1: - for permutation in SymmetricGroup(2*half_degree).generate(): - assert ap.weingarten_symplectic(permutation, d) == ( - permutation.signature()/(2*d) - ) + for permutation in SymmetricGroup(2 * half_degree).generate(): + assert ap.weingarten_symplectic(permutation, d) == (permutation.signature() / (2 * d)) else: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) - coefficient = permutation.signature()/(4*d*(d-1)*(2*d+1)) + coefficient = permutation.signature() / (4 * d * (d - 1) * (2 * d + 1)) assert ap.weingarten_symplectic(permutation, d) == ( - simplify((2*d-1)*coefficient) if permutation in hyperoctahedral + simplify((2 * d - 1) * coefficient) + if permutation in hyperoctahedral else coefficient ) -@pytest.mark.parametrize("half_degree", range(1,3)) +@pytest.mark.parametrize("half_degree", range(1, 3)) def test_weingarten_symplectic_hyperoctahedral_numeric(half_degree): """Symbolic validation of symplectic Weingarten function against results shown in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ if half_degree == 1: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): assert ap.weingarten_symplectic(permutation, 7) == ( - Fraction(permutation.signature(),(2*7)) + Fraction(permutation.signature(), (2 * 7)) ) else: - for permutation in SymmetricGroup(2*half_degree).generate(): + for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) - coefficient = Fraction(permutation.signature(),(4*7*(7-1)*(2*7+1))) + coefficient = Fraction(permutation.signature(), (4 * 7 * (7 - 1) * (2 * 7 + 1))) assert ap.weingarten_symplectic(permutation, 7) == ( - (2*7-1)*coefficient if permutation in hyperoctahedral - else coefficient + (2 * 7 - 1) * coefficient if permutation in hyperoctahedral else coefficient ) @@ -179,69 +223,189 @@ def test_weingarten_symplectic_hyperoctahedral_numeric(half_degree): [ (Permutation(1)), (Permutation(3)), - (Permutation(0,1,2,3)), + (Permutation(0, 1, 2, 3)), (Permutation(5)), - (Permutation(2,3,4,5)), - (Permutation(0,1,2,3,4,5)), - (Permutation(0,1,2,3,4,5,6,7)), - (Permutation(0,1,2,3,4,7)(5,6)), - (Permutation(0,1,2,3)(4,5,6,7)), - (Permutation(4,5,6,7)), + (Permutation(2, 3, 4, 5)), + (Permutation(0, 1, 2, 3, 4, 5)), + (Permutation(0, 1, 2, 3, 4, 5, 6, 7)), + (Permutation(0, 1, 2, 3, 4, 7)(5, 6)), + (Permutation(0, 1, 2, 3)(4, 5, 6, 7)), + (Permutation(4, 5, 6, 7)), (Permutation(7)), - ] + ], ) def test_weingarten_symplectic_orthogonal_relation(permutation): - """Symbolic validation of the relation between the symplectic and + """Symbolic validation of the relation between the symplectic and orthogonal Weingarten functions as seen in - `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: + `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ assert ap.weingarten_symplectic(permutation, d) == simplify( - (-1) ** (permutation.size//2) + (-1) ** (permutation.size // 2) * permutation.signature() - * ap.weingarten_orthogonal(permutation, -2*d) + * ap.weingarten_orthogonal(permutation, -2 * d) ) @pytest.mark.parametrize( - "permutation, partition", + "permutation", [ - (Permutation(3,), [2,]), - ((3,1), (1,1)), - (Permutation(5,)(0,1), 'a'), - ('a', (3,)), - (Permutation(5,)(0,3,4), 7), - (7, (1,1,1)), + (Permutation(2)), + (Permutation(4)), + (Permutation(0, 1, 2, 3, 4)), + (Permutation(6)), ], ) -def test_twisted_spherical_function_type_error(permutation, partition): +def test_weingarten_symplectic_degree_value_error(permutation): + "Test value error for odd symmetric group degree" + with pytest.raises(ValueError, match="The degree of the symmetric group S_2k should be even"): + ap.weingarten_symplectic(permutation, d) + + +@pytest.mark.parametrize( + "seq_i, seq_j, half_dimension", + [ + ((0, 0, 0, 0), (0, 0, 0, 0), 2), + ((0, 1, 0, 1), (0, 0, 0, 0), 2), + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3), + ((0, 1, 2, 0, 1, 2), (0, 0, 0, 0, 0, 0), 3), + ((0, 0, 1, 0, 0, 1), (0, 2, 2, 0, 2, 2), 3), + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0), 4), + ((0, 1, 2, 3, 0, 1, 2, 3), (0, 0, 0, 0, 0, 0, 0, 0), 4), + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4), + ], +) +def test_haar_integral_symplectic_symbolic_numeric(seq_i, seq_j, half_dimension): + """Test haar integral symplectic moments against Monte Carlo simulation by calling the + function with both a symbolic and a numeric dimension + """ + half_length = len(seq_i) // 2 + seq_i_symbolic = seq_i[:half_length] + tuple(i + d for i in seq_i[half_length:]) + seq_j_symbolic = seq_j[:half_length] + tuple(j + d for j in seq_j[half_length:]) + seq_i_numeric = seq_i[:half_length] + tuple(i + half_dimension for i in seq_i[half_length:]) + seq_j_numeric = seq_j[:half_length] + tuple(j + half_dimension for j in seq_j[half_length:]) + + integral_symb = float( + ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d).subs(d, half_dimension) + ) + integral_num = float( + ap.haar_integral_symplectic((seq_i_numeric, seq_j_numeric), half_dimension) + ) + mc_integral = symplectic_dict[(seq_i, seq_j, half_dimension)] + + assert mc_integral == integral_symb == integral_num + + +@pytest.mark.parametrize( + "sequences", + [ + ((1, 2, 3, 4), (1, 2, 3, 4), (1, 2, 3, 4)), + ((1, 2, 3, 4),), + ((1, 2, 3, 4), (1, 2, 3, 4, 5, 6)), + ((1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 5, 6, 7)), + ], +) +def test_haar_integral_symplectic_value_error_wrong_tuple(sequences): + "Value error for wrong sequence format" + with pytest.raises(ValueError, match="Wrong sequence format"): + ap.haar_integral_symplectic(sequences, d) + + +@pytest.mark.parametrize( + "sequences", + [ + (("a", "b", "c", "d"), (1, 2, 3, 4)), + ((1, 1, d + 1, d + 1), (1, 1, 1, 1)), + ], +) +def test_haar_integral_symplectic_type_error_integer_dimension(sequences): + "Type error for integer dimension with not integer sequences" + dimension = randint(1, 99) with pytest.raises(TypeError): - ap.twisted_spherical_function(permutation, partition) + ap.haar_integral_symplectic(sequences, dimension) @pytest.mark.parametrize( - "permutation, partition", + "sequences, dimension", [ - (Permutation(3,), (2,2)), - (Permutation(3,), (1,1,1)), - (Permutation(4,), (2,1)), - (Permutation(4,), (1,1,1)), + (((1, 3), (1, 3)), 1), + (((1, 2, 3, 5), (1, 2, 3, 4)), 2), + (((1, 2, 3, 41), (1, 2, 3, 41)), 20), ], ) -def test_twisted_spherical_function_degree_value_error(permutation, partition): - with pytest.raises(ValueError): - ap.twisted_spherical_function(permutation, partition) +def test_haar_integral_symplectic_value_error_outside_dimension_range(sequences, dimension): + "Value error for sequences values outside dimension range" + with pytest.raises( + ValueError, + match="The matrix indices are outside the dimension range", + ): + ap.haar_integral_symplectic(sequences, dimension) @pytest.mark.parametrize( - "permutation", + "sequences", [ - (Permutation(2)), - (Permutation(4)), - (Permutation(0,1,2,3,4)), - (Permutation(6)), - ] + ((1, 2, 3, 4), (1, 2, 3, "a")), + ((1, 2, 3, 4), (1, 2, 3, {1, 2})), + ((1, 2, 3, 4), (1, 2, 3, 4 * d)), + ((1, 2, 3, 2 * d + 1), (1, 2, 3, 4)), + ((1, 2, 3, d + 1), (1, 2, 3, 4.0)), + ((1, 2, 3, 4), (1, 2, 3, d**2)), + ((1, 2, 3, 4), (1, 2, 3, 1 + d**2 + d)), + ((1, 2, 3, 4), (1, 2, 3, d + Symbol("s"))), + ], ) -def test_weingarten_symplectic_degree_value_error(permutation): - with pytest.raises(ValueError, match = "The degree of the symmetric group S_2k should be even"): - ap.weingarten_symplectic(permutation, d) +def test_haar_integral_symplectic_type_error_wrong_format(sequences): + "Type error for symbolic dimension with wrong sequence format" + with pytest.raises(TypeError): + ap.haar_integral_symplectic(sequences, d) + + +@pytest.mark.parametrize( + "dimension", + [ + "a", + [1, 2], + {1, 2}, + 3.0, + ], +) +def test_haar_integral_symplectic_wrong_dimension_format(dimension): + "Type error if the symplectic dimension is not an int nor a symbol" + with pytest.raises(TypeError): + ap.haar_integral_symplectic(((1, 2, 3, 4), (1, 2, 3, 4)), dimension) + + +@pytest.mark.parametrize( + "sequences, dimension", + [ + (((1, 1, 1), (1, 1, 1)), d), + (((1, 1, 1, 1, 1), (1, 1, 1, 1, 1)), d), + (((1, 1 + d, 1 + d), (1, 1 + d, 1 + d)), d), + (((1, 1, 1, 1), (1, 1, 1, 1)), d), + (((1, 1, d + 1, d + 2), (1, 1, d + 1, d + 1)), d), + (((1, 0, 0, d), (1, d + 1, 0, d)), d), + (((0, 0, 0), (0, 0, 0)), 2), + (((0, 0, 0, 0), (0, 0, 0, 0)), 2), + (((1, 2, 3, 3), (1, 2, 3, 3)), 4), + (((1, 1, 5, 5, 5), (1, 1, 5, 5, 5)), 4), + ], +) +def test_haar_integral_symplectic_zero_cases(sequences, dimension): + "Test cases that yield zero" + assert not ap.haar_integral_symplectic(sequences, dimension) + + +@pytest.mark.parametrize("half_degree", range(1, 5)) +def test_haar_integral_symplectic_weingarten_reconciliation(half_degree): + "Test single permutation moments match the symplectic weingarten function" + seq_dim_base = tuple(i + d for i in range(half_degree)) + sequence = tuple(i + 1 for pair in zip(range(half_degree), seq_dim_base) for i in pair) + + for perm in ap.hyperoctahedral_transversal(2 * half_degree): + inv_perm = ~perm + perm_sequence = tuple(inv_perm(sequence)) + + assert ap.haar_integral_symplectic( + (sequence, perm_sequence), d + ) == ap.weingarten_symplectic(perm, d)