From ca10bbdf0d725953dc29a9fb84f5c44f4c5560a7 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Thu, 13 Nov 2025 22:41:39 +0900 Subject: [PATCH 01/24] symplectic moments --- haarpy/__init__.py | 3 ++ haarpy/symplectic.py | 75 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/haarpy/__init__.py b/haarpy/__init__.py index e28b65d..73a8d99 100644 --- a/haarpy/__init__.py +++ b/haarpy/__init__.py @@ -34,6 +34,7 @@ weingarten_centered_permutation haar_integral_unitary haar_integral_orthogonal + haar_integral_symplectic haar_integral_circular_orthogonal haar_integral_permutation haar_integral_centered_permutation @@ -104,6 +105,7 @@ from .symplectic import ( twisted_spherical_function, weingarten_symplectic, + haar_integral_symplectic, ) from .circular_ensembles import ( @@ -132,6 +134,7 @@ "weingarten_centered_permutation", "haar_integral_unitary", "haar_integral_orthogonal", + "haar_integral_symplectic", "haar_integral_circular_orthogonal", "haar_integral_permutation", "haar_integral_centered_permutation", diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index 092a377..0d6de4f 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -17,11 +17,18 @@ from math import prod from fractions import Fraction +from itertools import product from functools import lru_cache from sympy import Symbol, factorial, factor, fraction, simplify from sympy.combinatorics import Permutation from sympy.utilities.iterables import partitions -from haarpy import get_conjugacy_class, murn_naka_rule, hyperoctahedral, irrep_dimension +from haarpy import ( + get_conjugacy_class, + murn_naka_rule, + hyperoctahedral, + irrep_dimension, + hyperoctahedral_transversal, +) @lru_cache @@ -145,3 +152,69 @@ def weingarten_symplectic( weingarten = simplify(factor(numerator) / factor(denominator)) return weingarten + + +@lru_cache +def haar_integral_symplectic( + sequences: tuple[tuple[int]], + symplectic_dimension: int, +) -> Fraction: + """Returns integral over symplectic group polynomial sampled at random from the Haar measure + + Args: + sequences (tuple[tuple[int]]): Indices of matrix elements + symplectic_dimension (int): Dimension of the symplectic 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 different length + TypeError: If the symplectic_dimension is not int + """ + if len(sequences) != 2: + raise ValueError("Wrong tuple format") + + seq_i, seq_j = sequences + degree = len(seq_i) + + if degree != len(seq_j): + raise ValueError("Wrong tuple format") + + if not isinstance(symplectic_dimension, int): + raise TypeError( + "Unlike other compact groups, " + "the symplectic group dimension must be an integer to compute the integral." + ) + + if degree % 2: + return 0 + + def twisted_delta(seq, permutation, n): + return prod( + ( + 1 + if 1 <= i <= n and j == i + n + else -1 if 1 <= j <= n and i == j + n else 0 + ) + for i, j in zip(permutation(seq)[::2], permutation(seq)[1::2]) + ) + + permutation_i = ( + (twisted_delta(seq_i, permutation, symplectic_dimension), permutation) + for permutation in hyperoctahedral_transversal(degree) + ) + + permutation_j = ( + (twisted_delta(seq_j, permutation, symplectic_dimension), permutation) + for permutation in hyperoctahedral_transversal(degree) + ) + + return sum( + perm_i[0] + * perm_j[0] + * weingarten_symplectic(perm_j[1] * ~perm_i[1], symplectic_dimension) + for perm_i, perm_j in product(permutation_i, permutation_j) + if perm_i[0] * perm_j[0] + ) From 0c78f05e247be37922f4cdc54fbe680b12ff8054 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Thu, 13 Nov 2025 23:41:01 +0900 Subject: [PATCH 02/24] symplectic moments --- haarpy/circular_ensembles.py | 69 ++++++++++++++++++++++++++++++++++++ haarpy/symplectic.py | 7 ++++ 2 files changed, 76 insertions(+) diff --git a/haarpy/circular_ensembles.py b/haarpy/circular_ensembles.py index c54f459..65fa36d 100644 --- a/haarpy/circular_ensembles.py +++ b/haarpy/circular_ensembles.py @@ -15,6 +15,7 @@ Circular ensembles Python interface """ +from math import prod from fractions import Fraction from functools import lru_cache from typing import Union @@ -107,3 +108,71 @@ def haar_integral_circular_orthogonal( integral = factor(numerator) / factor(denominator) return integral + + +@lru_cache +def haar_integral_circular_symplectic( + sequences: tuple[tuple[int]], group_dimension: int +) -> Fraction: + """Returns integral over circular symplectic ensemble polynomial + sampled at random from the Haar measure + + Args: + sequences (tuple[tuple[int]]) : Indices of matrix elements + group_dimension (Symbol) : Dimension of the orthogonal 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 group_dimension is not an int + ValueError: If all sequence indices are not between 1 and dimension + """ + if len(sequences) != 2: + raise ValueError("Wrong tuple format") + + seq_i, seq_j = sequences + + if len(seq_i) % 2 or len(seq_j) % 2: + raise ValueError("Wrong tuple format") + + if not isinstance(group_dimension, int): + raise TypeError( + "Unlike other ensembles, " + "the CSE dimension must be an integer to compute the integral." + ) + + if not ( + all(1 <= i <= group_dimension for i in seq_i) + and all(1 <= j <= group_dimension for j in seq_j) + ): + raise ValueError + + coefficient = prod( + -1 if 1 <= i <= group_dimension else 1 for i in (seq_i + seq_j)[::2] + ) + + shifted_i = tuple( + ( + i + group_dimension + if 1 <= i <= group_dimension and index % 2 == 0 + else i - group_dimension if index % 2 == 0 else i + ) + for index, i in enumerate(seq_i) + ) + + shifted_j = tuple( + ( + i + group_dimension + if 1 <= i <= group_dimension and index % 2 == 0 + else i - group_dimension if index % 2 == 0 else i + ) + for index, i in enumerate(seq_j) + ) + + return coefficient * sum( + weingarten_circular_symplectic(permutation, group_dimension) + for permutation in stabilizer_coset(shifted_i, shifted_j) + ) diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index 0d6de4f..e96f286 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -172,6 +172,7 @@ def haar_integral_symplectic( ValueError: If sequences doesn't contain 2 tuples ValueError: If tuples i and j are of different length TypeError: If the symplectic_dimension is not int + ValueError: If all sequence indices are not between 1 and dimension """ if len(sequences) != 2: raise ValueError("Wrong tuple format") @@ -188,6 +189,12 @@ def haar_integral_symplectic( "the symplectic group dimension must be an integer to compute the integral." ) + if not ( + all(1 <= i <= symplectic_dimension for i in seq_i) + and all(1 <= j <= symplectic_dimension for j in seq_j) + ): + raise ValueError + if degree % 2: return 0 From dbe6a5d74e317c5c6ae724b1c0b6567e33e8e919 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Tue, 2 Dec 2025 17:33:46 +0900 Subject: [PATCH 03/24] haar symplectic --- haarpy/symplectic.py | 127 ++++++++++++++++------- haarpy/tests/test_symplectic.py | 178 +++++++++++++++++++++++++++----- 2 files changed, 240 insertions(+), 65 deletions(-) diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index 604defb..fb43f8a 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -17,11 +17,11 @@ from math import prod from fractions import Fraction -from itertools import product from functools import lru_cache -from sympy import Symbol, factorial, factor, fraction, simplify +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, @@ -156,14 +156,14 @@ def weingarten_symplectic( @lru_cache def haar_integral_symplectic( - sequences: tuple[tuple[int]], - symplectic_dimension: int, -) -> Fraction: + sequences: tuple[tuple[Expr]], + symplectic_dimension: Symbol, +) -> Expr: """Returns integral over symplectic group polynomial sampled at random from the Haar measure Args: - sequences (tuple[tuple[int]]): Indices of matrix elements - symplectic_dimension (int): Dimension of the symplectic group + sequences (tuple[tuple[Expr]]): Indices of matrix elements + symplectic_dimension (Symbol): Dimension of the symplectic group Returns: Expr: Integral under the Haar measure @@ -175,53 +175,104 @@ def haar_integral_symplectic( ValueError: If all sequence indices are not between 1 and dimension """ if len(sequences) != 2: - raise ValueError("Wrong tuple format") + raise ValueError("Wrong sequence format") seq_i, seq_j = sequences + degree = len(seq_i) if degree != len(seq_j): - raise ValueError("Wrong tuple format") + raise ValueError("Wrong sequence format") - if not isinstance(symplectic_dimension, int): - raise TypeError( - "Unlike other compact groups, " - "the symplectic group dimension must be an integer to compute the integral." + if isinstance(symplectic_dimension, int): + if not all(isinstance(i, int) for i in seq_i + seq_j): + raise TypeError + if not all(1 <= i <= 2 * symplectic_dimension for i in seq_i + seq_j): + raise ValueError("The matrix indices are outside the dimension range") + seq_i_position = tuple(0 if i <= symplectic_dimension else 1 for i in seq_i) + seq_j_position = tuple(0 if j <= symplectic_dimension else 1 for j in seq_j) + seq_i_value = tuple( + i if i <= symplectic_dimension else i - symplectic_dimension for i in seq_i + ) + seq_j_value = tuple( + j if j <= symplectic_dimension else j - symplectic_dimension for j in seq_j ) + elif isinstance(symplectic_dimension, Symbol): + if not all(isinstance(i, (int, Expr)) for i in seq_i + seq_j): + raise TypeError - if not ( - all(1 <= i <= symplectic_dimension for i in seq_i) - and all(1 <= j <= symplectic_dimension for j in seq_j) - ): - raise ValueError + if not all( + ( + len(xpr.as_ordered_terms()) == 2 + and xpr.as_ordered_terms()[0] == symplectic_dimension + and isinstance(xpr.as_ordered_terms()[1], Integer) + ) + or xpr == symplectic_dimension + or xpr == 2 * symplectic_dimension + for xpr in seq_i + seq_j + if isinstance(xpr, Expr) + ): + raise TypeError + seq_i_position = tuple( + 0 if isinstance(i, int) or i == symplectic_dimension else 1 for i in seq_i + ) + seq_j_position = tuple( + 0 if isinstance(j, int) or j == symplectic_dimension else 1 for j in seq_j + ) + seq_i_value = tuple( + ( + i + if isinstance(i, int) + else 0 if i in (symplectic_dimension, 2*symplectic_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 in (symplectic_dimension, 2*symplectic_dimension) + else j.as_ordered_terms()[1] + ) + for j in seq_j + ) + else: + raise TypeError if degree % 2: return 0 - def twisted_delta(seq, permutation, n): - return prod( - ( - 1 - if 1 <= i <= n and j == i + n - else -1 if 1 <= j <= n and i == j + n else 0 + 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]) ) - for i, j in zip(permutation(seq)[::2], permutation(seq)[1::2]) ) - permutation_i = ( - (twisted_delta(seq_i, permutation, symplectic_dimension), permutation) - for permutation in hyperoctahedral_transversal(degree) + permutation_i_tuple = tuple( + (perm, twisted_delta(seq_i_value, seq_i_position, perm)) + for perm in hyperoctahedral_transversal(degree) ) - - permutation_j = ( - (twisted_delta(seq_j, permutation, symplectic_dimension), permutation) - for permutation in hyperoctahedral_transversal(degree) + permutation_j_tuple = tuple( + (perm, twisted_delta(seq_j_value, seq_j_position, perm)) + for perm in hyperoctahedral_transversal(degree) ) - return sum( - perm_i[0] - * perm_j[0] - * weingarten_symplectic(perm_j[1] * ~perm_i[1], symplectic_dimension) - for perm_i, perm_j in product(permutation_i, permutation_j) - if perm_i[0] * perm_j[0] + integral = sum( + perm_i[1] + * perm_j[1] + * weingarten_symplectic(perm_j[0] * ~perm_i[0], symplectic_dimension) + for perm_i, perm_j in zip(permutation_i_tuple, permutation_j_tuple) + if perm_i[1] * perm_j[1] ) + + if isinstance(symplectic_dimension, Expr): + numerator, denominator = fraction(simplify(integral)) + integral = factor(numerator) / factor(denominator) + + return integral diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index ea8c3b4..75acb63 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -16,6 +16,7 @@ """ from math import factorial +from random import seed, randint, choice from fractions import Fraction from sympy import Symbol, simplify from sympy.combinatorics import Permutation, SymmetricGroup @@ -23,6 +24,7 @@ import pytest import haarpy as ap +seed(137) d = Symbol('d') @@ -132,6 +134,38 @@ def test_twisted_spherical_orthogonality_transversal_none_zero(permutation, part assert convolution == orthogonality +@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 @@ -204,44 +238,134 @@ def test_weingarten_symplectic_orthogonal_relation(permutation): @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( + "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,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", + [ + ((1,1,1),(1,1,1)), + ((1,1,1,1,1),(1,1,1,1,1)), + ((1,1+d,1+d),(1,1+d,1+d)), + ((1,1,1,1),(1,1,1,1)), + ((1,1,d+1,d+2),(1,1,d+1,d+1)), + ((1,d,d,2*d),(1,d+1,d,2*d)), + ] +) +def test_haar_integral_symplectic_zero_cases(sequences): + "Test cases that yield zero" + assert not ap.haar_integral_symplectic(sequences, d) + + +@pytest.mark.parametrize("half_degree", range(1,4)) +def test_haar_integral_symplectic_weingarten_reconciliation(half_degree): + "Test single permutation moments match the symplectic weingarten function" + seq_dim = tuple(i+d for i in range(half_degree)) + perm_base = tuple(2*i for i in range(half_degree)) + 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 permutation in SymmetricGroup(half_degree).generate(): + perm_half = permutation([2*i+1 for i in range(half_degree)]) + perm = Permutation([i for pair in zip(perm_base, perm_half) for i in pair]) + + perm_dim = permutation(seq_dim) + perm_sequence = tuple(i+1 for pair in zip(range(half_degree), perm_dim) for i in pair) + + assert ( + ap.haar_integral_symplectic((sequence, perm_sequence), d) + == ap.weingarten_symplectic(perm, d) + ) From e4745590e3af49abc6f109f3326dece866a91653 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Tue, 2 Dec 2025 18:15:59 +0900 Subject: [PATCH 04/24] sympletic moment --- haarpy/symplectic.py | 3 ++- haarpy/tests/test_symplectic.py | 14 +++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index fb43f8a..40934cf 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -18,6 +18,7 @@ from math import prod from fractions import Fraction from functools import lru_cache +from itertools import product from sympy import Symbol, factorial, factor, fraction, simplify, Expr from sympy.combinatorics import Permutation from sympy.utilities.iterables import partitions @@ -267,7 +268,7 @@ def twisted_delta(seq_value, seq_pos, perm): perm_i[1] * perm_j[1] * weingarten_symplectic(perm_j[0] * ~perm_i[0], symplectic_dimension) - for perm_i, perm_j in zip(permutation_i_tuple, permutation_j_tuple) + for perm_i, perm_j in product(permutation_i_tuple, permutation_j_tuple) if perm_i[1] * perm_j[1] ) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index 75acb63..ef01674 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -350,22 +350,18 @@ def test_haar_integral_symplectic_zero_cases(sequences): assert not ap.haar_integral_symplectic(sequences, d) -@pytest.mark.parametrize("half_degree", range(1,4)) +@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 = tuple(i+d for i in range(half_degree)) - perm_base = tuple(2*i for i in range(half_degree)) 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 permutation in SymmetricGroup(half_degree).generate(): - perm_half = permutation([2*i+1 for i in range(half_degree)]) - perm = Permutation([i for pair in zip(perm_base, perm_half) for i in pair]) - perm_dim = permutation(seq_dim) - perm_sequence = tuple(i+1 for pair in zip(range(half_degree), perm_dim) 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) ) + From bf067fd8c12ffbab3ed723cb6174957e23642127 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 12:31:46 +0900 Subject: [PATCH 05/24] symplectic moment from PR #41 --- haarpy/symplectic.py | 32 +++---- haarpy/tests/test_symplectic.py | 148 +++++++++++++++++++++++++++++++- requirements_dev.txt | 3 +- 3 files changed, 161 insertions(+), 22 deletions(-) diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index 40934cf..b241a89 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -172,8 +172,10 @@ def haar_integral_symplectic( Raise: ValueError: If sequences doesn't contain 2 tuples ValueError: If tuples i and j are of different length - TypeError: If the symplectic_dimension is not int - ValueError: If all sequence indices are not between 1 and dimension + TypeError: If the symplectic_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 symbolic sequences have the wrong format """ if len(sequences) != 2: raise ValueError("Wrong sequence format") @@ -188,15 +190,15 @@ def haar_integral_symplectic( if isinstance(symplectic_dimension, int): if not all(isinstance(i, int) for i in seq_i + seq_j): raise TypeError - if not all(1 <= i <= 2 * symplectic_dimension for i in seq_i + seq_j): + if not all(0 <= i <= 2 * symplectic_dimension - 1 for i in seq_i + seq_j): raise ValueError("The matrix indices are outside the dimension range") - seq_i_position = tuple(0 if i <= symplectic_dimension else 1 for i in seq_i) - seq_j_position = tuple(0 if j <= symplectic_dimension else 1 for j in seq_j) + seq_i_position = tuple(0 if i < symplectic_dimension else 1 for i in seq_i) + seq_j_position = tuple(0 if j < symplectic_dimension else 1 for j in seq_j) seq_i_value = tuple( - i if i <= symplectic_dimension else i - symplectic_dimension for i in seq_i + i if i < symplectic_dimension else i - symplectic_dimension for i in seq_i ) seq_j_value = tuple( - j if j <= symplectic_dimension else j - symplectic_dimension for j in seq_j + j if j < symplectic_dimension else j - symplectic_dimension for j in seq_j ) elif isinstance(symplectic_dimension, Symbol): if not all(isinstance(i, (int, Expr)) for i in seq_i + seq_j): @@ -209,23 +211,18 @@ def haar_integral_symplectic( and isinstance(xpr.as_ordered_terms()[1], Integer) ) or xpr == symplectic_dimension - or xpr == 2 * symplectic_dimension for xpr in seq_i + seq_j if isinstance(xpr, Expr) ): raise TypeError - seq_i_position = tuple( - 0 if isinstance(i, int) or i == symplectic_dimension else 1 for i in seq_i - ) - seq_j_position = tuple( - 0 if isinstance(j, int) or j == symplectic_dimension else 1 for j in seq_j - ) + 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 in (symplectic_dimension, 2*symplectic_dimension) - else i.as_ordered_terms()[1] + else 0 if i == symplectic_dimension else i.as_ordered_terms()[1] ) for i in seq_i ) @@ -233,8 +230,7 @@ def haar_integral_symplectic( ( j if isinstance(j, int) - else 0 if j in (symplectic_dimension, 2*symplectic_dimension) - else j.as_ordered_terms()[1] + else 0 if j == symplectic_dimension else j.as_ordered_terms()[1] ) for j in seq_j ) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index ef01674..eb7bd4e 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -15,12 +15,13 @@ Symplectic tests """ -from math import factorial -from random import seed, randint, choice +from math import factorial, prod +from random import seed, randint from fractions import Fraction from sympy import Symbol, simplify from sympy.combinatorics import Permutation, SymmetricGroup from sympy.utilities.iterables import partitions +import numpy as np import pytest import haarpy as ap @@ -28,6 +29,63 @@ d = Symbol('d') +def random_unit_quaternion(): + """Generate a random unit quaternion (q0,q1,q2,q3) with q0^2+q1^2+q2^2+q3^2 = 1.""" + vec = np.random.normal(size=4) + vec /= np.linalg.norm(vec) + return vec # [q0, q1, q2, q3] + + +def generate_random_usp(d): + """Generate a Haar-random USp(2d) matrix of size (2d x 2d).""" + # Step 1: Random quaternionic d x d matrix with i.i.d. N(0,1) components + A = np.random.normal(size=(d, d)) + B = np.random.normal(size=(d, d)) + C = np.random.normal(size=(d, d)) + D = np.random.normal(size=(d, d)) + # Prepare list to store orthonormal quaternion columns (each as 4 real component arrays) + basis = [] + # Step 2: Quaternionic Gram-Schmidt + for j in range(d): + # j-th column as quaternion vector components + v0, v1, v2, v3 = A[:, j].copy(), B[:, j].copy(), C[:, j].copy(), D[:, j].copy() + # Make orthogonal to previous basis vectors + for (u0, u1, u2, u3) in basis: + # Compute quaternion inner product: q = = sum_i conj(u_i)*v_i (a quaternion) + p0, p1, p2, p3 = u0, -u1, -u2, -u3 # components of conj(u) + q0 = p0 @ v0 - p1 @ v1 - p2 @ v2 - p3 @ v3 # (conj(u)·v)_real + q1 = p0 @ v1 + p1 @ v0 + p2 @ v3 - p3 @ v2 # (conj(u)·v)_i + q2 = p0 @ v2 - p1 @ v3 + p2 @ v0 + p3 @ v1 # (conj(u)·v)_j + q3 = p0 @ v3 + p1 @ v2 - p2 @ v1 + p3 @ v0 # (conj(u)·v)_k + # Subtract projection: v := v - u * q + t0 = u0 * q0 - u1 * q1 - u2 * q2 - u3 * q3 + t1 = u0 * q1 + u1 * q0 + u2 * q3 - u3 * q2 + t2 = u0 * q2 - u1 * q3 + u2 * q0 + u3 * q1 + t3 = u0 * q3 + u1 * q2 - u2 * q1 + u3 * q0 + v0 -= t0; v1 -= t1; v2 -= t2; v3 -= t3 + # Normalize v (so that = 1) + norm = np.sqrt(v0@v0 + v1@v1 + v2@v2 + v3@v3) + v0 /= norm; v1 /= norm; v2 /= norm; v3 /= norm + # Step 3: Multiply by a random unit quaternion (random phase) + q0, q1, q2, q3 = random_unit_quaternion() + t0 = v0*q0 - v1*q1 - v2*q2 - v3*q3 + t1 = v0*q1 + v1*q0 + v2*q3 - v3*q2 + t2 = v0*q2 - v1*q3 + v2*q0 + v3*q1 + t3 = v0*q3 + v1*q2 - v2*q1 + v3*q0 + v0, v1, v2, v3 = t0, t1, t2, t3 + basis.append((v0, v1, v2, v3)) + # Step 4: Assemble the complex 2d x 2d matrix from quaternion basis + d2 = 2 * d + U = np.zeros((d2, d2), dtype=np.complex128) + # Fill block entries for each quaternion column + for j, (u0, u1, u2, u3) in enumerate(basis): + U[:d, j] = u0 + 1j*u1 # top-left block + U[:d, j+d] = u2 + 1j*u3 # top-right block + U[d:, j] = -u2 + 1j*u3 # bottom-left block + U[d:, j+d] = u0 - 1j*u1 # bottom-right block + return U + + @pytest.mark.parametrize( "partition", [ @@ -342,7 +400,7 @@ def test_haar_integral_symplectic_wrong_dimension_format(dimension): ((1,1+d,1+d),(1,1+d,1+d)), ((1,1,1,1),(1,1,1,1)), ((1,1,d+1,d+2),(1,1,d+1,d+1)), - ((1,d,d,2*d),(1,d+1,d,2*d)), + ((1,0,0,d),(1,d+1,0,d)), ] ) def test_haar_integral_symplectic_zero_cases(sequences): @@ -365,3 +423,87 @@ def test_haar_integral_symplectic_weingarten_reconciliation(half_degree): == ap.weingarten_symplectic(perm, 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,1,0,1),(0,1,1,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,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), + ] +) +def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimension): + "Test haar integral symplectic moments against Monte Carlo simulation symbolic" + sample_size = int(1e4) + epsilon_real = 1e4 + epsilon_imag = 1e6 + + 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 = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) + integral = float(integral.subs(d, half_dimension)) + + monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) + monte_carlo_integral = sum( + prod( + symplectic_matrix[i-1, j-1] + for i, j in zip(seq_i_numeric, seq_j_numeric) + ) + for symplectic_matrix in monte_carlo_matrix + ) / sample_size + + assert ( + abs((integral-monte_carlo_integral.real)/integral) < epsilon_real + and abs(monte_carlo_integral.imag) < epsilon_imag + and integral != 0 + ) + + +@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,1,0,1),(0,1,1,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,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), + ] +) +def test_haar_integral_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dimension): + "Test haar integral symplectic moments against Monte Carlo simulation symbolic" + sample_size = int(1e4) + epsilon_real = 1e4 + epsilon_imag = 1e6 + + half_length = len(seq_i) // 2 + 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 = float(ap.haar_integral_symplectic((seq_i_numeric, seq_j_numeric), half_dimension)) + + monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) + monte_carlo_integral = sum( + prod( + symplectic_matrix[i-1, j-1] + for i, j in zip(seq_i_numeric, seq_j_numeric) + ) + for symplectic_matrix in monte_carlo_matrix + ) / sample_size + + assert ( + abs((integral-monte_carlo_integral.real)/integral) < epsilon_real + and abs(monte_carlo_integral.imag) < epsilon_imag + and integral != 0 + ) diff --git a/requirements_dev.txt b/requirements_dev.txt index 3566307..bea5174 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1 +1,2 @@ -pytest==8.3.2 \ No newline at end of file +pytest==8.3.2 +numpy==2.3.4 \ No newline at end of file From ae60505e0416f3748c2238fc89d9cade2d60b02a Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 12:32:34 +0900 Subject: [PATCH 06/24] symplectic moment from PR #41 --- haarpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haarpy/__init__.py b/haarpy/__init__.py index 95e4f11..04d1b47 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. From 9a76f5bd52537ec44037fd0bea9a07efb267f76a Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 12:36:13 +0900 Subject: [PATCH 07/24] symplectic moments from PR #41 --- haarpy/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/haarpy/__init__.py b/haarpy/__init__.py index 04d1b47..8c1ed55 100644 --- a/haarpy/__init__.py +++ b/haarpy/__init__.py @@ -36,6 +36,7 @@ haar_integral_orthogonal haar_integral_symplectic haar_integral_circular_orthogonal + haar_integral_circular_symplectic haar_integral_permutation haar_integral_centered_permutation get_conjugacy_class @@ -112,6 +113,7 @@ weingarten_circular_orthogonal, weingarten_circular_symplectic, haar_integral_circular_orthogonal, + haar_integral_circular_symplectic, ) from .permutation import ( @@ -136,6 +138,7 @@ "haar_integral_orthogonal", "haar_integral_symplectic", "haar_integral_circular_orthogonal", + "haar_integral_circular_symplectic", "haar_integral_permutation", "haar_integral_centered_permutation", "get_conjugacy_class", From 67a09c374a1afc10d7dc13195d74a80f74755fc9 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 17:48:05 +0900 Subject: [PATCH 08/24] symplectic moments merged in PR #43 --- haarpy/circular_ensembles.py | 139 ++++++++++++----- haarpy/symplectic.py | 52 +++---- haarpy/tests/test_circular_ensembles.py | 108 ++++++++++++++ haarpy/tests/test_symplectic.py | 191 ++++++++++++------------ requirements_dev.txt | 3 +- 5 files changed, 332 insertions(+), 161 deletions(-) diff --git a/haarpy/circular_ensembles.py b/haarpy/circular_ensembles.py index 65fa36d..5d82ecb 100644 --- a/haarpy/circular_ensembles.py +++ b/haarpy/circular_ensembles.py @@ -21,7 +21,8 @@ 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, @@ -112,14 +113,14 @@ def haar_integral_circular_orthogonal( @lru_cache def haar_integral_circular_symplectic( - sequences: tuple[tuple[int]], group_dimension: int -) -> Fraction: + 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 - group_dimension (Symbol) : Dimension of the orthogonal group + half_dimension (Symbol) : Half the dimension of the unitary group Returns: Expr : Integral under the Haar measure @@ -127,52 +128,110 @@ def haar_integral_circular_symplectic( Raise: ValueError : If sequences doesn't contain 2 tuples ValueError : If tuples i and j are of odd size - TypeError : If group_dimension is not an int - ValueError: If all sequence indices are not between 1 and dimension + 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 tuple format") seq_i, seq_j = sequences - if len(seq_i) % 2 or len(seq_j) % 2: - raise ValueError("Wrong tuple format") - - if not isinstance(group_dimension, int): - raise TypeError( - "Unlike other ensembles, " - "the CSE dimension must be an integer to compute the integral." - ) - - if not ( - all(1 <= i <= group_dimension for i in seq_i) - and all(1 <= j <= group_dimension for j in seq_j) - ): - raise ValueError + degree = len(seq_i) - coefficient = prod( - -1 if 1 <= i <= group_dimension else 1 for i in (seq_i + seq_j)[::2] - ) + if degree % 2 or len(seq_j) % 2: + raise ValueError("Wrong sequence format") - shifted_i = tuple( - ( - i + group_dimension - if 1 <= i <= group_dimension and index % 2 == 0 - else i - group_dimension if index % 2 == 0 else i + 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] ) - for index, i in enumerate(seq_i) - ) - - shifted_j = tuple( - ( - i + group_dimension - if 1 <= i <= group_dimension and index % 2 == 0 - else i - group_dimension if index % 2 == 0 else i + 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) + ) + 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] ) - for index, i in enumerate(seq_j) + 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 ) - return coefficient * sum( - weingarten_circular_symplectic(permutation, group_dimension) - for permutation in stabilizer_coset(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 b241a89..5bd9ba5 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -78,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 @@ -115,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, @@ -158,13 +156,13 @@ def weingarten_symplectic( @lru_cache def haar_integral_symplectic( sequences: tuple[tuple[Expr]], - symplectic_dimension: Symbol, + 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 - symplectic_dimension (Symbol): Dimension of the symplectic group + half_dimension (Symbol): Half the dimension of the symplectic group Returns: Expr: Integral under the Haar measure @@ -172,9 +170,10 @@ def haar_integral_symplectic( Raise: ValueError: If sequences doesn't contain 2 tuples ValueError: If tuples i and j are of different length - TypeError: If the symplectic_dimension is not int nor Symbol + 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: @@ -187,34 +186,38 @@ def haar_integral_symplectic( if degree != len(seq_j): raise ValueError("Wrong sequence format") - if isinstance(symplectic_dimension, int): + 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 * symplectic_dimension - 1 for i in seq_i + seq_j): + 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") - seq_i_position = tuple(0 if i < symplectic_dimension else 1 for i in seq_i) - seq_j_position = tuple(0 if j < symplectic_dimension else 1 for j in seq_j) + 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 < symplectic_dimension else i - symplectic_dimension for i in seq_i + i if i < half_dimension else i - half_dimension for i in seq_i ) seq_j_value = tuple( - j if j < symplectic_dimension else j - symplectic_dimension for j in seq_j + j if j < half_dimension else j - half_dimension for j in seq_j ) - elif isinstance(symplectic_dimension, Symbol): + 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] == symplectic_dimension + and xpr.as_ordered_terms()[0] == half_dimension and isinstance(xpr.as_ordered_terms()[1], Integer) ) - or xpr == symplectic_dimension + 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) @@ -222,7 +225,7 @@ def haar_integral_symplectic( ( i if isinstance(i, int) - else 0 if i == symplectic_dimension else i.as_ordered_terms()[1] + else 0 if i == half_dimension else i.as_ordered_terms()[1] ) for i in seq_i ) @@ -230,16 +233,13 @@ def haar_integral_symplectic( ( j if isinstance(j, int) - else 0 if j == symplectic_dimension else j.as_ordered_terms()[1] + else 0 if j == half_dimension else j.as_ordered_terms()[1] ) for j in seq_j ) else: raise TypeError - if degree % 2: - return 0 - def twisted_delta(seq_value, seq_pos, perm): return ( 0 @@ -263,12 +263,12 @@ def twisted_delta(seq_value, seq_pos, perm): integral = sum( perm_i[1] * perm_j[1] - * weingarten_symplectic(perm_j[0] * ~perm_i[0], symplectic_dimension) + * 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(symplectic_dimension, Expr): + if isinstance(half_dimension, Expr): numerator, denominator = fraction(simplify(integral)) integral = factor(numerator) / factor(denominator) diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index 4b26483..f596dc4 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -17,8 +17,10 @@ from math import prod from fractions import Fraction +import numpy as np from sympy import Symbol, simplify, factorial, factorial2 from sympy.combinatorics import SymmetricGroup +from scipy.stats import unitary_group import pytest import haarpy as ap @@ -194,3 +196,109 @@ 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),(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" + sample_size = int(1e4) + epsilon_real = 1e4 + epsilon = 1e6 + + integral = float(ap.haar_integral_circular_symplectic((seq_i, seq_j), half_dim)) + + unitary_gen = (unitary_group.rvs(2*half_dim) for _ in range(sample_size)) + + I = np.eye(half_dim, dtype=np.complex128) + Z = np.zeros((half_dim, half_dim), dtype=np.complex128) + J = np.block([[Z, I],[-I, Z]]) + + cse_gen = (J @ U.T @ J.T @ U for U in unitary_gen) + + monte_carlo_integral = sum( + prod( + cse[i-1, j-1] + for i, j in zip(seq_i, seq_j) + ) + for cse in cse_gen + ) / sample_size + + if integral: + assert ( + abs((integral-monte_carlo_integral.real)/integral) < epsilon_real + and abs(monte_carlo_integral.imag) < epsilon + and integral != 0 + ) + else: + assert ( + abs(monte_carlo_integral.real) < epsilon + and abs(monte_carlo_integral.imag) < epsilon + ) + + +@pytest.mark.parametrize( + 'seq_i, seq_j, half_dim', + [ + ((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" + sample_size = int(1e4) + epsilon_real = 1e4 + epsilon = 1e6 + + integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), d) + integral = float(integral.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) + + unitary_gen = (unitary_group.rvs(2*half_dim) for _ in range(sample_size)) + + I = np.eye(half_dim, dtype=np.complex128) + Z = np.zeros((half_dim, half_dim), dtype=np.complex128) + J = np.block([[Z, I],[-I, Z]]) + + cse_gen = (J @ U.T @ J.T @ U for U in unitary_gen) + + monte_carlo_integral = sum( + prod( + cse[i-1, j-1] + for i, j in zip(seq_i, seq_j) + ) + for cse in cse_gen + ) / sample_size + + if integral: + assert ( + abs((integral-monte_carlo_integral.real)/integral) < epsilon_real + and abs(monte_carlo_integral.imag) < epsilon + and integral != 0 + ) + else: + assert ( + abs(monte_carlo_integral.real) < epsilon + and abs(monte_carlo_integral.imag) < epsilon + ) \ No newline at end of file diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index eb7bd4e..89cd5b1 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -310,6 +310,91 @@ def test_weingarten_symplectic_degree_value_error(permutation): 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,1,0,1),(0,1,1,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,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), + ] +) +def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimension): + "Test haar integral symplectic moments against Monte Carlo simulation symbolic" + sample_size = int(1e4) + epsilon_real = 1e4 + epsilon_imag = 1e6 + + 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 = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) + integral = float(integral.subs(d, half_dimension)) + + monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) + monte_carlo_integral = sum( + prod( + symplectic_matrix[i-1, j-1] + for i, j in zip(seq_i_numeric, seq_j_numeric) + ) + for symplectic_matrix in monte_carlo_matrix + ) / sample_size + + assert ( + abs((integral-monte_carlo_integral.real)/integral) < epsilon_real + and abs(monte_carlo_integral.imag) < epsilon_imag + and integral != 0 + ) + + +@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,1,0,1),(0,1,1,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,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), + ] +) +def test_haar_integral_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dimension): + "Test haar integral symplectic moments against Monte Carlo simulation symbolic" + sample_size = int(1e4) + epsilon_real = 1e4 + epsilon_imag = 1e6 + + half_length = len(seq_i) // 2 + 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 = float(ap.haar_integral_symplectic((seq_i_numeric, seq_j_numeric), half_dimension)) + + monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) + monte_carlo_integral = sum( + prod( + symplectic_matrix[i-1, j-1] + for i, j in zip(seq_i_numeric, seq_j_numeric) + ) + for symplectic_matrix in monte_carlo_matrix + ) / sample_size + + assert ( + abs((integral-monte_carlo_integral.real)/integral) < epsilon_real + and abs(monte_carlo_integral.imag) < epsilon_imag + and integral != 0 + ) + + @pytest.mark.parametrize( "sequences", [ @@ -366,6 +451,7 @@ def test_haar_integral_symplectic_value_error_outside_dimension_range(sequences, ((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'))), @@ -393,19 +479,21 @@ def test_haar_integral_symplectic_wrong_dimension_format(dimension): @pytest.mark.parametrize( - "sequences", + "sequences, dimension", [ - ((1,1,1),(1,1,1)), - ((1,1,1,1,1),(1,1,1,1,1)), - ((1,1+d,1+d),(1,1+d,1+d)), - ((1,1,1,1),(1,1,1,1)), - ((1,1,d+1,d+2),(1,1,d+1,d+1)), - ((1,0,0,d),(1,d+1,0,d)), + (((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,0,0)), 2), + (((1,2,3,3), (1,2,3,3)), 4), ] ) -def test_haar_integral_symplectic_zero_cases(sequences): +def test_haar_integral_symplectic_zero_cases(sequences, dimension): "Test cases that yield zero" - assert not ap.haar_integral_symplectic(sequences, d) + assert not ap.haar_integral_symplectic(sequences, dimension) @pytest.mark.parametrize("half_degree", range(1,5)) @@ -422,88 +510,3 @@ def test_haar_integral_symplectic_weingarten_reconciliation(half_degree): ap.haar_integral_symplectic((sequence, perm_sequence), d) == ap.weingarten_symplectic(perm, 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,1,0,1),(0,1,1,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,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), - ] -) -def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimension): - "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - sample_size = int(1e4) - epsilon_real = 1e4 - epsilon_imag = 1e6 - - 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 = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) - integral = float(integral.subs(d, half_dimension)) - - monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) - monte_carlo_integral = sum( - prod( - symplectic_matrix[i-1, j-1] - for i, j in zip(seq_i_numeric, seq_j_numeric) - ) - for symplectic_matrix in monte_carlo_matrix - ) / sample_size - - assert ( - abs((integral-monte_carlo_integral.real)/integral) < epsilon_real - and abs(monte_carlo_integral.imag) < epsilon_imag - and integral != 0 - ) - - -@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,1,0,1),(0,1,1,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,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), - ] -) -def test_haar_integral_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dimension): - "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - sample_size = int(1e4) - epsilon_real = 1e4 - epsilon_imag = 1e6 - - half_length = len(seq_i) // 2 - 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 = float(ap.haar_integral_symplectic((seq_i_numeric, seq_j_numeric), half_dimension)) - - monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) - monte_carlo_integral = sum( - prod( - symplectic_matrix[i-1, j-1] - for i, j in zip(seq_i_numeric, seq_j_numeric) - ) - for symplectic_matrix in monte_carlo_matrix - ) / sample_size - - assert ( - abs((integral-monte_carlo_integral.real)/integral) < epsilon_real - and abs(monte_carlo_integral.imag) < epsilon_imag - and integral != 0 - ) diff --git a/requirements_dev.txt b/requirements_dev.txt index bea5174..e9fc8b6 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,2 +1,3 @@ pytest==8.3.2 -numpy==2.3.4 \ No newline at end of file +numpy==2.3.4 +scipy==1.16.3 \ No newline at end of file From c32965127cf23ca05b3c2a919ee710237f372665 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 20:51:25 +0900 Subject: [PATCH 09/24] symplectic moment merged from PR #43 --- haarpy/circular_ensembles.py | 2 +- haarpy/tests/test_circular_ensembles.py | 99 ++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/haarpy/circular_ensembles.py b/haarpy/circular_ensembles.py index 5d82ecb..9dbb967 100644 --- a/haarpy/circular_ensembles.py +++ b/haarpy/circular_ensembles.py @@ -135,7 +135,7 @@ def haar_integral_circular_symplectic( TypeError: If symbolic sequences have the wrong format """ if len(sequences) != 2: - raise ValueError("Wrong tuple format") + raise ValueError("Wrong sequence format") seq_i, seq_j = sequences diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index f596dc4..48ebd24 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -16,6 +16,7 @@ """ from math import prod +from random import randint from fractions import Fraction import numpy as np from sympy import Symbol, simplify, factorial, factorial2 @@ -301,4 +302,100 @@ def test_haar_integral_circular_symplectic_monte_carlo_symbolic(seq_i, seq_j, ha assert ( abs(monte_carlo_integral.real) < epsilon and abs(monte_carlo_integral.imag) < epsilon - ) \ No newline at end of file + ) + + +@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,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) From d9895769e31d0ea53b43d80fc6e7f8ab70af12a6 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 21:34:10 +0900 Subject: [PATCH 10/24] symplectic moment merged from PR #43 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index e9fc8b6..a4e7215 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ pytest==8.3.2 -numpy==2.3.4 +numpy==1.25.0 scipy==1.16.3 \ No newline at end of file From 94ebdebe73b1e1fc2c61a1e32800ed51286df360 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 21:47:54 +0900 Subject: [PATCH 11/24] symplectic moment merged from PR #43 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index a4e7215..c02d7ff 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ pytest==8.3.2 numpy==1.25.0 -scipy==1.16.3 \ No newline at end of file +scipy==1.1.0 \ No newline at end of file From 2b161361b15692cc44196d028c32fa88a97251ba Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 21:49:58 +0900 Subject: [PATCH 12/24] symplectic moment merged from PR #43 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index c02d7ff..a4fae6d 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ pytest==8.3.2 numpy==1.25.0 -scipy==1.1.0 \ No newline at end of file +scipy==1.2.1 \ No newline at end of file From b162f31c2c04ffb27c3d5189d330ed57842bcb5b Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 21:54:15 +0900 Subject: [PATCH 13/24] symplectic moment merged from PR #43 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index a4fae6d..a4e7215 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ pytest==8.3.2 numpy==1.25.0 -scipy==1.2.1 \ No newline at end of file +scipy==1.16.3 \ No newline at end of file From 4e13254c6cc0c2d92b72202434e556302e553260 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Wed, 3 Dec 2025 21:55:06 +0900 Subject: [PATCH 14/24] symplectic moment merged from PR #43 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index a4e7215..0a564bf 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ pytest==8.3.2 numpy==1.25.0 -scipy==1.16.3 \ No newline at end of file +scipy==1.13.1 \ No newline at end of file From 8a85dc57e05efd10c8fba8ad0d6fb62d7dd48dd4 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Thu, 4 Dec 2025 15:33:23 +0900 Subject: [PATCH 15/24] symplectic moments merged in PR #43 --- haarpy/tests/test_symplectic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index 89cd5b1..0168fd8 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -487,8 +487,10 @@ def test_haar_integral_symplectic_wrong_dimension_format(dimension): (((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): From 567444d52ff27647fb9fa9aed10e3d8db8af0923 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Fri, 5 Dec 2025 13:49:26 +0900 Subject: [PATCH 16/24] symplectic moments merged from PR #43 --- haarpy/tests/test_symplectic.py | 112 +++++++------------------------- requirements_dev.txt | 4 +- 2 files changed, 23 insertions(+), 93 deletions(-) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index 0168fd8..c457f0d 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -15,75 +15,27 @@ Symplectic tests """ -from math import factorial, prod -from random import seed, randint +from math import factorial +from random import randint from fractions import Fraction from sympy import Symbol, simplify from sympy.combinatorics import Permutation, SymmetricGroup from sympy.utilities.iterables import partitions -import numpy as np import pytest import haarpy as ap -seed(137) d = Symbol('d') - -def random_unit_quaternion(): - """Generate a random unit quaternion (q0,q1,q2,q3) with q0^2+q1^2+q2^2+q3^2 = 1.""" - vec = np.random.normal(size=4) - vec /= np.linalg.norm(vec) - return vec # [q0, q1, q2, q3] - - -def generate_random_usp(d): - """Generate a Haar-random USp(2d) matrix of size (2d x 2d).""" - # Step 1: Random quaternionic d x d matrix with i.i.d. N(0,1) components - A = np.random.normal(size=(d, d)) - B = np.random.normal(size=(d, d)) - C = np.random.normal(size=(d, d)) - D = np.random.normal(size=(d, d)) - # Prepare list to store orthonormal quaternion columns (each as 4 real component arrays) - basis = [] - # Step 2: Quaternionic Gram-Schmidt - for j in range(d): - # j-th column as quaternion vector components - v0, v1, v2, v3 = A[:, j].copy(), B[:, j].copy(), C[:, j].copy(), D[:, j].copy() - # Make orthogonal to previous basis vectors - for (u0, u1, u2, u3) in basis: - # Compute quaternion inner product: q = = sum_i conj(u_i)*v_i (a quaternion) - p0, p1, p2, p3 = u0, -u1, -u2, -u3 # components of conj(u) - q0 = p0 @ v0 - p1 @ v1 - p2 @ v2 - p3 @ v3 # (conj(u)·v)_real - q1 = p0 @ v1 + p1 @ v0 + p2 @ v3 - p3 @ v2 # (conj(u)·v)_i - q2 = p0 @ v2 - p1 @ v3 + p2 @ v0 + p3 @ v1 # (conj(u)·v)_j - q3 = p0 @ v3 + p1 @ v2 - p2 @ v1 + p3 @ v0 # (conj(u)·v)_k - # Subtract projection: v := v - u * q - t0 = u0 * q0 - u1 * q1 - u2 * q2 - u3 * q3 - t1 = u0 * q1 + u1 * q0 + u2 * q3 - u3 * q2 - t2 = u0 * q2 - u1 * q3 + u2 * q0 + u3 * q1 - t3 = u0 * q3 + u1 * q2 - u2 * q1 + u3 * q0 - v0 -= t0; v1 -= t1; v2 -= t2; v3 -= t3 - # Normalize v (so that = 1) - norm = np.sqrt(v0@v0 + v1@v1 + v2@v2 + v3@v3) - v0 /= norm; v1 /= norm; v2 /= norm; v3 /= norm - # Step 3: Multiply by a random unit quaternion (random phase) - q0, q1, q2, q3 = random_unit_quaternion() - t0 = v0*q0 - v1*q1 - v2*q2 - v3*q3 - t1 = v0*q1 + v1*q0 + v2*q3 - v3*q2 - t2 = v0*q2 - v1*q3 + v2*q0 + v3*q1 - t3 = v0*q3 + v1*q2 - v2*q1 + v3*q0 - v0, v1, v2, v3 = t0, t1, t2, t3 - basis.append((v0, v1, v2, v3)) - # Step 4: Assemble the complex 2d x 2d matrix from quaternion basis - d2 = 2 * d - U = np.zeros((d2, d2), dtype=np.complex128) - # Fill block entries for each quaternion column - for j, (u0, u1, u2, u3) in enumerate(basis): - U[:d, j] = u0 + 1j*u1 # top-left block - U[:d, j+d] = u2 + 1j*u3 # top-right block - U[d:, j] = -u2 + 1j*u3 # bottom-left block - U[d:, j+d] = u0 - 1j*u1 # bottom-right block - return U +monte_carlo_symplectic_dict = { + ((0, 0, 0, 0), (0, 0, 0, 0), 2) : (0.10033684843833776-1.145379354559459e-20j), + ((0, 1, 0, 1), (0, 0, 0, 0), 2) : (-0.04990811793582537-8.943783307107027e-21j), + ((0, 1, 0, 1), (0, 1, 1, 0), 2) : (-0.02505580071352494-9.625388767948332e-07j), + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3) : (0.017633018076551557-1.0423137720400771e-20j), + ((0, 1, 2, 0, 1, 2), (0, 0, 0, 0, 0, 0), 3) : (0.0029691649199574807-4.311344798154337e-22j), + ((0, 0, 1, 0, 0, 1), (0, 2, 2, 0, 2, 2), 3) : (-0.0037265932000567895-4.301613654142747e-23j), + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0), 4) : (0.0030119499237324467-3.579791351992152e-21j), + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4) : (0.0005034436860443855-3.83813858200423e-22j), +} @pytest.mark.parametrize( @@ -315,7 +267,6 @@ def test_weingarten_symplectic_degree_value_error(permutation): [ ((0,0,0,0),(0,0,0,0), 2), ((0,1,0,1),(0,0,0,0), 2), - ((0,1,0,1),(0,1,1,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), @@ -325,31 +276,21 @@ def test_weingarten_symplectic_degree_value_error(permutation): ) def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimension): "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - sample_size = int(1e4) - epsilon_real = 1e4 - epsilon_imag = 1e6 + epsilon_real = 5e-2 + epsilon_imag = 1e-6 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 = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) integral = float(integral.subs(d, half_dimension)) - monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) - monte_carlo_integral = sum( - prod( - symplectic_matrix[i-1, j-1] - for i, j in zip(seq_i_numeric, seq_j_numeric) - ) - for symplectic_matrix in monte_carlo_matrix - ) / sample_size + mc_integral = monte_carlo_symplectic_dict[(seq_i, seq_j, half_dimension)] assert ( - abs((integral-monte_carlo_integral.real)/integral) < epsilon_real - and abs(monte_carlo_integral.imag) < epsilon_imag + abs((integral - mc_integral.real) / integral) < epsilon_real + and abs(mc_integral.imag) < epsilon_imag and integral != 0 ) @@ -359,7 +300,6 @@ def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimens [ ((0,0,0,0),(0,0,0,0), 2), ((0,1,0,1),(0,0,0,0), 2), - ((0,1,0,1),(0,1,1,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), @@ -369,9 +309,8 @@ def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimens ) def test_haar_integral_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dimension): "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - sample_size = int(1e4) - epsilon_real = 1e4 - epsilon_imag = 1e6 + epsilon_real = 5e-2 + epsilon_imag = 1e-6 half_length = len(seq_i) // 2 seq_i_numeric = seq_i[:half_length] + tuple(i+half_dimension for i in seq_i[half_length:]) @@ -379,18 +318,11 @@ def test_haar_integral_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dimensi integral = float(ap.haar_integral_symplectic((seq_i_numeric, seq_j_numeric), half_dimension)) - monte_carlo_matrix = (generate_random_usp(half_dimension) for _ in range(sample_size)) - monte_carlo_integral = sum( - prod( - symplectic_matrix[i-1, j-1] - for i, j in zip(seq_i_numeric, seq_j_numeric) - ) - for symplectic_matrix in monte_carlo_matrix - ) / sample_size + mc_integral = monte_carlo_symplectic_dict[(seq_i, seq_j, half_dimension)] assert ( - abs((integral-monte_carlo_integral.real)/integral) < epsilon_real - and abs(monte_carlo_integral.imag) < epsilon_imag + abs((integral - mc_integral.real) / integral) < epsilon_real + and abs(mc_integral.imag) < epsilon_imag and integral != 0 ) diff --git a/requirements_dev.txt b/requirements_dev.txt index 0a564bf..3566307 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1 @@ -pytest==8.3.2 -numpy==1.25.0 -scipy==1.13.1 \ No newline at end of file +pytest==8.3.2 \ No newline at end of file From fbb00c3d624569e94104b6355317ad92e534fc30 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Fri, 5 Dec 2025 15:12:15 +0900 Subject: [PATCH 17/24] symplectic moments merged from PR #43 --- haarpy/tests/test_symplectic.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index c457f0d..42b2308 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -27,14 +27,15 @@ d = Symbol('d') monte_carlo_symplectic_dict = { - ((0, 0, 0, 0), (0, 0, 0, 0), 2) : (0.10033684843833776-1.145379354559459e-20j), - ((0, 1, 0, 1), (0, 0, 0, 0), 2) : (-0.04990811793582537-8.943783307107027e-21j), + ((0, 0, 0, 0), (0, 0, 0, 0), 2) : (0.09948889355794269+6.851639354662377e-21j), + ((0, 1, 0, 1), (0, 0, 0, 0), 2) : (0.05025720143792921+3.8747831289542884e-21j), ((0, 1, 0, 1), (0, 1, 1, 0), 2) : (-0.02505580071352494-9.625388767948332e-07j), - ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3) : (0.017633018076551557-1.0423137720400771e-20j), - ((0, 1, 2, 0, 1, 2), (0, 0, 0, 0, 0, 0), 3) : (0.0029691649199574807-4.311344798154337e-22j), - ((0, 0, 1, 0, 0, 1), (0, 2, 2, 0, 2, 2), 3) : (-0.0037265932000567895-4.301613654142747e-23j), - ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0), 4) : (0.0030119499237324467-3.579791351992152e-21j), - ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4) : (0.0005034436860443855-3.83813858200423e-22j), + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3) : (0.018000029928256993+2.5207244033791384e-21j), + ((0, 1, 2, 0, 1, 2), (0, 0, 0, 0, 0, 0), 3) : (0.0029514164273132704-1.8345674171674893e-21j), + ((0, 0, 1, 0, 0, 1), (0, 2, 2, 0, 2, 2), 3) : (0.0037630369365813177+9.729122199612046e-23j), + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0), 4) : (0.0029909334194272684-2.5931001156985104e-21j), + ((0, 1, 2, 3, 0, 1, 2, 3), (0, 0, 0, 0, 0, 0, 0, 0), 4) : (0.0001260733931029849-4.471619720427258e-23j), + ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4) : (0.000502809515889242-4.1519024921401192e-22j), } @@ -271,6 +272,7 @@ def test_weingarten_symplectic_degree_value_error(permutation): ((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), ] ) @@ -304,6 +306,7 @@ def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimens ((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), ] ) From b914b366add91293ad8c5cf480f047592ce886d2 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Fri, 5 Dec 2025 18:06:53 +0900 Subject: [PATCH 18/24] symplectic moments merged from PR #43 --- haarpy/circular_ensembles.py | 1 + haarpy/tests/test_circular_ensembles.py | 82 +++++++++++-------------- haarpy/tests/test_symplectic.py | 4 +- 3 files changed, 38 insertions(+), 49 deletions(-) diff --git a/haarpy/circular_ensembles.py b/haarpy/circular_ensembles.py index 9dbb967..08f17af 100644 --- a/haarpy/circular_ensembles.py +++ b/haarpy/circular_ensembles.py @@ -180,6 +180,7 @@ def haar_integral_circular_symplectic( 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 diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index 48ebd24..51a4c19 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -18,7 +18,6 @@ from math import prod from random import randint from fractions import Fraction -import numpy as np from sympy import Symbol, simplify, factorial, factorial2 from sympy.combinatorics import SymmetricGroup from scipy.stats import unitary_group @@ -27,6 +26,20 @@ d = Symbol('d') +monte_carlo_cse_dict = { + ((0, 0), (0, 0), 1) : (1+0j), + ((0, 1), (0, 1), 1) : (6.202449849650142e-34+0j), + ((0, 1, 2, 3), (0, 1, 2, 3), 2) : (0.16670734637771975+0j), + ((0, 0, 0, 0), (0, 0, 0, 0), 2) : (0.16594246254246486+0j), + ((0, 3, 0, 3), (0, 3, 0, 3), 2) : (0.16705053183343524+0j), + ((0, 0, 0, 3), (0, 3, 0, 0), 2) : (0.08309679067762976+0j), + ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3) : (0.028659601947427615+0j), + ((5, 5, 5, 5, 5, 5), (5, 5, 5, 5, 5, 5), 3) : (0.028909295959926393+0j), + ((2, 2, 5, 5, 5, 5), (2, 2, 5, 5, 5, 5), 3) : (0.028304583571937526+0j), + ((2, 2, 5, 5, 5, 5), (2, 5, 5, 2, 5, 5), 3) : (-2.6504451440099176e-37-7.700460878474545e-38j), + ((2, 2, 5, 5, 3, 3), (2, 2, 5, 5, 3, 3), 3) : (0.015927428707892998+0j), + ((7, 7, 0, 0, 0, 0, 0, 0), (7, 0, 0, 7, 0, 0, 0, 0), 4) : (-0.00023053596111631466-2.3274900374779226e-06j), +} @pytest.mark.parametrize("half_degree", range(1,3)) def test_weingarten_circular_orthogonal_hyperoctahedral_symbolic(half_degree): @@ -202,6 +215,9 @@ def test_haar_integral_coe_value_error(sequences): @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), @@ -215,44 +231,31 @@ def test_haar_integral_coe_value_error(sequences): ) 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" - sample_size = int(1e4) - epsilon_real = 1e4 - epsilon = 1e6 + epsilon_real = 5e-2 + epsilon = 1e-6 integral = float(ap.haar_integral_circular_symplectic((seq_i, seq_j), half_dim)) - unitary_gen = (unitary_group.rvs(2*half_dim) for _ in range(sample_size)) - - I = np.eye(half_dim, dtype=np.complex128) - Z = np.zeros((half_dim, half_dim), dtype=np.complex128) - J = np.block([[Z, I],[-I, Z]]) - - cse_gen = (J @ U.T @ J.T @ U for U in unitary_gen) - - monte_carlo_integral = sum( - prod( - cse[i-1, j-1] - for i, j in zip(seq_i, seq_j) - ) - for cse in cse_gen - ) / sample_size + mc_integral = monte_carlo_cse_dict[(seq_i, seq_j, half_dim)] if integral: assert ( - abs((integral-monte_carlo_integral.real)/integral) < epsilon_real - and abs(monte_carlo_integral.imag) < epsilon - and integral != 0 + abs((integral-mc_integral.real)/integral) < epsilon_real + and abs(mc_integral.imag) < epsilon ) else: assert ( - abs(monte_carlo_integral.real) < epsilon - and abs(monte_carlo_integral.imag) < epsilon + abs(mc_integral.real) < epsilon + and abs(mc_integral.imag) < epsilon ) @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), @@ -266,9 +269,8 @@ def test_haar_integral_circular_symplectic_monte_carlo_numeric(seq_i, seq_j, hal ) 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" - sample_size = int(1e4) - epsilon_real = 1e4 - epsilon = 1e6 + epsilon_real = 5e-2 + epsilon = 1e-6 integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), d) integral = float(integral.subs(d, half_dim)) @@ -276,32 +278,17 @@ def test_haar_integral_circular_symplectic_monte_carlo_symbolic(seq_i, seq_j, ha 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) - unitary_gen = (unitary_group.rvs(2*half_dim) for _ in range(sample_size)) - - I = np.eye(half_dim, dtype=np.complex128) - Z = np.zeros((half_dim, half_dim), dtype=np.complex128) - J = np.block([[Z, I],[-I, Z]]) - - cse_gen = (J @ U.T @ J.T @ U for U in unitary_gen) - - monte_carlo_integral = sum( - prod( - cse[i-1, j-1] - for i, j in zip(seq_i, seq_j) - ) - for cse in cse_gen - ) / sample_size + mc_integral = monte_carlo_cse_dict[((seq_i, seq_j, half_dim))] if integral: assert ( - abs((integral-monte_carlo_integral.real)/integral) < epsilon_real - and abs(monte_carlo_integral.imag) < epsilon - and integral != 0 + abs((integral-mc_integral.real)/integral) < epsilon_real + and abs(mc_integral.imag) < epsilon ) else: assert ( - abs(monte_carlo_integral.real) < epsilon - and abs(monte_carlo_integral.imag) < epsilon + abs(mc_integral.real) < epsilon + and abs(mc_integral.imag) < epsilon ) @@ -362,6 +349,7 @@ def test_haar_integral_circular_symplectic_value_error_outside_dimension_range(s ((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'))), diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index 42b2308..95b7224 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -278,7 +278,7 @@ def test_weingarten_symplectic_degree_value_error(permutation): ) def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimension): "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - epsilon_real = 5e-2 + epsilon_real = 2e-2 epsilon_imag = 1e-6 half_length = len(seq_i) // 2 @@ -312,7 +312,7 @@ def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimens ) def test_haar_integral_symplectic_monte_carlo_numeric(seq_i, seq_j, half_dimension): "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - epsilon_real = 5e-2 + epsilon_real = 2e-2 epsilon_imag = 1e-6 half_length = len(seq_i) // 2 From 8e70def43d3a3347bf272691faa27515f2a6bafc Mon Sep 17 00:00:00 2001 From: yaniccd Date: Fri, 5 Dec 2025 18:10:08 +0900 Subject: [PATCH 19/24] symplectic moments merged from PR #43 --- haarpy/tests/test_circular_ensembles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index 51a4c19..4ed751d 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -20,7 +20,6 @@ from fractions import Fraction from sympy import Symbol, simplify, factorial, factorial2 from sympy.combinatorics import SymmetricGroup -from scipy.stats import unitary_group import pytest import haarpy as ap From e478c7d14440397cb1bfa19e989321a6663f81b2 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Fri, 5 Dec 2025 19:00:49 +0900 Subject: [PATCH 20/24] symplectic moments merged from PR #43 --- haarpy/tests/test_circular_ensembles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index 4ed751d..dafa591 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -231,7 +231,7 @@ def test_haar_integral_coe_value_error(sequences): 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" epsilon_real = 5e-2 - epsilon = 1e-6 + epsilon = 1e-5 integral = float(ap.haar_integral_circular_symplectic((seq_i, seq_j), half_dim)) @@ -269,7 +269,7 @@ def test_haar_integral_circular_symplectic_monte_carlo_numeric(seq_i, seq_j, hal 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" epsilon_real = 5e-2 - epsilon = 1e-6 + epsilon = 1e-5 integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), d) integral = float(integral.subs(d, half_dim)) From 96dbe1544ef682aadf91024b099f70980b099b77 Mon Sep 17 00:00:00 2001 From: Nicolas Quesada Date: Sat, 6 Dec 2025 22:50:13 -0500 Subject: [PATCH 21/24] cleaning test_symplectic --- haarpy/tests/test_symplectic.py | 64 +++++++-------------------------- 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index 95b7224..f997478 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -27,15 +27,14 @@ d = Symbol('d') monte_carlo_symplectic_dict = { - ((0, 0, 0, 0), (0, 0, 0, 0), 2) : (0.09948889355794269+6.851639354662377e-21j), - ((0, 1, 0, 1), (0, 0, 0, 0), 2) : (0.05025720143792921+3.8747831289542884e-21j), - ((0, 1, 0, 1), (0, 1, 1, 0), 2) : (-0.02505580071352494-9.625388767948332e-07j), - ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3) : (0.018000029928256993+2.5207244033791384e-21j), - ((0, 1, 2, 0, 1, 2), (0, 0, 0, 0, 0, 0), 3) : (0.0029514164273132704-1.8345674171674893e-21j), - ((0, 0, 1, 0, 0, 1), (0, 2, 2, 0, 2, 2), 3) : (0.0037630369365813177+9.729122199612046e-23j), - ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0), 4) : (0.0029909334194272684-2.5931001156985104e-21j), - ((0, 1, 2, 3, 0, 1, 2, 3), (0, 0, 0, 0, 0, 0, 0, 0), 4) : (0.0001260733931029849-4.471619720427258e-23j), - ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4) : (0.000502809515889242-4.1519024921401192e-22j), + ((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, } @@ -276,58 +275,19 @@ def test_weingarten_symplectic_degree_value_error(permutation): ((0,0,0,0,0,0,0,0),(0,0,1,1,0,0,1,1), 4), ] ) -def test_haar_integral_symplectic_monte_carlo_symbolic(seq_i, seq_j, half_dimension): +def test_haar_integral_symplectic_symbolic(seq_i, seq_j, half_dimension): "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - epsilon_real = 2e-2 - epsilon_imag = 1e-6 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:]) - integral = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) - integral = float(integral.subs(d, half_dimension)) + integral_frac = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) + integral_num = integral_frac.subs(d, half_dimension) mc_integral = monte_carlo_symplectic_dict[(seq_i, seq_j, half_dimension)] - assert ( - abs((integral - mc_integral.real) / integral) < epsilon_real - and abs(mc_integral.imag) < epsilon_imag - and integral != 0 - ) - - -@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_monte_carlo_numeric(seq_i, seq_j, half_dimension): - "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - epsilon_real = 2e-2 - epsilon_imag = 1e-6 - - half_length = len(seq_i) // 2 - 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 = float(ap.haar_integral_symplectic((seq_i_numeric, seq_j_numeric), half_dimension)) - - mc_integral = monte_carlo_symplectic_dict[(seq_i, seq_j, half_dimension)] - - assert ( - abs((integral - mc_integral.real) / integral) < epsilon_real - and abs(mc_integral.imag) < epsilon_imag - and integral != 0 - ) + assert mc_integral == float(integral_num) @pytest.mark.parametrize( From 73fd23fef58474f1aa1bb0148ba0fee0503bb8ae Mon Sep 17 00:00:00 2001 From: Nicolas Quesada Date: Sat, 6 Dec 2025 23:18:13 -0500 Subject: [PATCH 22/24] minor typo --- haarpy/symplectic.py | 2 +- haarpy/tests/test_circular_ensembles.py | 323 +++++++++----------- haarpy/tests/test_symplectic.py | 390 +++++++++++++----------- 3 files changed, 359 insertions(+), 356 deletions(-) diff --git a/haarpy/symplectic.py b/haarpy/symplectic.py index 5bd9ba5..33c1590 100644 --- a/haarpy/symplectic.py +++ b/haarpy/symplectic.py @@ -168,7 +168,7 @@ def haar_integral_symplectic( Expr: Integral under the Haar measure Raise: - ValueError: If sequences doesn't contain 2 tuples + 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 diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index dafa591..9a9751b 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -23,187 +23,184 @@ import pytest import haarpy as ap -d = Symbol('d') - -monte_carlo_cse_dict = { - ((0, 0), (0, 0), 1) : (1+0j), - ((0, 1), (0, 1), 1) : (6.202449849650142e-34+0j), - ((0, 1, 2, 3), (0, 1, 2, 3), 2) : (0.16670734637771975+0j), - ((0, 0, 0, 0), (0, 0, 0, 0), 2) : (0.16594246254246486+0j), - ((0, 3, 0, 3), (0, 3, 0, 3), 2) : (0.16705053183343524+0j), - ((0, 0, 0, 3), (0, 3, 0, 0), 2) : (0.08309679067762976+0j), - ((0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0), 3) : (0.028659601947427615+0j), - ((5, 5, 5, 5, 5, 5), (5, 5, 5, 5, 5, 5), 3) : (0.028909295959926393+0j), - ((2, 2, 5, 5, 5, 5), (2, 2, 5, 5, 5, 5), 3) : (0.028304583571937526+0j), - ((2, 2, 5, 5, 5, 5), (2, 5, 5, 2, 5, 5), 3) : (-2.6504451440099176e-37-7.700460878474545e-38j), - ((2, 2, 5, 5, 3, 3), (2, 2, 5, 5, 3, 3), 3) : (0.015927428707892998+0j), - ((7, 7, 0, 0, 0, 0, 0, 0), (7, 0, 0, 7, 0, 0, 0, 0), 4) : (-0.00023053596111631466-2.3274900374779226e-06j), +d = Symbol("d") + +# The values in the dictionary below were verified against Montecarlo 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" @@ -212,59 +209,48 @@ def test_haar_integral_coe_value_error(sequences): @pytest.mark.parametrize( - 'seq_i, seq_j, half_dim', + "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), - ] + ((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" - epsilon_real = 5e-2 - epsilon = 1e-5 - integral = float(ap.haar_integral_circular_symplectic((seq_i, seq_j), half_dim)) + integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), half_dim) - mc_integral = monte_carlo_cse_dict[(seq_i, seq_j, half_dim)] + mc_integral = cse_dict[(seq_i, seq_j, half_dim)] - if integral: - assert ( - abs((integral-mc_integral.real)/integral) < epsilon_real - and abs(mc_integral.imag) < epsilon - ) - else: - assert ( - abs(mc_integral.real) < epsilon - and abs(mc_integral.imag) < epsilon - ) + assert float(integral) == mc_integral @pytest.mark.parametrize( - 'seq_i, seq_j, half_dim', + "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), - ] + ((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" @@ -277,48 +263,37 @@ def test_haar_integral_circular_symplectic_monte_carlo_symbolic(seq_i, seq_j, ha 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 = monte_carlo_cse_dict[((seq_i, seq_j, half_dim))] + mc_integral = cse_dict[((seq_i, seq_j, half_dim))] + + assert float(integral) == mc_integral - if integral: - assert ( - abs((integral-mc_integral.real)/integral) < epsilon_real - and abs(mc_integral.imag) < epsilon - ) - else: - assert ( - abs(mc_integral.real) < epsilon - and abs(mc_integral.imag) < epsilon - ) @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)), - ] + ((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" - ): + 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)), - ] + (("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) + dimension = randint(1, 99) with pytest.raises(TypeError): ap.haar_integral_circular_symplectic(sequences, dimension) @@ -326,12 +301,14 @@ def test_haar_integral_circular_symplectic_type_error_integer_dimension(sequence @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), - ] + (((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): +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, @@ -343,16 +320,16 @@ def test_haar_integral_circular_symplectic_value_error_outside_dimension_range(s @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'))), - ] + ((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" @@ -363,25 +340,25 @@ def test_haar_integral_circular_symplectic_type_error_wrong_format(sequences): @pytest.mark.parametrize( "dimension", [ - 'a', - [1,2], - {1,2}, + "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) + 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), - ] + (((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" diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index f997478..5edf82e 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -24,32 +24,36 @@ import pytest import haarpy as ap -d = Symbol('d') - -monte_carlo_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, +d = Symbol("d") + + +# The values in the dictionary below were verified against Montecarlo 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 @@ -57,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) @@ -75,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): @@ -99,11 +101,7 @@ def test_twisted_spherical_orthogonality_transversal_zero(permutation, partition `_ """ degree = permutation.size - convolution = sum( - ap.twisted_spherical_function(tau, partition1) - * ap.twisted_spherical_function(permutation * ~tau, partition2) - for tau in ap.hyperoctahedral_transversal(degree) - ) + convolution = sum(ap.twisted_spherical_function(tau, partition1) * ap.twisted_spherical_function(permutation * ~tau, partition2) for tau in ap.hyperoctahedral_transversal(degree)) assert not convolution @@ -111,48 +109,77 @@ def test_twisted_spherical_orthogonality_transversal_zero(permutation, partition @pytest.mark.parametrize( "permutation, partition", [ - (Permutation(3,), (2,)), - (Permutation(3,), (1,1)), - (Permutation(5,)(0,1), (2,1)), - (Permutation(0,1,2,3,4,5), (3,)), - (Permutation(5,)(0,3,4), (3,)), - (Permutation(0,1,2,3,4,5), (1,1,1)), - (Permutation(0,1,2,3,4,5), (2,1)), - (Permutation(0,3,5), (2,1)), - (Permutation(0,3,4,5), (2,1)), - (Permutation(0,2,3,4,5), (2,1)), + ( + Permutation( + 3, + ), + (2,), + ), + ( + Permutation( + 3, + ), + (1, 1), + ), + ( + Permutation( + 5, + )(0, 1), + (2, 1), + ), + (Permutation(0, 1, 2, 3, 4, 5), (3,)), + ( + Permutation( + 5, + )(0, 3, 4), + (3,), + ), + (Permutation(0, 1, 2, 3, 4, 5), (1, 1, 1)), + (Permutation(0, 1, 2, 3, 4, 5), (2, 1)), + (Permutation(0, 3, 5), (2, 1)), + (Permutation(0, 3, 4, 5), (2, 1)), + (Permutation(0, 2, 3, 4, 5), (2, 1)), ], ) 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 half_degree = degree // 2 - convolution = sum( - ap.twisted_spherical_function(tau, partition) - * ap.twisted_spherical_function(permutation * ~tau, partition) - for tau in ap.hyperoctahedral_transversal(degree) - ) + convolution = sum(ap.twisted_spherical_function(tau, partition) * ap.twisted_spherical_function(permutation * ~tau, partition) for tau in ap.hyperoctahedral_transversal(degree)) duplicate_partition = tuple(part for part in partition for _ in range(2)) - orthogonality = ( - Fraction(factorial(degree), 2**half_degree*factorial(half_degree)) - * ap.twisted_spherical_function(permutation, partition) - / ap.irrep_dimension(duplicate_partition) - ) + orthogonality = 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( "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)), + ( + 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): @@ -164,10 +191,30 @@ def test_twisted_spherical_function_type_error(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)), + ( + 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): @@ -176,46 +223,36 @@ def test_twisted_spherical_function_degree_value_error(permutation, partition): ap.twisted_spherical_function(permutation, partition) -@pytest.mark.parametrize("half_degree", range(1,3)) +@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)) - assert ap.weingarten_symplectic(permutation, d) == ( - simplify((2*d-1)*coefficient) if permutation in hyperoctahedral - else coefficient - ) + 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 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(): - assert ap.weingarten_symplectic(permutation, 7) == ( - Fraction(permutation.signature(),(2*7)) - ) + for permutation in SymmetricGroup(2 * half_degree).generate(): + assert ap.weingarten_symplectic(permutation, 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))) - assert ap.weingarten_symplectic(permutation, 7) == ( - (2*7-1)*coefficient if permutation in hyperoctahedral - else coefficient - ) + 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) @pytest.mark.parametrize( @@ -223,28 +260,24 @@ 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) - * permutation.signature() - * ap.weingarten_orthogonal(permutation, -2*d) - ) + assert ap.weingarten_symplectic(permutation, d) == simplify((-1) ** (permutation.size // 2) * permutation.signature() * ap.weingarten_orthogonal(permutation, -2 * d)) @pytest.mark.parametrize( @@ -252,72 +285,68 @@ def test_weingarten_symplectic_orthogonal_relation(permutation): [ (Permutation(2)), (Permutation(4)), - (Permutation(0,1,2,3,4)), + (Permutation(0, 1, 2, 3, 4)), (Permutation(6)), - ] + ], ) 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"): + 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', + "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), - ] + ((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(seq_i, seq_j, half_dimension): "Test haar integral symplectic moments against Monte Carlo simulation symbolic" 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_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:]) integral_frac = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) integral_num = integral_frac.subs(d, half_dimension) - mc_integral = monte_carlo_symplectic_dict[(seq_i, seq_j, half_dimension)] + mc_integral = symplectic_dict[(seq_i, seq_j, half_dimension)] assert mc_integral == float(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)), - ] + ((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" - ): + 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)), - ] + (("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) + dimension = randint(1, 99) with pytest.raises(TypeError): ap.haar_integral_symplectic(sequences, dimension) @@ -325,10 +354,10 @@ def test_haar_integral_symplectic_type_error_integer_dimension(sequences): @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), - ] + (((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_symplectic_value_error_outside_dimension_range(sequences, dimension): "Value error for sequences values outside dimension range" @@ -342,15 +371,15 @@ def test_haar_integral_symplectic_value_error_outside_dimension_range(sequences, @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,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'))), - ] + ((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_haar_integral_symplectic_type_error_wrong_format(sequences): "Type error for symbolic dimension with wrong sequence format" @@ -361,49 +390,46 @@ def test_haar_integral_symplectic_type_error_wrong_format(sequences): @pytest.mark.parametrize( "dimension", [ - 'a', - [1,2], - {1,2}, + "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) + 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), - ] + (((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)) +@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) + 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): + 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) - ) + assert ap.haar_integral_symplectic((sequence, perm_sequence), d) == ap.weingarten_symplectic(perm, d) From f59c3ede8d69687fbf1ad1ce95030682a5f203ff Mon Sep 17 00:00:00 2001 From: yaniccd Date: Mon, 8 Dec 2025 15:02:53 +0900 Subject: [PATCH 23/24] symplectic tests merged from PR #44 --- haarpy/tests/test_circular_ensembles.py | 9 +- haarpy/tests/test_symplectic.py | 164 ++++++++++-------------- 2 files changed, 72 insertions(+), 101 deletions(-) diff --git a/haarpy/tests/test_circular_ensembles.py b/haarpy/tests/test_circular_ensembles.py index 9a9751b..5f557d5 100644 --- a/haarpy/tests/test_circular_ensembles.py +++ b/haarpy/tests/test_circular_ensembles.py @@ -25,7 +25,7 @@ d = Symbol("d") -# The values in the dictionary below were verified against Montecarlo simulations +# 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, @@ -227,7 +227,6 @@ def test_haar_integral_coe_value_error(sequences): ) 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)] @@ -254,11 +253,7 @@ def test_haar_integral_circular_symplectic_monte_carlo_numeric(seq_i, seq_j, hal ) 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" - epsilon_real = 5e-2 - epsilon = 1e-5 - - integral = ap.haar_integral_circular_symplectic((seq_i, seq_j), d) - integral = float(integral.subs(d, half_dim)) + 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) diff --git a/haarpy/tests/test_symplectic.py b/haarpy/tests/test_symplectic.py index 5edf82e..a55fde1 100644 --- a/haarpy/tests/test_symplectic.py +++ b/haarpy/tests/test_symplectic.py @@ -27,7 +27,7 @@ d = Symbol("d") -# The values in the dictionary below were verified against Montecarlo simulations +# 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 = { @@ -101,7 +101,11 @@ def test_twisted_spherical_orthogonality_transversal_zero(permutation, partition `_ """ degree = permutation.size - convolution = sum(ap.twisted_spherical_function(tau, partition1) * ap.twisted_spherical_function(permutation * ~tau, partition2) for tau in ap.hyperoctahedral_transversal(degree)) + convolution = sum( + ap.twisted_spherical_function(tau, partition1) + * ap.twisted_spherical_function(permutation * ~tau, partition2) + for tau in ap.hyperoctahedral_transversal(degree) + ) assert not convolution @@ -109,36 +113,16 @@ def test_twisted_spherical_orthogonality_transversal_zero(permutation, partition @pytest.mark.parametrize( "permutation, partition", [ - ( - Permutation( - 3, - ), - (2,), - ), - ( - Permutation( - 3, - ), - (1, 1), - ), - ( - Permutation( - 5, - )(0, 1), - (2, 1), - ), - (Permutation(0, 1, 2, 3, 4, 5), (3,)), - ( - Permutation( - 5, - )(0, 3, 4), - (3,), - ), - (Permutation(0, 1, 2, 3, 4, 5), (1, 1, 1)), - (Permutation(0, 1, 2, 3, 4, 5), (2, 1)), - (Permutation(0, 3, 5), (2, 1)), - (Permutation(0, 3, 4, 5), (2, 1)), - (Permutation(0, 2, 3, 4, 5), (2, 1)), + (Permutation(3,), (2,)), + (Permutation(3,), (1,1)), + (Permutation(5,)(0,1), (2,1)), + (Permutation(0,1,2,3,4,5), (3,)), + (Permutation(5,)(0,3,4), (3,)), + (Permutation(0,1,2,3,4,5), (1,1,1)), + (Permutation(0,1,2,3,4,5), (2,1)), + (Permutation(0,3,5), (2,1)), + (Permutation(0,3,4,5), (2,1)), + (Permutation(0,2,3,4,5), (2,1)), ], ) def test_twisted_spherical_orthogonality_transversal_none_zero(permutation, partition): @@ -148,38 +132,29 @@ def test_twisted_spherical_orthogonality_transversal_none_zero(permutation, part """ degree = permutation.size half_degree = degree // 2 - convolution = sum(ap.twisted_spherical_function(tau, partition) * ap.twisted_spherical_function(permutation * ~tau, partition) for tau in ap.hyperoctahedral_transversal(degree)) + convolution = sum( + ap.twisted_spherical_function(tau, partition) + * ap.twisted_spherical_function(permutation * ~tau, partition) + for tau in ap.hyperoctahedral_transversal(degree) + ) duplicate_partition = tuple(part for part in partition for _ in range(2)) - orthogonality = Fraction(factorial(degree), 2**half_degree * factorial(half_degree)) * ap.twisted_spherical_function(permutation, partition) / ap.irrep_dimension(duplicate_partition) + orthogonality = ( + 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( "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)), + (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): @@ -191,30 +166,10 @@ def test_twisted_spherical_function_type_error(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), - ), + (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): @@ -236,7 +191,11 @@ def test_weingarten_symplectic_hyperoctahedral_symbolic(half_degree): for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) 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 else coefficient) + assert ap.weingarten_symplectic(permutation, d) == ( + simplify((2 * d - 1) * coefficient) + if permutation in hyperoctahedral + else coefficient + ) @pytest.mark.parametrize("half_degree", range(1, 3)) @@ -247,12 +206,16 @@ def test_weingarten_symplectic_hyperoctahedral_numeric(half_degree): """ if half_degree == 1: for permutation in SymmetricGroup(2 * half_degree).generate(): - assert ap.weingarten_symplectic(permutation, 7) == (Fraction(permutation.signature(), (2 * 7))) + assert ap.weingarten_symplectic(permutation, 7) == ( + Fraction(permutation.signature(), (2 * 7)) + ) else: for permutation in SymmetricGroup(2 * half_degree).generate(): hyperoctahedral = ap.HyperoctahedralGroup(half_degree) 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) + assert ap.weingarten_symplectic(permutation, 7) == ( + (2 * 7 - 1) * coefficient if permutation in hyperoctahedral else coefficient + ) @pytest.mark.parametrize( @@ -277,7 +240,11 @@ def test_weingarten_symplectic_orthogonal_relation(permutation): `Matsumoto. Weingarten calculus for matrix ensembles associated with compact symmetric spaces: `_ """ - assert ap.weingarten_symplectic(permutation, d) == simplify((-1) ** (permutation.size // 2) * permutation.signature() * ap.weingarten_orthogonal(permutation, -2 * d)) + assert ap.weingarten_symplectic(permutation, d) == simplify( + (-1) ** (permutation.size // 2) + * permutation.signature() + * ap.weingarten_orthogonal(permutation, -2 * d) + ) @pytest.mark.parametrize( @@ -308,19 +275,26 @@ def test_weingarten_symplectic_degree_value_error(permutation): ((0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 1, 1, 0, 0, 1, 1), 4), ], ) -def test_haar_integral_symplectic_symbolic(seq_i, seq_j, half_dimension): - "Test haar integral symplectic moments against Monte Carlo simulation symbolic" - +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:]) - - integral_frac = ap.haar_integral_symplectic((seq_i_symbolic, seq_j_symbolic), d) - integral_num = integral_frac.subs(d, half_dimension) - + 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 == float(integral_num) + assert mc_integral == integral_symb == integral_num + @pytest.mark.parametrize( "sequences", @@ -432,4 +406,6 @@ def test_haar_integral_symplectic_weingarten_reconciliation(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) + assert ap.haar_integral_symplectic( + (sequence, perm_sequence), d + ) == ap.weingarten_symplectic(perm, d) From 8ef9e69925d0838e9581832df716f7ca71ad9c83 Mon Sep 17 00:00:00 2001 From: yaniccd Date: Mon, 8 Dec 2025 15:56:48 +0900 Subject: [PATCH 24/24] symplectic moments merged from PR #43 --- .github/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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