From d319e029d246743d982d41caf1362392eb47b169 Mon Sep 17 00:00:00 2001 From: Gausshj Date: Thu, 20 Nov 2025 20:35:30 +0800 Subject: [PATCH] dev(conjugate): add support for conjugation - Add support for conjugation - Add some test cases for conjugation --- grassmann_tensor/tensor.py | 30 +++++++++++ tests/conjugate_test.py | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 tests/conjugate_test.py diff --git a/grassmann_tensor/tensor.py b/grassmann_tensor/tensor.py index 7b9dbb7..7e68fc8 100644 --- a/grassmann_tensor/tensor.py +++ b/grassmann_tensor/tensor.py @@ -970,6 +970,36 @@ def identity(self, pairs: tuple[tuple[int, ...], tuple[int, ...]]) -> GrassmannT return tensor_identity + def conjugate(self) -> GrassmannTensor: + tensor_conj = self.tensor.conj() + + dim = self.tensor.dim() + parity = self.parity + + total_parity = functools.reduce( + torch.logical_xor, + ( + torch.logical_and( + self._unsqueeze(parity[i], i, dim), + self._unsqueeze(parity[j], j, dim), + ) + for j in range(dim) + for i in range(0, j) + ), + torch.zeros([], dtype=torch.bool, device=self.tensor.device), + ) + + tensor_conj = torch.where(total_parity, -tensor_conj, tensor_conj) + + return dataclasses.replace( + self, + _arrow=tuple(not arrow for arrow in self.arrow), + _tensor=tensor_conj, + ) + + def conj(self) -> GrassmannTensor: + return self.conjugate() + def __post_init__(self) -> None: assert len(self._arrow) == self._tensor.dim(), ( f"Arrow length ({len(self._arrow)}) must match tensor dimensions ({self._tensor.dim()})." diff --git a/tests/conjugate_test.py b/tests/conjugate_test.py new file mode 100644 index 0000000..c0263c7 --- /dev/null +++ b/tests/conjugate_test.py @@ -0,0 +1,106 @@ +import torch +import pytest +from typing import TypeAlias + +from grassmann_tensor import GrassmannTensor + +Arrow_Edges: TypeAlias = tuple[tuple[bool, ...], tuple[tuple[int, int], ...]] + + +@pytest.fixture( + params=[ + ( + (True, False), + ((2, 2), (2, 2)), + ), + ( + (True, False, False), + ((2, 2), (2, 2), (2, 2)), + ), + ] +) +def arrow_edges(request: pytest.FixtureRequest) -> Arrow_Edges: + return request.param + + +@pytest.fixture( + params=[ + ( + (True, False), + ((4, 0), (4, 0)), + ), + ( + (True, False, False), + ((4, 0), (4, 0), (4, 0)), + ), + ] +) +def arrow_edges_without_symmetry(request: pytest.FixtureRequest) -> Arrow_Edges: + return request.param + + +def create_random_tensor( + arrow_edges: Arrow_Edges, + *, + dtype: torch.dtype, +) -> GrassmannTensor: + arrow, edges = arrow_edges + shape = [even + odd for even, odd in edges] + + if dtype == torch.float64: + tensor = torch.rand(*shape, dtype=dtype) + else: + tensor = torch.randn(*shape) + 1j * torch.randn(*shape).to(dtype) + return GrassmannTensor(arrow, edges, tensor) + + +@pytest.fixture +def random_real_tensor(arrow_edges: Arrow_Edges) -> GrassmannTensor: + return create_random_tensor(arrow_edges, dtype=torch.float64) + + +@pytest.fixture +def random_complex_tensor(arrow_edges: Arrow_Edges) -> GrassmannTensor: + return create_random_tensor(arrow_edges, dtype=torch.complex128) + + +def test_conjugate_involution_with_complex_tensor(random_complex_tensor: GrassmannTensor) -> None: + contrast_a = random_complex_tensor + contrast_b = contrast_a.conj().conj() + assert contrast_a.arrow == contrast_b.arrow + assert torch.allclose(contrast_a.tensor, contrast_b.tensor) + + +def test_conjugate_involution_with_real_tensor(random_real_tensor: GrassmannTensor) -> None: + contrast_a = random_real_tensor + contrast_b = contrast_a.conj().conj() + assert contrast_a.arrow == contrast_b.arrow + assert torch.allclose(contrast_a.tensor, contrast_b.tensor) + + +def test_conjugate_reverse_order_of_contraction( + arrow_edges: Arrow_Edges, +) -> None: + a = create_random_tensor(arrow_edges, dtype=torch.complex128).update_mask() + b = create_random_tensor(arrow_edges, dtype=torch.complex128).update_mask() + + contrast_a = GrassmannTensor.contract(a, b, a.tensor.dim() - 1, 0) + contrast_a = contrast_a.conj() + + a_conj = a.conj() + b_conj = b.conj() + + contrast_b = GrassmannTensor.contract(a_conj, b_conj, a_conj.tensor.dim() - 1, 0) + + assert contrast_a.arrow == contrast_b.arrow + assert torch.allclose(contrast_a.tensor, contrast_b.tensor) + + +def test_conjugate_without_symmetry_equality( + arrow_edges_without_symmetry: Arrow_Edges, +) -> None: + a = create_random_tensor(arrow_edges_without_symmetry, dtype=torch.float64) + b = create_random_tensor(arrow_edges_without_symmetry, dtype=torch.complex128) + + assert torch.allclose(a.tensor, a.conj().tensor) + assert torch.allclose(b.tensor.conj(), b.conj().tensor)