From 2013bfc371c35586e0a43dd1a5f443f33a4bd429 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Sun, 31 Aug 2025 10:51:22 +0800 Subject: [PATCH 1/8] Add support for more case in reshape. --- grassmann_tensor/tensor.py | 162 ++++++++++++++++++++++++------------- 1 file changed, 104 insertions(+), 58 deletions(-) diff --git a/grassmann_tensor/tensor.py b/grassmann_tensor/tensor.py index 697bb7e..d4497b0 100644 --- a/grassmann_tensor/tensor.py +++ b/grassmann_tensor/tensor.py @@ -176,8 +176,8 @@ def _reorder_indices(self, edges: tuple[tuple[int, int], ...]) -> tuple[int, int torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) flatten_parity = parity.flatten() - even = (~flatten_parity).nonzero().squeeze() - odd = flatten_parity.nonzero().squeeze() + even = (~flatten_parity).nonzero().squeeze(-1) + odd = flatten_parity.nonzero().squeeze(-1) reorder = torch.cat([even, odd], dim=0) total = functools.reduce( @@ -212,7 +212,7 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens # 5. Apply the sign for merging # 6. Reorder the indices for merging - # pylint: disable=too-many-branches, too-many-locals, too-many-statements + # pylint: disable=too-many-branches, too-many-locals, too-many-statements, too-many-nested-blocks arrow: list[bool] = [] edges: list[tuple[int, int]] = [] @@ -225,7 +225,7 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens cursor_plan: int = 0 cursor_self: int = 0 - while True: + while cursor_plan != len(new_shape) or cursor_self != self.tensor.dim(): if new_shape[cursor_plan] == -1: # Does not change arrow.append(self.arrow[cursor_self]) @@ -233,63 +233,109 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens shape.append(self.tensor.shape[cursor_self]) cursor_self += 1 cursor_plan += 1 - else: - cursor_new_shape = new_shape[cursor_plan] - total = cursor_new_shape if isinstance(cursor_new_shape, int) else cursor_new_shape[0] + cursor_new_shape[1] - if total >= self.tensor.shape[cursor_self]: - # Merging - new_cursor_self = cursor_self - self_total = 1 - while True: - self_total *= self.tensor.shape[new_cursor_self] - new_cursor_self += 1 - if self_total == total: - break - assert self_total < total, f"Dimension mismatch with edges {self.edges} and new shape {new_shape}." - assert new_cursor_self < self.tensor.dim(), f"New shape {new_shape} exceeds tensor dimensions {self.tensor.dim()}." - even, odd, reorder, sign = self._reorder_indices(self.edges[cursor_self:new_cursor_self]) - if isinstance(cursor_new_shape, tuple): - assert (even, odd) == cursor_new_shape, f"New even and odd number mismatch during merging {self.edges} to {new_shape}." + continue + if new_shape[cursor_plan] == (1, 0): + # An trivial plan edge + arrow.append(False) + edges.append((1, 0)) + shape.append(1) + cursor_plan += 1 + continue + if self.edges[cursor_self] == (1, 0): + # An trivial self edge + cursor_self += 1 + continue + cursor_new_shape = new_shape[cursor_plan] + total = cursor_new_shape if isinstance(cursor_new_shape, int) else cursor_new_shape[0] + cursor_new_shape[1] + # one of total and shape[cursor_self] is not trivial, otherwise it should be handled before + if total == self.tensor.shape[cursor_self]: + # We do not know whether it is merging or splitting, check more + if isinstance(cursor_new_shape, int) or cursor_new_shape == self.edges[cursor_self]: + # If the new shape is exactly the same as the current edge, we treat it as no change arrow.append(self.arrow[cursor_self]) - assert all( - self_arrow == arrow[-1] for self_arrow in self.arrow[cursor_self:new_cursor_self]), f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." - edges.append((even, odd)) - shape.append(total) - if cursor_self + 1 != new_cursor_self: - # Really something merged - merging_sign.append((cursor_plan, sign)) - merging_reorder.append((cursor_plan, reorder)) - cursor_self = new_cursor_self + edges.append(self.edges[cursor_self]) + shape.append(self.tensor.shape[cursor_self]) + cursor_self += 1 cursor_plan += 1 - else: - # Splitting - new_cursor_plan = cursor_plan - plan_total = 1 - while True: - new_cursor_new_shape = new_shape[new_cursor_plan] - assert isinstance(new_cursor_new_shape, tuple), f"New shape must be a pair when splitting, got {new_cursor_new_shape}." - plan_total *= new_cursor_new_shape[0] + new_cursor_new_shape[1] - new_cursor_plan += 1 - if plan_total == self.tensor.shape[cursor_self]: + continue + # Let's see if there are (0, 1) edges in the remaining self edges, if yes, we treat it as merging, otherwise splitting + cursor_self_finding = cursor_self + cursor_self_found = False + while True: + cursor_self_finding += 1 + if cursor_self_finding == self.tensor.dim(): + break + if self.edges[cursor_self_finding] == (1, 0): + continue + if self.edges[cursor_self_finding] == (0, 1): + cursor_self_found = True + break + break + merging = cursor_self_found + if total > self.tensor.shape[cursor_self]: + merging = True + if total < self.tensor.shape[cursor_self]: + merging = False + if merging: + # Merging between [cursor_self, new_cursor_self) and the another side contains dimension as self_total + new_cursor_self = cursor_self + self_total = 1 + while True: + # Try to include more dimension from self + self_total *= self.tensor.shape[new_cursor_self] + new_cursor_self += 1 + # One dimension included, check if we can stop + if self_total == total: + even, odd, reorder, sign = self._reorder_indices(self.edges[cursor_self:new_cursor_self]) + if isinstance(cursor_new_shape, tuple): + if (even, odd) == cursor_new_shape: + break + else: break - assert plan_total < self.tensor.shape[cursor_self], f"Dimension mismatch with edges {self.edges} and new shape {new_shape}." - assert new_cursor_plan < len(new_shape), f"New shape {new_shape} exceeds specified dimensions {len(new_shape)}." - # new_shape has been verified to be tuple[int, int] in the loop - even, odd, reorder, sign = self._reorder_indices(typing.cast(tuple[tuple[int, int], ...], new_shape[cursor_plan:new_cursor_plan])) - assert (even, odd) == self.edges[cursor_self], f"New even and odd number mismatch during splitting {self.edges[cursor_self]} to {new_shape[cursor_plan:new_cursor_plan]}." - for i in range(cursor_plan, new_cursor_plan): - # new_shape has been verified to be tuple[int, int] in the loop - new_cursor_new_shape = typing.cast(tuple[int, int], new_shape[i]) - arrow.append(self.arrow[cursor_self]) - edges.append(new_cursor_new_shape) - shape.append(new_cursor_new_shape[0] + new_cursor_new_shape[1]) - splitting_reorder.append((cursor_self, reorder)) - splitting_sign.append((cursor_self, sign)) - cursor_self += 1 - cursor_plan = new_cursor_plan - - if cursor_plan == len(new_shape) and cursor_self == self.tensor.dim(): - break + # For some reason we cannot stop here, continue to include more dimension, check something before continue + assert self_total <= total, f"Dimension mismatch in merging with edges {self.edges} and new shape {new_shape}." + assert new_cursor_self < self.tensor.dim(), f"New shape exceeds in merging with edges {self.edges} and new shape {new_shape}." + # The merging block [cursor_self, new_cursor_self) has been determined + arrow.append(self.arrow[cursor_self]) + assert all(self_arrow == arrow[-1] for self_arrow in self.arrow[cursor_self:new_cursor_self]), f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." + edges.append((even, odd)) + shape.append(total) + if cursor_self + 1 != new_cursor_self: + # Really something merged, otherwise no need to reorder or sign, which helps to avoid unnecessary operation for performance + merging_sign.append((cursor_plan, sign)) + merging_reorder.append((cursor_plan, reorder)) + cursor_self = new_cursor_self + cursor_plan += 1 + else: + # Splitting between [cursor_plan, new_cursor_plan) and the another side contains dimension as plan_total + new_cursor_plan = cursor_plan + plan_total = 1 + while True: + # Try to include more dimension from new_shape + new_cursor_new_shape = new_shape[new_cursor_plan] + assert isinstance(new_cursor_new_shape, tuple), f"New shape must be a pair when splitting, got {new_cursor_new_shape}." + plan_total *= new_cursor_new_shape[0] + new_cursor_new_shape[1] + new_cursor_plan += 1 + # One dimension included, check if we can stop + if plan_total == self.tensor.shape[cursor_self]: + # new_shape block has been verified to be always tuple[int, int] before + even, odd, reorder, sign = self._reorder_indices(typing.cast(tuple[tuple[int, int], ...], new_shape[cursor_plan:new_cursor_plan])) + if (even, odd) == self.edges[cursor_self]: + break + # For some reason we cannot stop here, continue to include more dimension, check something before continue + assert plan_total <= self.tensor.shape[cursor_self], f"Dimension mismatch in splitting with edges {self.edges} and new shape {new_shape}." + assert new_cursor_plan < len(new_shape), f"New shape exceeds in splitting with edges {self.edges} and new shape {new_shape}." + # The splitting block [cursor_plan, new_cursor_plan) has been determined + for i in range(cursor_plan, new_cursor_plan): + # new_shape block has been verified to be always tuple[int, int] in the loop + new_cursor_new_shape = typing.cast(tuple[int, int], new_shape[i]) + arrow.append(self.arrow[cursor_self]) + edges.append(new_cursor_new_shape) + shape.append(new_cursor_new_shape[0] + new_cursor_new_shape[1]) + splitting_reorder.append((cursor_self, reorder)) + splitting_sign.append((cursor_self, sign)) + cursor_self += 1 + cursor_plan = new_cursor_plan tensor = self.tensor From f9535e0856461f387bb9b41a41f059114694d0b3 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Sun, 31 Aug 2025 11:02:26 +0800 Subject: [PATCH 2/8] Add more tests for reshape. --- tests/reshape_test.py | 115 +++++++++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 24 deletions(-) diff --git a/tests/reshape_test.py b/tests/reshape_test.py index f04b273..b08123e 100644 --- a/tests/reshape_test.py +++ b/tests/reshape_test.py @@ -1,3 +1,4 @@ +import random import pytest import torch from grassmann_tensor import GrassmannTensor @@ -17,34 +18,62 @@ def test_reshape_consistency(arrow: tuple[bool, ...], plan_range: tuple[int, int assert torch.allclose(a.tensor, c.tensor) -def test_reshape_merging_dimension_mismatch_edges() -> None: +def insert_trivial_between_elements(input_list: list[tuple[int, int]], p: float) -> list[tuple[int, int]]: + result = [] + for i in input_list: + if random.random() < p: + result.append((1, 0)) + result.append(i) + return result + + +@pytest.mark.parametrize("arrow", [(i, j, k, l, m) for i in [False, True] for j in [False, True] for k in [False, True] for l in [False, True] for m in [False, True]]) +@pytest.mark.parametrize("plan_range", [(i, j) for i in range(5) for j in range(5) if j > i]) +def test_reshape_trivial_edges(arrow: tuple[bool, ...], plan_range: tuple[int, int]) -> None: + l, h = plan_range + if not all(arrow[l:h]) and any(arrow[l:h]): + pytest.skip("Invalid reshape plan for the given arrow configuration.") + edge = (2, 2) + a = GrassmannTensor(arrow, (edge, edge, edge, edge, edge), torch.randn([4, 4, 4, 4, 4])) + plan = tuple(insert_trivial_between_elements([-1] * l + [4**(h - l)] + [-1] * (5 - h), 0.5)) + b = a.reshape(plan) + c = b.reshape(a.edges) + assert a.edges == c.edges + + +def test_reshape_merging_dimension_mismatch_edges_because_of_nonequal() -> None: arrow = (True, True, True) edges = ((2, 2), (8, 8), (2, 2)) a = GrassmannTensor(arrow, edges, torch.randn([4, 16, 4])) _ = a.reshape((64, -1)) _ = a.reshape((-1, 64)) - with pytest.raises(AssertionError, match="Dimension mismatch with edges"): + with pytest.raises(AssertionError, match="Dimension mismatch in merging"): _ = a.reshape((16, -1, -1)) +def test_reshape_merging_dimension_mismatch_edges_because_of_different_even_odd() -> None: + arrow = (True, True, True, True, True) + edges = ((0, 1), (1, 3), (1, 3), (0, 1), (2, 2)) + a = GrassmannTensor(arrow, edges, torch.randn([1, 4, 4, 1, 4])) + _ = a.reshape((16, -1, -1)) + _ = a.reshape(((6, 10), -1, -1)) + _ = a.reshape(((10, 6), -1)) + _ = a.reshape((4, -1, -1, -1)) + _ = a.reshape(((3, 1), -1, -1, -1)) + with pytest.raises(AssertionError, match="Dimension mismatch in merging"): + _ = a.reshape(((2, 2), -1, -1, -1)) + with pytest.raises(AssertionError, match="Dimension mismatch in merging"): + _ = a.reshape(((1, 3), -1, -1, -1)) + + def test_reshape_merging_new_shape_exceeds() -> None: arrow = (True,) edges = ((2, 2),) a = GrassmannTensor(arrow, edges, torch.randn([4])) - with pytest.raises(AssertionError, match="exceeds tensor dimensions"): + with pytest.raises(AssertionError, match="New shape exceeds in merging"): _ = a.reshape((16, -1)) -def test_reshape_merging_even_odd_mismatch() -> None: - arrow = (True, True, True) - edges = ((2, 2), (8, 8), (2, 2)) - a = GrassmannTensor(arrow, edges, torch.randn([4, 16, 4])) - _ = a.reshape(((32, 32), (2, 2))) - _ = a.reshape(((2, 2), (32, 32))) - with pytest.raises(AssertionError, match="New even and odd number mismatch during merging"): - _ = a.reshape(((30, 34), (2, 2))) - - def test_reshape_merging_mixed_arrows() -> None: arrow = (True, False, True) edges = ((2, 2), (2, 2), (2, 2)) @@ -62,27 +91,65 @@ def test_reshape_splitting_shape_type() -> None: _ = a.reshape((2, (2, 2))) -def test_reshape_splitting_dimension_mismatch_edges() -> None: +def test_reshape_splitting_dimension_mismatch_edges_because_of_nonequal() -> None: arrow = (True,) edges = ((8, 8),) a = GrassmannTensor(arrow, edges, torch.randn([16])) _ = a.reshape(((2, 2), (2, 2))) - with pytest.raises(AssertionError, match="Dimension mismatch with edges"): + with pytest.raises(AssertionError, match="Dimension mismatch in splitting"): _ = a.reshape(((4, 4), (2, 2))) +def test_reshape_splitting_dimension_mismatch_edges_because_of_different_even_odd() -> None: + arrow = (True, True) + edges = ((3, 1), (2, 2)) + a = GrassmannTensor(arrow, edges, torch.randn([4, 4])) + _ = a.reshape(((0, 1), (3, 1), (0, 1), (2, 2))) + with pytest.raises(AssertionError, match="Dimension mismatch in splitting"): + _ = a.reshape(((0, 1), (2, 2), (0, 1), (2, 2))) + with pytest.raises(AssertionError, match="Dimension mismatch in splitting"): + _ = a.reshape(((0, 1), (3, 1), (2, 2))) + + def test_reshape_splitting_shape_exceeds() -> None: arrow = (False,) + edges = ((8, 8),) + a = GrassmannTensor(arrow, edges, torch.randn([16])) + with pytest.raises(AssertionError, match="New shape exceeds in splitting"): + _ = a.reshape(((1, 1), (1, 1))) + + +def test_reshape_equal_eddges_trivial() -> None: + arrow = (True,) edges = ((2, 2),) a = GrassmannTensor(arrow, edges, torch.randn([4])) - with pytest.raises(AssertionError, match="exceeds specified dimensions"): - _ = a.reshape(((3, 0), (0, 1))) + _ = a.reshape((4,)) + _ = a.reshape(((2, 2),)) -def test_reshape_splitting_even_odd_mismatch() -> None: - arrow = (False,) - edges = ((6, 10),) - a = GrassmannTensor(arrow, edges, torch.randn([16])) - _ = a.reshape(((1, 3), (3, 1))) - with pytest.raises(AssertionError, match="New even and odd number mismatch during splitting"): - _ = a.reshape(((2, 2), (2, 2))) +def test_reshape_equal_eddges_nontrivial_splitting() -> None: + arrow = (True,) + edges = ((1, 3),) + a = GrassmannTensor(arrow, edges, torch.randn([4])) + _ = a.reshape(((3, 1), (1, 0), (0, 1))) + + +def test_reshape_equal_eddges_nontrivial_splitting_with_other_edge() -> None: + arrow = (True, True) + edges = ((1, 3), (2, 2)) + a = GrassmannTensor(arrow, edges, torch.randn([4, 4])) + _ = a.reshape(((3, 1), (1, 0), (0, 1), (2, 2))) + + +def test_reshape_equal_eddges_nontrivial_merging() -> None: + arrow = (True, True, True) + edges = ((1, 3), (1, 0), (0, 1)) + a = GrassmannTensor(arrow, edges, torch.randn([4, 1, 1])) + _ = a.reshape(((3, 1),)) + + +def test_reshape_equal_eddges_nontrivial_merging_with_other_edge() -> None: + arrow = (True, True, True, True) + edges = ((1, 3), (1, 0), (0, 1), (2, 2)) + a = GrassmannTensor(arrow, edges, torch.randn([4, 1, 1, 4])) + _ = a.reshape(((3, 1), (2, 2))) From 796cb8bb018ed1db13ae54b4bc03efb1e6a8799a Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Sun, 31 Aug 2025 12:16:38 +0800 Subject: [PATCH 3/8] Skip a useless check in edge reshape, as we checked before. --- grassmann_tensor/tensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/grassmann_tensor/tensor.py b/grassmann_tensor/tensor.py index d4497b0..baaec73 100644 --- a/grassmann_tensor/tensor.py +++ b/grassmann_tensor/tensor.py @@ -300,10 +300,8 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens assert all(self_arrow == arrow[-1] for self_arrow in self.arrow[cursor_self:new_cursor_self]), f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." edges.append((even, odd)) shape.append(total) - if cursor_self + 1 != new_cursor_self: - # Really something merged, otherwise no need to reorder or sign, which helps to avoid unnecessary operation for performance - merging_sign.append((cursor_plan, sign)) - merging_reorder.append((cursor_plan, reorder)) + merging_sign.append((cursor_plan, sign)) + merging_reorder.append((cursor_plan, reorder)) cursor_self = new_cursor_self cursor_plan += 1 else: From ed15467d35c53d63ef61d38b6e8ac5a57325735e Mon Sep 17 00:00:00 2001 From: Gausshj Date: Mon, 1 Sep 2025 16:12:28 +0800 Subject: [PATCH 4/8] style: fix typo in function name --- tests/reshape_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/reshape_test.py b/tests/reshape_test.py index b08123e..208f095 100644 --- a/tests/reshape_test.py +++ b/tests/reshape_test.py @@ -119,7 +119,7 @@ def test_reshape_splitting_shape_exceeds() -> None: _ = a.reshape(((1, 1), (1, 1))) -def test_reshape_equal_eddges_trivial() -> None: +def test_reshape_equal_edges_trivial() -> None: arrow = (True,) edges = ((2, 2),) a = GrassmannTensor(arrow, edges, torch.randn([4])) @@ -127,28 +127,28 @@ def test_reshape_equal_eddges_trivial() -> None: _ = a.reshape(((2, 2),)) -def test_reshape_equal_eddges_nontrivial_splitting() -> None: +def test_reshape_equal_edges_nontrivial_splitting() -> None: arrow = (True,) edges = ((1, 3),) a = GrassmannTensor(arrow, edges, torch.randn([4])) _ = a.reshape(((3, 1), (1, 0), (0, 1))) -def test_reshape_equal_eddges_nontrivial_splitting_with_other_edge() -> None: +def test_reshape_equal_edges_nontrivial_splitting_with_other_edge() -> None: arrow = (True, True) edges = ((1, 3), (2, 2)) a = GrassmannTensor(arrow, edges, torch.randn([4, 4])) _ = a.reshape(((3, 1), (1, 0), (0, 1), (2, 2))) -def test_reshape_equal_eddges_nontrivial_merging() -> None: +def test_reshape_equal_edges_nontrivial_merging() -> None: arrow = (True, True, True) edges = ((1, 3), (1, 0), (0, 1)) a = GrassmannTensor(arrow, edges, torch.randn([4, 1, 1])) _ = a.reshape(((3, 1),)) -def test_reshape_equal_eddges_nontrivial_merging_with_other_edge() -> None: +def test_reshape_equal_edges_nontrivial_merging_with_other_edge() -> None: arrow = (True, True, True, True) edges = ((1, 3), (1, 0), (0, 1), (2, 2)) a = GrassmannTensor(arrow, edges, torch.randn([4, 1, 1, 4])) From aab9668888b02a4cd10aa033875e11995e3bbd7b Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Tue, 2 Sep 2025 10:38:36 +0800 Subject: [PATCH 5/8] Fix a typo in typing in reshape_test.py. --- tests/reshape_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/reshape_test.py b/tests/reshape_test.py index 208f095..f79517b 100644 --- a/tests/reshape_test.py +++ b/tests/reshape_test.py @@ -18,8 +18,8 @@ def test_reshape_consistency(arrow: tuple[bool, ...], plan_range: tuple[int, int assert torch.allclose(a.tensor, c.tensor) -def insert_trivial_between_elements(input_list: list[tuple[int, int]], p: float) -> list[tuple[int, int]]: - result = [] +def insert_trivial_between_elements(input_list: list[tuple[int, int] | int], p: float) -> list[tuple[int, int] | int]: + result: list[tuple[int, int] | int] = [] for i in input_list: if random.random() < p: result.append((1, 0)) From 206b41363d7a102806500bc84716b6ee85bc5d84 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Tue, 2 Sep 2025 20:26:48 +0800 Subject: [PATCH 6/8] Revert "style: Reformat the codes." This reverts commit 018916100d182888ddd49031c4d6d4873b850523. --- grassmann_tensor/tensor.py | 166 ++++++++---------------------------- grassmann_tensor/version.py | 1 - tests/arithmetic_test.py | 106 +++++++++-------------- tests/attributes_test.py | 28 +++--- tests/clone_test.py | 4 +- tests/conversion_test.py | 5 +- tests/creation_test.py | 52 +++++------ tests/import_test.py | 1 - tests/matmul_test.py | 64 ++++++-------- tests/permute_test.py | 122 +++++--------------------- tests/reshape_test.py | 16 +--- tests/reverse_test.py | 130 +++++----------------------- tests/update_mask_test.py | 56 ++++++------ 13 files changed, 210 insertions(+), 541 deletions(-) diff --git a/grassmann_tensor/tensor.py b/grassmann_tensor/tensor.py index 0236dfc..697bb7e 100644 --- a/grassmann_tensor/tensor.py +++ b/grassmann_tensor/tensor.py @@ -64,13 +64,7 @@ def mask(self) -> torch.Tensor: self._mask = self._tensor_mask() return self._mask - def to( - self, - whatever: torch.device | torch.dtype | str | None = None, - *, - device: torch.device | None = None, - dtype: torch.dtype | None = None, - ) -> GrassmannTensor: + def to(self, whatever: torch.device | torch.dtype | str | None = None, *, device: torch.device | None = None, dtype: torch.dtype | None = None) -> GrassmannTensor: """ Copy the tensor to a specified device or copy it to a specified data type. """ @@ -98,18 +92,14 @@ def to( return dataclasses.replace( self, _tensor=self._tensor.to(device=device), - _parity=tuple(p.to(device) for p in self._parity) - if self._parity is not None - else None, + _parity=tuple(p.to(device) for p in self._parity) if self._parity is not None else None, _mask=self._mask.to(device) if self._mask is not None else None, ) case _: return dataclasses.replace( self, _tensor=self._tensor.to(device=device, dtype=dtype), - _parity=tuple(p.to(device=device) for p in self._parity) - if self._parity is not None - else None, + _parity=tuple(p.to(device=device) for p in self._parity) if self._parity is not None else None, _mask=self._mask.to(device=device) if self._mask is not None else None, ) @@ -124,12 +114,8 @@ def permute(self, before_by_after: tuple[int, ...]) -> GrassmannTensor: """ Permute the indices of the Grassmann tensor. """ - assert len(before_by_after) == len(set(before_by_after)), ( - "Permutation indices must be unique." - ) - assert set(before_by_after) == set(range(self.tensor.dim())), ( - "Permutation indices must cover all dimensions." - ) + assert len(before_by_after) == len(set(before_by_after)), "Permutation indices must be unique." + assert set(before_by_after) == set(range(self.tensor.dim())), "Permutation indices must cover all dimensions." arrow = tuple(self.arrow[i] for i in before_by_after) edges = tuple(self.edges[i] for i in before_by_after) @@ -140,14 +126,10 @@ def permute(self, before_by_after: tuple[int, ...]) -> GrassmannTensor: total_parity = functools.reduce( torch.logical_xor, ( - torch.logical_and( - self._unsqueeze(parity[i], i, self.tensor.dim()), - self._unsqueeze(parity[j], j, self.tensor.dim()), - ) + torch.logical_and(self._unsqueeze(parity[i], i, self.tensor.dim()), self._unsqueeze(parity[j], j, self.tensor.dim())) for j in range(self.tensor.dim()) for i in range(0, j) # all 0 <= i < j < dim - if before_by_after[i] > before_by_after[j] - ), + if before_by_after[i] > before_by_after[j]), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(total_parity, -tensor, +tensor) @@ -169,20 +151,14 @@ def reverse(self, indices: tuple[int, ...]) -> GrassmannTensor: This package always applies it to the tensor with arrow as True. """ assert len(set(indices)) == len(indices), f"Indices must be unique. Got {indices}." - assert all(0 <= i < self.tensor.dim() for i in indices), ( - f"Indices must be within tensor dimensions. Got {indices}." - ) + assert all(0 <= i < self.tensor.dim() for i in indices), f"Indices must be within tensor dimensions. Got {indices}." arrow = tuple(self.arrow[i] ^ i in indices for i in range(self.tensor.dim())) tensor = self.tensor total_parity = functools.reduce( torch.logical_xor, - ( - self._unsqueeze(parity, index, self.tensor.dim()) - for index, parity in enumerate(self.parity) - if index in indices and self.arrow[index] - ), + (self._unsqueeze(parity, index, self.tensor.dim()) for index, parity in enumerate(self.parity) if index in indices and self.arrow[index]), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(total_parity, -tensor, +tensor) @@ -193,15 +169,10 @@ def reverse(self, indices: tuple[int, ...]) -> GrassmannTensor: _tensor=tensor, ) - def _reorder_indices( - self, edges: tuple[tuple[int, int], ...] - ) -> tuple[int, int, torch.Tensor, torch.Tensor]: + def _reorder_indices(self, edges: tuple[tuple[int, int], ...]) -> tuple[int, int, torch.Tensor, torch.Tensor]: parity = functools.reduce( torch.logical_xor, - ( - self._unsqueeze(self._edge_mask(even, odd), index, len(edges)) - for index, (even, odd) in enumerate(edges) - ), + (self._unsqueeze(self._edge_mask(even, odd), index, len(edges)) for index, (even, odd) in enumerate(edges)), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) flatten_parity = parity.flatten() @@ -211,10 +182,7 @@ def _reorder_indices( total = functools.reduce( torch.add, - ( - self._unsqueeze(self._edge_mask(even, odd), index, len(edges)).to(dtype=torch.int16) - for index, (even, odd) in enumerate(edges) - ), + (self._unsqueeze(self._edge_mask(even, odd), index, len(edges)).to(dtype=torch.int16) for index, (even, odd) in enumerate(edges)), torch.zeros([], dtype=torch.int16, device=self.tensor.device), ) count = total * (total - 1) @@ -267,11 +235,7 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens cursor_plan += 1 else: cursor_new_shape = new_shape[cursor_plan] - total = ( - cursor_new_shape - if isinstance(cursor_new_shape, int) - else cursor_new_shape[0] + cursor_new_shape[1] - ) + total = cursor_new_shape if isinstance(cursor_new_shape, int) else cursor_new_shape[0] + cursor_new_shape[1] if total >= self.tensor.shape[cursor_self]: # Merging new_cursor_self = cursor_self @@ -281,26 +245,14 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens new_cursor_self += 1 if self_total == total: break - assert self_total < total, ( - f"Dimension mismatch with edges {self.edges} and new shape {new_shape}." - ) - assert new_cursor_self < self.tensor.dim(), ( - f"New shape {new_shape} exceeds tensor dimensions {self.tensor.dim()}." - ) - even, odd, reorder, sign = self._reorder_indices( - self.edges[cursor_self:new_cursor_self] - ) + assert self_total < total, f"Dimension mismatch with edges {self.edges} and new shape {new_shape}." + assert new_cursor_self < self.tensor.dim(), f"New shape {new_shape} exceeds tensor dimensions {self.tensor.dim()}." + even, odd, reorder, sign = self._reorder_indices(self.edges[cursor_self:new_cursor_self]) if isinstance(cursor_new_shape, tuple): - assert (even, odd) == cursor_new_shape, ( - f"New even and odd number mismatch during merging {self.edges} to {new_shape}." - ) + assert (even, odd) == cursor_new_shape, f"New even and odd number mismatch during merging {self.edges} to {new_shape}." arrow.append(self.arrow[cursor_self]) assert all( - self_arrow == arrow[-1] - for self_arrow in self.arrow[cursor_self:new_cursor_self] - ), ( - f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." - ) + self_arrow == arrow[-1] for self_arrow in self.arrow[cursor_self:new_cursor_self]), f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." edges.append((even, odd)) shape.append(total) if cursor_self + 1 != new_cursor_self: @@ -315,28 +267,16 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens plan_total = 1 while True: new_cursor_new_shape = new_shape[new_cursor_plan] - assert isinstance(new_cursor_new_shape, tuple), ( - f"New shape must be a pair when splitting, got {new_cursor_new_shape}." - ) + assert isinstance(new_cursor_new_shape, tuple), f"New shape must be a pair when splitting, got {new_cursor_new_shape}." plan_total *= new_cursor_new_shape[0] + new_cursor_new_shape[1] new_cursor_plan += 1 if plan_total == self.tensor.shape[cursor_self]: break - assert plan_total < self.tensor.shape[cursor_self], ( - f"Dimension mismatch with edges {self.edges} and new shape {new_shape}." - ) - assert new_cursor_plan < len(new_shape), ( - f"New shape {new_shape} exceeds specified dimensions {len(new_shape)}." - ) + assert plan_total < self.tensor.shape[cursor_self], f"Dimension mismatch with edges {self.edges} and new shape {new_shape}." + assert new_cursor_plan < len(new_shape), f"New shape {new_shape} exceeds specified dimensions {len(new_shape)}." # new_shape has been verified to be tuple[int, int] in the loop - even, odd, reorder, sign = self._reorder_indices( - typing.cast( - tuple[tuple[int, int], ...], new_shape[cursor_plan:new_cursor_plan] - ) - ) - assert (even, odd) == self.edges[cursor_self], ( - f"New even and odd number mismatch during splitting {self.edges[cursor_self]} to {new_shape[cursor_plan:new_cursor_plan]}." - ) + even, odd, reorder, sign = self._reorder_indices(typing.cast(tuple[tuple[int, int], ...], new_shape[cursor_plan:new_cursor_plan])) + assert (even, odd) == self.edges[cursor_self], f"New even and odd number mismatch during splitting {self.edges[cursor_self]} to {new_shape[cursor_plan:new_cursor_plan]}." for i in range(cursor_plan, new_cursor_plan): # new_shape has been verified to be tuple[int, int] in the loop new_cursor_new_shape = typing.cast(tuple[int, int], new_shape[i]) @@ -360,11 +300,7 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens splitting_parity = functools.reduce( torch.logical_xor, - ( - self._unsqueeze(sign, index, self.tensor.dim()) - for index, sign in splitting_sign - if self.arrow[index] - ), + (self._unsqueeze(sign, index, self.tensor.dim()) for index, sign in splitting_sign if self.arrow[index]), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(splitting_parity, -tensor, +tensor) @@ -373,11 +309,7 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens merging_parity = functools.reduce( torch.logical_xor, - ( - self._unsqueeze(sign, index, tensor.dim()) - for index, sign in merging_sign - if arrow[index] - ), + (self._unsqueeze(sign, index, tensor.dim()) for index, sign in merging_sign if arrow[index]), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(merging_parity, -tensor, +tensor) @@ -406,12 +338,8 @@ def matmul(self, other: GrassmannTensor) -> GrassmannTensor: tensor_b = tensor_b.reshape((-1, (1, 0))) vector_b = True - assert all(odd == 0 for (even, odd) in tensor_a.edges[:-2]), ( - f"All edges except the last two must be pure even. Got {tensor_a.edges[:-2]}." - ) - assert all(odd == 0 for (even, odd) in tensor_b.edges[:-2]), ( - f"All edges except the last two must be pure even. Got {tensor_b.edges[:-2]}." - ) + assert all(odd == 0 for (even, odd) in tensor_a.edges[:-2]), f"All edges except the last two must be pure even. Got {tensor_a.edges[:-2]}." + assert all(odd == 0 for (even, odd) in tensor_b.edges[:-2]), f"All edges except the last two must be pure even. Got {tensor_b.edges[:-2]}." if tensor_a.arrow[-1] is not True: tensor_a = tensor_a.reverse((tensor_a.tensor.dim() - 1,)) @@ -430,9 +358,7 @@ def matmul(self, other: GrassmannTensor) -> GrassmannTensor: candidate_a = tensor_a.edges[i - 2][0] if i >= -broadcast_b: candidate_b = tensor_b.edges[i - 2][0] - assert candidate_a == candidate_b or candidate_a == 1 or candidate_b == 1, ( - f"Cannot broadcast edges {tensor_a.edges[i - 2]} and {tensor_b.edges[i - 2]}." - ) + assert candidate_a == candidate_b or candidate_a == 1 or candidate_b == 1, f"Cannot broadcast edges {tensor_a.edges[i - 2]} and {tensor_b.edges[i - 2]}." edges.append((max(candidate_a, candidate_b), 0)) if not vector_a: arrow.append(tensor_a.arrow[-2]) @@ -453,35 +379,21 @@ def matmul(self, other: GrassmannTensor) -> GrassmannTensor: ) 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()})." - ) - assert len(self._edges) == self._tensor.dim(), ( - f"Edges length ({len(self._edges)}) must match tensor dimensions ({self._tensor.dim()})." - ) + assert len(self._arrow) == self._tensor.dim(), f"Arrow length ({len(self._arrow)}) must match tensor dimensions ({self._tensor.dim()})." + assert len(self._edges) == self._tensor.dim(), f"Edges length ({len(self._edges)}) must match tensor dimensions ({self._tensor.dim()})." for dim, (even, odd) in zip(self._tensor.shape, self._edges): - assert even >= 0 and odd >= 0 and dim == even + odd, ( - f"Dimension {dim} must equal sum of even ({even}) and odd ({odd}) parts, and both must be non-negative." - ) + assert even >= 0 and odd >= 0 and dim == even + odd, f"Dimension {dim} must equal sum of even ({even}) and odd ({odd}) parts, and both must be non-negative." def _unsqueeze(self, tensor: torch.Tensor, index: int, dim: int) -> torch.Tensor: return tensor.view([-1 if i == index else 1 for i in range(dim)]) def _edge_mask(self, even: int, odd: int) -> torch.Tensor: - return torch.cat( - [ - torch.zeros(even, dtype=torch.bool, device=self.tensor.device), - torch.ones(odd, dtype=torch.bool, device=self.tensor.device), - ] - ) + return torch.cat([torch.zeros(even, dtype=torch.bool, device=self.tensor.device), torch.ones(odd, dtype=torch.bool, device=self.tensor.device)]) def _tensor_mask(self) -> torch.Tensor: return functools.reduce( torch.logical_xor, - ( - self._unsqueeze(parity, index, self._tensor.dim()) - for index, parity in enumerate(self.parity) - ), + (self._unsqueeze(parity, index, self._tensor.dim()) for index, parity in enumerate(self.parity)), torch.zeros_like(self._tensor, dtype=torch.bool), ) @@ -489,12 +401,8 @@ def _validate_edge_compatibility(self, other: GrassmannTensor) -> None: """ Validate that the edges of two ParityTensor instances are compatible for arithmetic operations. """ - assert self._arrow == other.arrow, ( - f"Arrows must match for arithmetic operations. Got {self._arrow} and {other.arrow}." - ) - assert self._edges == other.edges, ( - f"Edges must match for arithmetic operations. Got {self._edges} and {other.edges}." - ) + assert self._arrow == other.arrow, f"Arrows must match for arithmetic operations. Got {self._arrow} and {other.arrow}." + assert self._edges == other.edges, f"Edges must match for arithmetic operations. Got {self._edges} and {other.edges}." def __pos__(self) -> GrassmannTensor: return dataclasses.replace( @@ -687,9 +595,7 @@ def clone(self) -> GrassmannTensor: return dataclasses.replace( self, _tensor=self._tensor.clone(), - _parity=tuple(parity.clone() for parity in self._parity) - if self._parity is not None - else None, + _parity=tuple(parity.clone() for parity in self._parity) if self._parity is not None else None, _mask=self._mask.clone() if self._mask is not None else None, ) diff --git a/grassmann_tensor/version.py b/grassmann_tensor/version.py index 0c775ec..2d474b2 100644 --- a/grassmann_tensor/version.py +++ b/grassmann_tensor/version.py @@ -11,7 +11,6 @@ except ModuleNotFoundError: try: import importlib.metadata - __version__ = importlib.metadata.version("parity") except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" diff --git a/tests/arithmetic_test.py b/tests/arithmetic_test.py index c5f758c..8f56b59 100644 --- a/tests/arithmetic_test.py +++ b/tests/arithmetic_test.py @@ -5,69 +5,52 @@ from grassmann_tensor import GrassmannTensor -@pytest.fixture( - params=[ - ( - GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - ), - ( - GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), - GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), - ), - ( - GrassmannTensor( - (True, True, False, False), - ((1, 2), (2, 2), (1, 1), (3, 1)), - torch.randn([3, 4, 2, 4]), - ), - GrassmannTensor( - (True, True, False, False), - ((1, 2), (2, 2), (1, 1), (3, 1)), - torch.randn([3, 4, 2, 4]), - ), - ), - ] -) +@pytest.fixture(params=[ + ( + GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + ), + ( + GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), + GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), + ), + ( + GrassmannTensor((True, True, False, False), ((1, 2), (2, 2), (1, 1), (3, 1)), torch.randn([3, 4, 2, 4])), + GrassmannTensor((True, True, False, False), ((1, 2), (2, 2), (1, 1), (3, 1)), torch.randn([3, 4, 2, 4])), + ), +]) def tensors(request: pytest.FixtureRequest) -> tuple[GrassmannTensor, GrassmannTensor]: return request.param -@pytest.fixture( - params=[ - ( - GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - GrassmannTensor((False,), ((2, 2),), torch.randn([4])), - ), - ( - GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), - GrassmannTensor((True, False, True), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), - ), - ( - GrassmannTensor((True, True, False), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), - GrassmannTensor( - (True, True, False, False), - ((3, 2), (2, 2), (1, 1), (3, 1)), - torch.randn([5, 4, 2, 4]), - ), - ), - ] -) +@pytest.fixture(params=[ + ( + GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + GrassmannTensor((False,), ((2, 2),), torch.randn([4])), + ), + ( + GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), + GrassmannTensor((True, False, True), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), + ), + ( + GrassmannTensor((True, True, False), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), + GrassmannTensor((True, True, False, False), ((3, 2), (2, 2), (1, 1), (3, 1)), torch.randn([5, 4, 2, 4])), + ), +]) def mismatch_tensors(request: pytest.FixtureRequest) -> tuple[GrassmannTensor, GrassmannTensor]: return request.param -@pytest.fixture( - params=[ - torch.randn([]), - torch.randn([]).item(), - ] -) +@pytest.fixture(params=[ + torch.randn([]), + torch.randn([]).item(), +]) def scalar(request: pytest.FixtureRequest) -> torch.Tensor | float: return request.param class FakeTensor: + def __init__(self) -> None: pass @@ -123,20 +106,15 @@ def __rtruediv__(self, other: typing.Any) -> FakeTensor: @pytest.mark.parametrize( "unsupported_type", [ - "string", # string - None, # NoneType - {"key", "value"}, # dict - [1, 2, 3], # list - {1, 2}, # set - object(), # arbitrary object - FakeTensor(), # an ill defined tensor-like object - ], -) -def test_arithmetic( - unsupported_type: typing.Any, - tensors: tuple[GrassmannTensor, GrassmannTensor], - scalar: torch.Tensor | float, -) -> None: + "string", #string + None, #NoneType + {"key", "value"}, #dict + [1, 2, 3], #list + {1, 2}, #set + object(), #arbitrary object + FakeTensor(), #an ill defined tensor-like object + ]) +def test_arithmetic(unsupported_type: typing.Any, tensors: tuple[GrassmannTensor, GrassmannTensor], scalar: torch.Tensor | float) -> None: tensor_a, tensor_b = tensors # Test __pos__ method. diff --git a/tests/attributes_test.py b/tests/attributes_test.py index 9a12a82..7a8e57c 100644 --- a/tests/attributes_test.py +++ b/tests/attributes_test.py @@ -5,19 +5,17 @@ Initialization = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor] -@pytest.fixture( - params=[ - ((False, False), ((2, 2), (2, 2)), torch.randn([4, 4])), - ((False, True), ((2, 2), (1, 3)), torch.randn([4, 4])), - ((False, True), ((2, 0), (1, 3)), torch.randn([2, 4])), - ((True, False), ((0, 2), (1, 3)), torch.randn([2, 4])), - ((True, False), ((0, 0), (1, 3)), torch.randn([0, 4])), - ((True,), ((2, 0),), torch.randn([2])), - ((False,), ((0, 2),), torch.randn([2])), - ((), (), torch.randn([])), - ((False, False, True), ((2, 2), (1, 3), (4, 0)), torch.randn([4, 4, 4])), - ] -) +@pytest.fixture(params=[ + ((False, False), ((2, 2), (2, 2)), torch.randn([4, 4])), + ((False, True), ((2, 2), (1, 3)), torch.randn([4, 4])), + ((False, True), ((2, 0), (1, 3)), torch.randn([2, 4])), + ((True, False), ((0, 2), (1, 3)), torch.randn([2, 4])), + ((True, False), ((0, 0), (1, 3)), torch.randn([0, 4])), + ((True,), ((2, 0),), torch.randn([2])), + ((False,), ((0, 2),), torch.randn([2])), + ((), (), torch.randn([])), + ((False, False, True), ((2, 2), (1, 3), (4, 0)), torch.randn([4, 4, 4])), +]) def x(request: pytest.FixtureRequest) -> Initialization: return request.param @@ -52,9 +50,7 @@ def test_mask(x: Initialization) -> None: tensor = GrassmannTensor(*x) assert tensor.mask.shape == tensor.tensor.shape assert tensor.mask.dtype == torch.bool - for indices in zip( - *torch.unravel_index(torch.arange(tensor.tensor.numel()), tensor.tensor.shape) - ): + for indices in zip(*torch.unravel_index(torch.arange(tensor.tensor.numel()), tensor.tensor.shape)): mask = tensor.mask[indices] expect = False for rank, parity in enumerate(tensor.parity): diff --git a/tests/clone_test.py b/tests/clone_test.py index 28d498a..6a67bcc 100644 --- a/tests/clone_test.py +++ b/tests/clone_test.py @@ -37,9 +37,7 @@ def test_clone( if parity: assert cloned_tensor._parity is not None assert original_tensor._parity is not None - assert all( - torch.equal(c, o) for c, o in zip(cloned_tensor._parity, original_tensor._parity) - ) + assert all(torch.equal(c, o) for c, o in zip(cloned_tensor._parity, original_tensor._parity)) else: assert cloned_tensor._parity is original_tensor._parity if mask: diff --git a/tests/conversion_test.py b/tests/conversion_test.py index 29b1a59..0151dc8 100644 --- a/tests/conversion_test.py +++ b/tests/conversion_test.py @@ -47,10 +47,7 @@ def test_conversion( assert y.arrow == x.arrow assert y.edges == x.edges assert y.tensor.dtype == torch.complex128 if dtype_arg != "none" else torch.float32 - assert ( - y.tensor.device.type - == (torch.device(device_str) if device_arg != "none" else torch.device("cpu:0")).type - ) + assert y.tensor.device.type == (torch.device(device_str) if device_arg != "none" else torch.device("cpu:0")).type assert torch.allclose(y.tensor, x.tensor.to(dtype=y.tensor.dtype, device=y.tensor.device)) diff --git a/tests/creation_test.py b/tests/creation_test.py index 1185293..e4eb406 100644 --- a/tests/creation_test.py +++ b/tests/creation_test.py @@ -5,52 +5,40 @@ Initialization = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor] -@pytest.mark.parametrize( - "x", - [ - ((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - ((True, False), ((2, 2), (3, 1)), torch.randn([4, 4])), - ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), - ], -) +@pytest.mark.parametrize("x", [ + ((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + ((True, False), ((2, 2), (3, 1)), torch.randn([4, 4])), + ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), +]) def test_creation_success(x: Initialization) -> None: GrassmannTensor(*x) -@pytest.mark.parametrize( - "x", - [ - ((False,), ((2, 2), (1, 3)), torch.randn([4, 4])), - ((True, False, True), ((2, 2), (3, 1)), torch.randn([4, 4])), - ((False, True), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), - ], -) +@pytest.mark.parametrize("x", [ + ((False,), ((2, 2), (1, 3)), torch.randn([4, 4])), + ((True, False, True), ((2, 2), (3, 1)), torch.randn([4, 4])), + ((False, True), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), +]) def test_creation_invalid_arrow(x: Initialization) -> None: with pytest.raises(AssertionError, match="Arrow length"): GrassmannTensor(*x) -@pytest.mark.parametrize( - "x", - [ - ((False, False), ((2, 2),), torch.randn([4, 4])), - ((True, False), ((2, 2), (1, 1), (3, 1)), torch.randn([4, 4])), - ((False, True, False), ((1, 1), (1, 1)), torch.randn([2, 4, 2])), - ], -) +@pytest.mark.parametrize("x", [ + ((False, False), ((2, 2),), torch.randn([4, 4])), + ((True, False), ((2, 2), (1, 1), (3, 1)), torch.randn([4, 4])), + ((False, True, False), ((1, 1), (1, 1)), torch.randn([2, 4, 2])), +]) def test_creation_invalid_edges(x: Initialization) -> None: with pytest.raises(AssertionError, match="Edges length"): GrassmannTensor(*x) -@pytest.mark.parametrize( - "x", - [ - ((False, False), ((2, 2), (1, 3)), torch.randn([4, 2])), - ((True, False), ((2, 2), (3, 1)), torch.randn([2, 4])), - ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([4, 4, 2])), - ], -) +@pytest.mark.parametrize("x", [ + ((False, False), ((2, 2), (1, 3)), torch.randn([4, 2])), + ((True, False), ((2, 2), (3, 1)), torch.randn([2, 4])), + ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([4, 4, 2])), +]) def test_creation_invalid_shape(x: Initialization) -> None: with pytest.raises(AssertionError, match="must equal sum of"): GrassmannTensor(*x) diff --git a/tests/import_test.py b/tests/import_test.py index 20b0985..5a91002 100644 --- a/tests/import_test.py +++ b/tests/import_test.py @@ -1,4 +1,3 @@ def test_import() -> None: from grassmann_tensor import GrassmannTensor - assert isinstance(GrassmannTensor, type) diff --git a/tests/matmul_test.py b/tests/matmul_test.py index f00316e..d7eb048 100644 --- a/tests/matmul_test.py +++ b/tests/matmul_test.py @@ -5,30 +5,23 @@ MatmulMatrixCase = tuple[bool, bool, tuple[int, int], tuple[int, int], tuple[int, int]] -@pytest.mark.parametrize( - "x", - [ - (False, False, (1, 1), (1, 1), (1, 1)), - (False, True, (1, 1), (1, 1), (1, 1)), - (True, False, (1, 1), (1, 1), (1, 1)), - (True, True, (1, 1), (1, 1), (1, 1)), - (False, False, (2, 2), (2, 2), (2, 2)), - (False, True, (2, 2), (2, 2), (2, 2)), - (True, False, (2, 2), (2, 2), (2, 2)), - (True, True, (2, 2), (2, 2), (2, 2)), - ], -) +@pytest.mark.parametrize("x", [ + (False, False, (1, 1), (1, 1), (1, 1)), + (False, True, (1, 1), (1, 1), (1, 1)), + (True, False, (1, 1), (1, 1), (1, 1)), + (True, True, (1, 1), (1, 1), (1, 1)), + (False, False, (2, 2), (2, 2), (2, 2)), + (False, True, (2, 2), (2, 2), (2, 2)), + (True, False, (2, 2), (2, 2), (2, 2)), + (True, True, (2, 2), (2, 2), (2, 2)), +]) def test_matmul_matrix_tf(x: MatmulMatrixCase) -> None: arrow_a, arrow_b, edge_a, edge_common, edge_b = x dim_a = sum(edge_a) dim_common = sum(edge_common) dim_b = sum(edge_b) - a = GrassmannTensor( - (arrow_a, True), (edge_a, edge_common), torch.randn([dim_a, dim_common]) - ).update_mask() - b = GrassmannTensor( - (False, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b]) - ).update_mask() + a = GrassmannTensor((arrow_a, True), (edge_a, edge_common), torch.randn([dim_a, dim_common])).update_mask() + b = GrassmannTensor((False, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b])).update_mask() c = a.matmul(b) expected = a.tensor.matmul(b.tensor) assert c.arrow == (arrow_a, arrow_b) @@ -36,33 +29,26 @@ def test_matmul_matrix_tf(x: MatmulMatrixCase) -> None: assert torch.allclose(c.tensor, expected) -@pytest.mark.parametrize( - "x", - [ - (False, False, (1, 1), (1, 1), (1, 1)), - (False, True, (1, 1), (1, 1), (1, 1)), - (True, False, (1, 1), (1, 1), (1, 1)), - (True, True, (1, 1), (1, 1), (1, 1)), - (False, False, (2, 2), (2, 2), (2, 2)), - (False, True, (2, 2), (2, 2), (2, 2)), - (True, False, (2, 2), (2, 2), (2, 2)), - (True, True, (2, 2), (2, 2), (2, 2)), - ], -) +@pytest.mark.parametrize("x", [ + (False, False, (1, 1), (1, 1), (1, 1)), + (False, True, (1, 1), (1, 1), (1, 1)), + (True, False, (1, 1), (1, 1), (1, 1)), + (True, True, (1, 1), (1, 1), (1, 1)), + (False, False, (2, 2), (2, 2), (2, 2)), + (False, True, (2, 2), (2, 2), (2, 2)), + (True, False, (2, 2), (2, 2), (2, 2)), + (True, True, (2, 2), (2, 2), (2, 2)), +]) def test_matmul_matrix_ft(x: MatmulMatrixCase) -> None: arrow_a, arrow_b, edge_a, edge_common, edge_b = x dim_a = sum(edge_a) dim_common = sum(edge_common) dim_b = sum(edge_b) - a = GrassmannTensor( - (arrow_a, False), (edge_a, edge_common), torch.randn([dim_a, dim_common]) - ).update_mask() - b = GrassmannTensor( - (True, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b]) - ).update_mask() + a = GrassmannTensor((arrow_a, False), (edge_a, edge_common), torch.randn([dim_a, dim_common])).update_mask() + b = GrassmannTensor((True, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b])).update_mask() c = a.matmul(b) expected = a.tensor.matmul(b.tensor) - expected[edge_a[0] :, edge_b[0] :] *= -1 + expected[edge_a[0]:, edge_b[0]:] *= -1 assert c.arrow == (arrow_a, arrow_b) assert c.edges == (edge_a, edge_b) assert torch.allclose(c.tensor, expected) diff --git a/tests/permute_test.py b/tests/permute_test.py index 12f6949..f7197f7 100644 --- a/tests/permute_test.py +++ b/tests/permute_test.py @@ -2,68 +2,21 @@ import torch from grassmann_tensor import GrassmannTensor -PermuteCase = tuple[ - tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor -] +PermuteCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor] -@pytest.mark.parametrize( - "x", - [ - ((), (), torch.tensor(6), (), torch.tensor(6)), - ((False,), ((1, 1),), torch.tensor([1, 2]), (0,), torch.tensor([1, 2])), - ((False, True), ((1, 1), (0, 0)), torch.zeros([2, 0]), (1, 0), torch.zeros([0, 2])), - ( - (False, True), - ((1, 1), (0, 1)), - torch.tensor([[0], [4]]), - (1, 0), - torch.tensor([[0, -4]]), - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (0, 1), - torch.tensor([[1, 0], [0, 4]]), - ), - ( - (True, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (1, 0), - torch.tensor([[1, 0], [0, -4]]), - ), - ( - (False, True, True), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (0, 2, 1), - torch.tensor([[[1, 0], [0, -2]], [[0, 4], [3, 0]]]), - ), - ( - (True, True, True), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (1, 0, 2), - torch.tensor([[[1, 0], [0, 3]], [[0, 2], [-4, 0]]]), - ), - ( - (True, False, False), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (2, 1, 0), - torch.tensor([[[1, 0], [0, -4]], [[0, -3], [-2, 0]]]), - ), - ( - (False, False, False), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (2, 0, 1), - torch.tensor([[[1, 0], [0, 4]], [[0, -2], [-3, 0]]]), - ), - ], -) +@pytest.mark.parametrize("x", [ + ((), (), torch.tensor(6), (), torch.tensor(6)), + ((False,), ((1, 1),), torch.tensor([1, 2]), (0,), torch.tensor([1, 2])), + ((False, True), ((1, 1), (0, 0)), torch.zeros([2, 0]), (1, 0), torch.zeros([0, 2])), + ((False, True), ((1, 1), (0, 1)), torch.tensor([[0], [4]]), (1, 0), torch.tensor([[0, -4]])), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 1), torch.tensor([[1, 0], [0, 4]])), + ((True, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (1, 0), torch.tensor([[1, 0], [0, -4]])), + ((False, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0, 2, 1), torch.tensor([[[1, 0], [0, -2]], [[0, 4], [3, 0]]])), + ((True, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (1, 0, 2), torch.tensor([[[1, 0], [0, 3]], [[0, 2], [-4, 0]]])), + ((True, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (2, 1, 0), torch.tensor([[[1, 0], [0, -4]], [[0, -3], [-2, 0]]])), + ((False, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (2, 0, 1), torch.tensor([[[1, 0], [0, 4]], [[0, -2], [-3, 0]]])), +]) def test_permute(x: PermuteCase) -> None: arrow, edges, tensor, before_by_after, expected = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) @@ -71,37 +24,14 @@ def test_permute(x: PermuteCase) -> None: assert torch.allclose(result.tensor, expected) -PermuteFailCase = tuple[ - tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str -] +PermuteFailCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str] -@pytest.mark.parametrize( - "x", - [ - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (0, 0), - "Permutation indices must be unique", - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (2, 0), - "Permutation indices must cover all dimensions", - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (0, 0, 1), - "Permutation indices must be unique", - ), - ], -) +@pytest.mark.parametrize("x", [ + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 0), "Permutation indices must be unique"), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (2, 0), "Permutation indices must cover all dimensions"), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 0, 1), "Permutation indices must be unique"), +]) def test_permute_fail(x: PermuteFailCase) -> None: arrow, edges, tensor, before_by_after, message = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) @@ -111,17 +41,13 @@ def test_permute_fail(x: PermuteFailCase) -> None: def test_permute_high_order() -> None: edge = (2, 2) - a = GrassmannTensor( - (False, False, False, False, False, False), - (edge, edge, edge, edge, edge, edge), - torch.randn(4, 4, 4, 4, 4, 4), - ).update_mask() + a = GrassmannTensor((False, False, False, False, False, False), (edge, edge, edge, edge, edge, edge), torch.randn(4, 4, 4, 4, 4, 4)).update_mask() # a[i, j, k, l, m, n] -> b[l, j, i, n, k, m] b = a.permute((3, 1, 0, 5, 2, 4)) for i in range(4): for j in range(4): for k in range(4): - for l in range(4): # noqa: E741 + for l in range(4): for m in range(4): for n in range(4): p = [bool(x & 2) for x in (i, j, k, l, m, n)] @@ -131,11 +57,7 @@ def test_permute_high_order() -> None: # (l) (i j k) m n # l (j) (i) k m n # l j i (n) (k m) - sign = ( - (p[3] & (p[0] ^ p[1] ^ p[2])) - ^ (p[1] & p[0]) - ^ (p[5] & (p[2] ^ p[4])) - ) + sign = (p[3] & (p[0] ^ p[1] ^ p[2])) ^ (p[1] & p[0]) ^ (p[5] & (p[2] ^ p[4])) if sign: assert b.tensor[l, j, i, n, k, m] == -a.tensor[i, j, k, l, m, n] else: diff --git a/tests/reshape_test.py b/tests/reshape_test.py index 60f693c..f04b273 100644 --- a/tests/reshape_test.py +++ b/tests/reshape_test.py @@ -3,25 +3,15 @@ from grassmann_tensor import GrassmannTensor -@pytest.mark.parametrize( - "arrow", - [ - (i, j, k, l, m) - for i in [False, True] - for j in [False, True] - for k in [False, True] - for l in [False, True] # noqa: E741 - for m in [False, True] - ], -) +@pytest.mark.parametrize("arrow", [(i, j, k, l, m) for i in [False, True] for j in [False, True] for k in [False, True] for l in [False, True] for m in [False, True]]) @pytest.mark.parametrize("plan_range", [(i, j) for i in range(5) for j in range(5) if j > i]) def test_reshape_consistency(arrow: tuple[bool, ...], plan_range: tuple[int, int]) -> None: - l, h = plan_range # noqa: E741 + l, h = plan_range if not all(arrow[l:h]) and any(arrow[l:h]): pytest.skip("Invalid reshape plan for the given arrow configuration.") edge = (2, 2) a = GrassmannTensor(arrow, (edge, edge, edge, edge, edge), torch.randn([4, 4, 4, 4, 4])) - plan = tuple([-1] * l + [4 ** (h - l)] + [-1] * (5 - h)) + plan = tuple([-1] * l + [4**(h - l)] + [-1] * (5 - h)) b = a.reshape(plan) c = b.reshape(a.edges) assert torch.allclose(a.tensor, c.tensor) diff --git a/tests/reverse_test.py b/tests/reverse_test.py index ead3806..59cb9f8 100644 --- a/tests/reverse_test.py +++ b/tests/reverse_test.py @@ -2,95 +2,24 @@ import torch from grassmann_tensor.tensor import GrassmannTensor -ReverseCase = tuple[ - tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor -] +ReverseCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor] -@pytest.mark.parametrize( - "x", - [ - ((), (), torch.tensor(6), (), torch.tensor(6)), - ((False, False), ((1, 1), (0, 0)), torch.zeros([2, 0]), (0,), torch.zeros([2, 0])), - ( - (False, False), - ((1, 1), (0, 1)), - torch.tensor([[0], [4]]), - (0,), - torch.tensor([[0], [4]]), - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (), - torch.tensor([[1, 0], [0, 4]]), - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (0,), - torch.tensor([[1, 0], [0, 4]]), - ), - ( - (True, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (0,), - torch.tensor([[1, 0], [0, -4]]), - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (1,), - torch.tensor([[1, 0], [0, 4]]), - ), - ( - (False, True), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (1,), - torch.tensor([[1, 0], [0, -4]]), - ), - ( - (False, False, False), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (0,), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - ), - ( - (True, False, False), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (0,), - torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]]), - ), - ( - (False, True, True), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (2,), - torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]]), - ), - ( - (True, False, True), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (0, 1), - torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]]), - ), - ( - (True, True, True), - ((1, 1), (1, 1), (1, 1)), - torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), - (0, 1), - torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]]), - ), - ], -) +@pytest.mark.parametrize("x", [ + ((), (), torch.tensor(6), (), torch.tensor(6)), + ((False, False), ((1, 1), (0, 0)), torch.zeros([2, 0]), (0,), torch.zeros([2, 0])), + ((False, False), ((1, 1), (0, 1)), torch.tensor([[0], [4]]), (0,), torch.tensor([[0], [4]])), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (), torch.tensor([[1, 0], [0, 4]])), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0,), torch.tensor([[1, 0], [0, 4]])), + ((True, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0,), torch.tensor([[1, 0], [0, -4]])), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (1,), torch.tensor([[1, 0], [0, 4]])), + ((False, True), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (1,), torch.tensor([[1, 0], [0, -4]])), + ((False, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0,), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]])), + ((True, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0,), torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]])), + ((False, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (2,), torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]])), + ((True, False, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0, 1), torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]])), + ((True, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0, 1), torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]])), +]) def test_reverse(x: ReverseCase) -> None: arrow, edges, tensor, reverse_by, expected = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) @@ -98,30 +27,13 @@ def test_reverse(x: ReverseCase) -> None: assert torch.allclose(result.tensor, expected) -ReverseFailCase = tuple[ - tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str -] +ReverseFailCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str] -@pytest.mark.parametrize( - "x", - [ - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (0, 0), - "Indices must be unique", - ), - ( - (False, False), - ((1, 1), (1, 1)), - torch.tensor([[1, 0], [0, 4]]), - (2,), - "Indices must be within tensor dimensions", - ), - ], -) +@pytest.mark.parametrize("x", [ + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 0), "Indices must be unique"), + ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (2,), "Indices must be within tensor dimensions"), +]) def test_reverse_fail(x: ReverseFailCase) -> None: arrow, edges, tensor, reverse_by, message = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) diff --git a/tests/update_mask_test.py b/tests/update_mask_test.py index fe52f15..84ea25f 100644 --- a/tests/update_mask_test.py +++ b/tests/update_mask_test.py @@ -5,35 +5,33 @@ Initialization = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor] -@pytest.fixture( - params=[ - ( - (), - (), - torch.randn([], dtype=torch.float32), - ), - ( - (False, True), - ((2, 2), (0, 2)), - torch.randn([4, 2], dtype=torch.float32), - ), - ( - (False, True), - ((2, 2), (0, 0)), - torch.randn([4, 0], dtype=torch.float32), - ), - ( - (True, False), - ((2, 2), (3, 1)), - torch.randn([4, 4], dtype=torch.float32), - ), - ( - (False, True, False), - ((1, 1), (2, 2), (1, 1)), - torch.randn([2, 4, 2], dtype=torch.float32), - ), - ] -) +@pytest.fixture(params=[ + ( + (), + (), + torch.randn([], dtype=torch.float32), + ), + ( + (False, True), + ((2, 2), (0, 2)), + torch.randn([4, 2], dtype=torch.float32), + ), + ( + (False, True), + ((2, 2), (0, 0)), + torch.randn([4, 0], dtype=torch.float32), + ), + ( + (True, False), + ((2, 2), (3, 1)), + torch.randn([4, 4], dtype=torch.float32), + ), + ( + (False, True, False), + ((1, 1), (2, 2), (1, 1)), + torch.randn([2, 4, 2], dtype=torch.float32), + ), +]) def x(request: pytest.FixtureRequest) -> Initialization: return request.param From 3e564dff8a8622c8b45f41316de2cb40becd6499 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Tue, 2 Sep 2025 20:29:12 +0800 Subject: [PATCH 7/8] style: Reformat codes again. --- grassmann_tensor/tensor.py | 159 ++++++++++++++++++++++++++++-------- grassmann_tensor/version.py | 1 + tests/arithmetic_test.py | 106 ++++++++++++++---------- tests/attributes_test.py | 28 ++++--- tests/clone_test.py | 4 +- tests/conversion_test.py | 5 +- tests/creation_test.py | 52 +++++++----- tests/import_test.py | 1 + tests/matmul_test.py | 64 +++++++++------ tests/permute_test.py | 122 ++++++++++++++++++++++----- tests/reshape_test.py | 36 ++++++-- tests/reverse_test.py | 130 ++++++++++++++++++++++++----- tests/update_mask_test.py | 56 +++++++------ 13 files changed, 552 insertions(+), 212 deletions(-) diff --git a/grassmann_tensor/tensor.py b/grassmann_tensor/tensor.py index baaec73..7905353 100644 --- a/grassmann_tensor/tensor.py +++ b/grassmann_tensor/tensor.py @@ -64,7 +64,13 @@ def mask(self) -> torch.Tensor: self._mask = self._tensor_mask() return self._mask - def to(self, whatever: torch.device | torch.dtype | str | None = None, *, device: torch.device | None = None, dtype: torch.dtype | None = None) -> GrassmannTensor: + def to( + self, + whatever: torch.device | torch.dtype | str | None = None, + *, + device: torch.device | None = None, + dtype: torch.dtype | None = None, + ) -> GrassmannTensor: """ Copy the tensor to a specified device or copy it to a specified data type. """ @@ -92,14 +98,18 @@ def to(self, whatever: torch.device | torch.dtype | str | None = None, *, device return dataclasses.replace( self, _tensor=self._tensor.to(device=device), - _parity=tuple(p.to(device) for p in self._parity) if self._parity is not None else None, + _parity=tuple(p.to(device) for p in self._parity) + if self._parity is not None + else None, _mask=self._mask.to(device) if self._mask is not None else None, ) case _: return dataclasses.replace( self, _tensor=self._tensor.to(device=device, dtype=dtype), - _parity=tuple(p.to(device=device) for p in self._parity) if self._parity is not None else None, + _parity=tuple(p.to(device=device) for p in self._parity) + if self._parity is not None + else None, _mask=self._mask.to(device=device) if self._mask is not None else None, ) @@ -114,8 +124,12 @@ def permute(self, before_by_after: tuple[int, ...]) -> GrassmannTensor: """ Permute the indices of the Grassmann tensor. """ - assert len(before_by_after) == len(set(before_by_after)), "Permutation indices must be unique." - assert set(before_by_after) == set(range(self.tensor.dim())), "Permutation indices must cover all dimensions." + assert len(before_by_after) == len(set(before_by_after)), ( + "Permutation indices must be unique." + ) + assert set(before_by_after) == set(range(self.tensor.dim())), ( + "Permutation indices must cover all dimensions." + ) arrow = tuple(self.arrow[i] for i in before_by_after) edges = tuple(self.edges[i] for i in before_by_after) @@ -126,10 +140,14 @@ def permute(self, before_by_after: tuple[int, ...]) -> GrassmannTensor: total_parity = functools.reduce( torch.logical_xor, ( - torch.logical_and(self._unsqueeze(parity[i], i, self.tensor.dim()), self._unsqueeze(parity[j], j, self.tensor.dim())) + torch.logical_and( + self._unsqueeze(parity[i], i, self.tensor.dim()), + self._unsqueeze(parity[j], j, self.tensor.dim()), + ) for j in range(self.tensor.dim()) for i in range(0, j) # all 0 <= i < j < dim - if before_by_after[i] > before_by_after[j]), + if before_by_after[i] > before_by_after[j] + ), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(total_parity, -tensor, +tensor) @@ -151,14 +169,20 @@ def reverse(self, indices: tuple[int, ...]) -> GrassmannTensor: This package always applies it to the tensor with arrow as True. """ assert len(set(indices)) == len(indices), f"Indices must be unique. Got {indices}." - assert all(0 <= i < self.tensor.dim() for i in indices), f"Indices must be within tensor dimensions. Got {indices}." + assert all(0 <= i < self.tensor.dim() for i in indices), ( + f"Indices must be within tensor dimensions. Got {indices}." + ) arrow = tuple(self.arrow[i] ^ i in indices for i in range(self.tensor.dim())) tensor = self.tensor total_parity = functools.reduce( torch.logical_xor, - (self._unsqueeze(parity, index, self.tensor.dim()) for index, parity in enumerate(self.parity) if index in indices and self.arrow[index]), + ( + self._unsqueeze(parity, index, self.tensor.dim()) + for index, parity in enumerate(self.parity) + if index in indices and self.arrow[index] + ), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(total_parity, -tensor, +tensor) @@ -169,10 +193,15 @@ def reverse(self, indices: tuple[int, ...]) -> GrassmannTensor: _tensor=tensor, ) - def _reorder_indices(self, edges: tuple[tuple[int, int], ...]) -> tuple[int, int, torch.Tensor, torch.Tensor]: + def _reorder_indices( + self, edges: tuple[tuple[int, int], ...] + ) -> tuple[int, int, torch.Tensor, torch.Tensor]: parity = functools.reduce( torch.logical_xor, - (self._unsqueeze(self._edge_mask(even, odd), index, len(edges)) for index, (even, odd) in enumerate(edges)), + ( + self._unsqueeze(self._edge_mask(even, odd), index, len(edges)) + for index, (even, odd) in enumerate(edges) + ), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) flatten_parity = parity.flatten() @@ -182,7 +211,10 @@ def _reorder_indices(self, edges: tuple[tuple[int, int], ...]) -> tuple[int, int total = functools.reduce( torch.add, - (self._unsqueeze(self._edge_mask(even, odd), index, len(edges)).to(dtype=torch.int16) for index, (even, odd) in enumerate(edges)), + ( + self._unsqueeze(self._edge_mask(even, odd), index, len(edges)).to(dtype=torch.int16) + for index, (even, odd) in enumerate(edges) + ), torch.zeros([], dtype=torch.int16, device=self.tensor.device), ) count = total * (total - 1) @@ -246,7 +278,11 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens cursor_self += 1 continue cursor_new_shape = new_shape[cursor_plan] - total = cursor_new_shape if isinstance(cursor_new_shape, int) else cursor_new_shape[0] + cursor_new_shape[1] + total = ( + cursor_new_shape + if isinstance(cursor_new_shape, int) + else cursor_new_shape[0] + cursor_new_shape[1] + ) # one of total and shape[cursor_self] is not trivial, otherwise it should be handled before if total == self.tensor.shape[cursor_self]: # We do not know whether it is merging or splitting, check more @@ -286,18 +322,29 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens new_cursor_self += 1 # One dimension included, check if we can stop if self_total == total: - even, odd, reorder, sign = self._reorder_indices(self.edges[cursor_self:new_cursor_self]) + even, odd, reorder, sign = self._reorder_indices( + self.edges[cursor_self:new_cursor_self] + ) if isinstance(cursor_new_shape, tuple): if (even, odd) == cursor_new_shape: break else: break # For some reason we cannot stop here, continue to include more dimension, check something before continue - assert self_total <= total, f"Dimension mismatch in merging with edges {self.edges} and new shape {new_shape}." - assert new_cursor_self < self.tensor.dim(), f"New shape exceeds in merging with edges {self.edges} and new shape {new_shape}." + assert self_total <= total, ( + f"Dimension mismatch in merging with edges {self.edges} and new shape {new_shape}." + ) + assert new_cursor_self < self.tensor.dim(), ( + f"New shape exceeds in merging with edges {self.edges} and new shape {new_shape}." + ) # The merging block [cursor_self, new_cursor_self) has been determined arrow.append(self.arrow[cursor_self]) - assert all(self_arrow == arrow[-1] for self_arrow in self.arrow[cursor_self:new_cursor_self]), f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." + assert all( + self_arrow == arrow[-1] + for self_arrow in self.arrow[cursor_self:new_cursor_self] + ), ( + f"Cannot merge edges with different arrows {self.arrow[cursor_self:new_cursor_self]}." + ) edges.append((even, odd)) shape.append(total) merging_sign.append((cursor_plan, sign)) @@ -311,18 +358,28 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens while True: # Try to include more dimension from new_shape new_cursor_new_shape = new_shape[new_cursor_plan] - assert isinstance(new_cursor_new_shape, tuple), f"New shape must be a pair when splitting, got {new_cursor_new_shape}." + assert isinstance(new_cursor_new_shape, tuple), ( + f"New shape must be a pair when splitting, got {new_cursor_new_shape}." + ) plan_total *= new_cursor_new_shape[0] + new_cursor_new_shape[1] new_cursor_plan += 1 # One dimension included, check if we can stop if plan_total == self.tensor.shape[cursor_self]: # new_shape block has been verified to be always tuple[int, int] before - even, odd, reorder, sign = self._reorder_indices(typing.cast(tuple[tuple[int, int], ...], new_shape[cursor_plan:new_cursor_plan])) + even, odd, reorder, sign = self._reorder_indices( + typing.cast( + tuple[tuple[int, int], ...], new_shape[cursor_plan:new_cursor_plan] + ) + ) if (even, odd) == self.edges[cursor_self]: break # For some reason we cannot stop here, continue to include more dimension, check something before continue - assert plan_total <= self.tensor.shape[cursor_self], f"Dimension mismatch in splitting with edges {self.edges} and new shape {new_shape}." - assert new_cursor_plan < len(new_shape), f"New shape exceeds in splitting with edges {self.edges} and new shape {new_shape}." + assert plan_total <= self.tensor.shape[cursor_self], ( + f"Dimension mismatch in splitting with edges {self.edges} and new shape {new_shape}." + ) + assert new_cursor_plan < len(new_shape), ( + f"New shape exceeds in splitting with edges {self.edges} and new shape {new_shape}." + ) # The splitting block [cursor_plan, new_cursor_plan) has been determined for i in range(cursor_plan, new_cursor_plan): # new_shape block has been verified to be always tuple[int, int] in the loop @@ -344,7 +401,11 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens splitting_parity = functools.reduce( torch.logical_xor, - (self._unsqueeze(sign, index, self.tensor.dim()) for index, sign in splitting_sign if self.arrow[index]), + ( + self._unsqueeze(sign, index, self.tensor.dim()) + for index, sign in splitting_sign + if self.arrow[index] + ), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(splitting_parity, -tensor, +tensor) @@ -353,7 +414,11 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens merging_parity = functools.reduce( torch.logical_xor, - (self._unsqueeze(sign, index, tensor.dim()) for index, sign in merging_sign if arrow[index]), + ( + self._unsqueeze(sign, index, tensor.dim()) + for index, sign in merging_sign + if arrow[index] + ), torch.zeros([], dtype=torch.bool, device=self.tensor.device), ) tensor = torch.where(merging_parity, -tensor, +tensor) @@ -382,8 +447,12 @@ def matmul(self, other: GrassmannTensor) -> GrassmannTensor: tensor_b = tensor_b.reshape((-1, (1, 0))) vector_b = True - assert all(odd == 0 for (even, odd) in tensor_a.edges[:-2]), f"All edges except the last two must be pure even. Got {tensor_a.edges[:-2]}." - assert all(odd == 0 for (even, odd) in tensor_b.edges[:-2]), f"All edges except the last two must be pure even. Got {tensor_b.edges[:-2]}." + assert all(odd == 0 for (even, odd) in tensor_a.edges[:-2]), ( + f"All edges except the last two must be pure even. Got {tensor_a.edges[:-2]}." + ) + assert all(odd == 0 for (even, odd) in tensor_b.edges[:-2]), ( + f"All edges except the last two must be pure even. Got {tensor_b.edges[:-2]}." + ) if tensor_a.arrow[-1] is not True: tensor_a = tensor_a.reverse((tensor_a.tensor.dim() - 1,)) @@ -402,7 +471,9 @@ def matmul(self, other: GrassmannTensor) -> GrassmannTensor: candidate_a = tensor_a.edges[i - 2][0] if i >= -broadcast_b: candidate_b = tensor_b.edges[i - 2][0] - assert candidate_a == candidate_b or candidate_a == 1 or candidate_b == 1, f"Cannot broadcast edges {tensor_a.edges[i - 2]} and {tensor_b.edges[i - 2]}." + assert candidate_a == candidate_b or candidate_a == 1 or candidate_b == 1, ( + f"Cannot broadcast edges {tensor_a.edges[i - 2]} and {tensor_b.edges[i - 2]}." + ) edges.append((max(candidate_a, candidate_b), 0)) if not vector_a: arrow.append(tensor_a.arrow[-2]) @@ -423,21 +494,35 @@ def matmul(self, other: GrassmannTensor) -> GrassmannTensor: ) 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()})." - assert len(self._edges) == self._tensor.dim(), f"Edges length ({len(self._edges)}) must match tensor dimensions ({self._tensor.dim()})." + assert len(self._arrow) == self._tensor.dim(), ( + f"Arrow length ({len(self._arrow)}) must match tensor dimensions ({self._tensor.dim()})." + ) + assert len(self._edges) == self._tensor.dim(), ( + f"Edges length ({len(self._edges)}) must match tensor dimensions ({self._tensor.dim()})." + ) for dim, (even, odd) in zip(self._tensor.shape, self._edges): - assert even >= 0 and odd >= 0 and dim == even + odd, f"Dimension {dim} must equal sum of even ({even}) and odd ({odd}) parts, and both must be non-negative." + assert even >= 0 and odd >= 0 and dim == even + odd, ( + f"Dimension {dim} must equal sum of even ({even}) and odd ({odd}) parts, and both must be non-negative." + ) def _unsqueeze(self, tensor: torch.Tensor, index: int, dim: int) -> torch.Tensor: return tensor.view([-1 if i == index else 1 for i in range(dim)]) def _edge_mask(self, even: int, odd: int) -> torch.Tensor: - return torch.cat([torch.zeros(even, dtype=torch.bool, device=self.tensor.device), torch.ones(odd, dtype=torch.bool, device=self.tensor.device)]) + return torch.cat( + [ + torch.zeros(even, dtype=torch.bool, device=self.tensor.device), + torch.ones(odd, dtype=torch.bool, device=self.tensor.device), + ] + ) def _tensor_mask(self) -> torch.Tensor: return functools.reduce( torch.logical_xor, - (self._unsqueeze(parity, index, self._tensor.dim()) for index, parity in enumerate(self.parity)), + ( + self._unsqueeze(parity, index, self._tensor.dim()) + for index, parity in enumerate(self.parity) + ), torch.zeros_like(self._tensor, dtype=torch.bool), ) @@ -445,8 +530,12 @@ def _validate_edge_compatibility(self, other: GrassmannTensor) -> None: """ Validate that the edges of two ParityTensor instances are compatible for arithmetic operations. """ - assert self._arrow == other.arrow, f"Arrows must match for arithmetic operations. Got {self._arrow} and {other.arrow}." - assert self._edges == other.edges, f"Edges must match for arithmetic operations. Got {self._edges} and {other.edges}." + assert self._arrow == other.arrow, ( + f"Arrows must match for arithmetic operations. Got {self._arrow} and {other.arrow}." + ) + assert self._edges == other.edges, ( + f"Edges must match for arithmetic operations. Got {self._edges} and {other.edges}." + ) def __pos__(self) -> GrassmannTensor: return dataclasses.replace( @@ -639,7 +728,9 @@ def clone(self) -> GrassmannTensor: return dataclasses.replace( self, _tensor=self._tensor.clone(), - _parity=tuple(parity.clone() for parity in self._parity) if self._parity is not None else None, + _parity=tuple(parity.clone() for parity in self._parity) + if self._parity is not None + else None, _mask=self._mask.clone() if self._mask is not None else None, ) diff --git a/grassmann_tensor/version.py b/grassmann_tensor/version.py index 2d474b2..0c775ec 100644 --- a/grassmann_tensor/version.py +++ b/grassmann_tensor/version.py @@ -11,6 +11,7 @@ except ModuleNotFoundError: try: import importlib.metadata + __version__ = importlib.metadata.version("parity") except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" diff --git a/tests/arithmetic_test.py b/tests/arithmetic_test.py index 8f56b59..c5f758c 100644 --- a/tests/arithmetic_test.py +++ b/tests/arithmetic_test.py @@ -5,52 +5,69 @@ from grassmann_tensor import GrassmannTensor -@pytest.fixture(params=[ - ( - GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - ), - ( - GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), - GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), - ), - ( - GrassmannTensor((True, True, False, False), ((1, 2), (2, 2), (1, 1), (3, 1)), torch.randn([3, 4, 2, 4])), - GrassmannTensor((True, True, False, False), ((1, 2), (2, 2), (1, 1), (3, 1)), torch.randn([3, 4, 2, 4])), - ), -]) +@pytest.fixture( + params=[ + ( + GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + ), + ( + GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), + GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), + ), + ( + GrassmannTensor( + (True, True, False, False), + ((1, 2), (2, 2), (1, 1), (3, 1)), + torch.randn([3, 4, 2, 4]), + ), + GrassmannTensor( + (True, True, False, False), + ((1, 2), (2, 2), (1, 1), (3, 1)), + torch.randn([3, 4, 2, 4]), + ), + ), + ] +) def tensors(request: pytest.FixtureRequest) -> tuple[GrassmannTensor, GrassmannTensor]: return request.param -@pytest.fixture(params=[ - ( - GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - GrassmannTensor((False,), ((2, 2),), torch.randn([4])), - ), - ( - GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), - GrassmannTensor((True, False, True), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), - ), - ( - GrassmannTensor((True, True, False), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), - GrassmannTensor((True, True, False, False), ((3, 2), (2, 2), (1, 1), (3, 1)), torch.randn([5, 4, 2, 4])), - ), -]) +@pytest.fixture( + params=[ + ( + GrassmannTensor((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + GrassmannTensor((False,), ((2, 2),), torch.randn([4])), + ), + ( + GrassmannTensor((True, False, True), ((1, 1), (2, 2), (3, 1)), torch.randn([2, 4, 4])), + GrassmannTensor((True, False, True), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), + ), + ( + GrassmannTensor((True, True, False), ((1, 2), (2, 2), (3, 1)), torch.randn([3, 4, 4])), + GrassmannTensor( + (True, True, False, False), + ((3, 2), (2, 2), (1, 1), (3, 1)), + torch.randn([5, 4, 2, 4]), + ), + ), + ] +) def mismatch_tensors(request: pytest.FixtureRequest) -> tuple[GrassmannTensor, GrassmannTensor]: return request.param -@pytest.fixture(params=[ - torch.randn([]), - torch.randn([]).item(), -]) +@pytest.fixture( + params=[ + torch.randn([]), + torch.randn([]).item(), + ] +) def scalar(request: pytest.FixtureRequest) -> torch.Tensor | float: return request.param class FakeTensor: - def __init__(self) -> None: pass @@ -106,15 +123,20 @@ def __rtruediv__(self, other: typing.Any) -> FakeTensor: @pytest.mark.parametrize( "unsupported_type", [ - "string", #string - None, #NoneType - {"key", "value"}, #dict - [1, 2, 3], #list - {1, 2}, #set - object(), #arbitrary object - FakeTensor(), #an ill defined tensor-like object - ]) -def test_arithmetic(unsupported_type: typing.Any, tensors: tuple[GrassmannTensor, GrassmannTensor], scalar: torch.Tensor | float) -> None: + "string", # string + None, # NoneType + {"key", "value"}, # dict + [1, 2, 3], # list + {1, 2}, # set + object(), # arbitrary object + FakeTensor(), # an ill defined tensor-like object + ], +) +def test_arithmetic( + unsupported_type: typing.Any, + tensors: tuple[GrassmannTensor, GrassmannTensor], + scalar: torch.Tensor | float, +) -> None: tensor_a, tensor_b = tensors # Test __pos__ method. diff --git a/tests/attributes_test.py b/tests/attributes_test.py index 7a8e57c..9a12a82 100644 --- a/tests/attributes_test.py +++ b/tests/attributes_test.py @@ -5,17 +5,19 @@ Initialization = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor] -@pytest.fixture(params=[ - ((False, False), ((2, 2), (2, 2)), torch.randn([4, 4])), - ((False, True), ((2, 2), (1, 3)), torch.randn([4, 4])), - ((False, True), ((2, 0), (1, 3)), torch.randn([2, 4])), - ((True, False), ((0, 2), (1, 3)), torch.randn([2, 4])), - ((True, False), ((0, 0), (1, 3)), torch.randn([0, 4])), - ((True,), ((2, 0),), torch.randn([2])), - ((False,), ((0, 2),), torch.randn([2])), - ((), (), torch.randn([])), - ((False, False, True), ((2, 2), (1, 3), (4, 0)), torch.randn([4, 4, 4])), -]) +@pytest.fixture( + params=[ + ((False, False), ((2, 2), (2, 2)), torch.randn([4, 4])), + ((False, True), ((2, 2), (1, 3)), torch.randn([4, 4])), + ((False, True), ((2, 0), (1, 3)), torch.randn([2, 4])), + ((True, False), ((0, 2), (1, 3)), torch.randn([2, 4])), + ((True, False), ((0, 0), (1, 3)), torch.randn([0, 4])), + ((True,), ((2, 0),), torch.randn([2])), + ((False,), ((0, 2),), torch.randn([2])), + ((), (), torch.randn([])), + ((False, False, True), ((2, 2), (1, 3), (4, 0)), torch.randn([4, 4, 4])), + ] +) def x(request: pytest.FixtureRequest) -> Initialization: return request.param @@ -50,7 +52,9 @@ def test_mask(x: Initialization) -> None: tensor = GrassmannTensor(*x) assert tensor.mask.shape == tensor.tensor.shape assert tensor.mask.dtype == torch.bool - for indices in zip(*torch.unravel_index(torch.arange(tensor.tensor.numel()), tensor.tensor.shape)): + for indices in zip( + *torch.unravel_index(torch.arange(tensor.tensor.numel()), tensor.tensor.shape) + ): mask = tensor.mask[indices] expect = False for rank, parity in enumerate(tensor.parity): diff --git a/tests/clone_test.py b/tests/clone_test.py index 6a67bcc..28d498a 100644 --- a/tests/clone_test.py +++ b/tests/clone_test.py @@ -37,7 +37,9 @@ def test_clone( if parity: assert cloned_tensor._parity is not None assert original_tensor._parity is not None - assert all(torch.equal(c, o) for c, o in zip(cloned_tensor._parity, original_tensor._parity)) + assert all( + torch.equal(c, o) for c, o in zip(cloned_tensor._parity, original_tensor._parity) + ) else: assert cloned_tensor._parity is original_tensor._parity if mask: diff --git a/tests/conversion_test.py b/tests/conversion_test.py index 0151dc8..29b1a59 100644 --- a/tests/conversion_test.py +++ b/tests/conversion_test.py @@ -47,7 +47,10 @@ def test_conversion( assert y.arrow == x.arrow assert y.edges == x.edges assert y.tensor.dtype == torch.complex128 if dtype_arg != "none" else torch.float32 - assert y.tensor.device.type == (torch.device(device_str) if device_arg != "none" else torch.device("cpu:0")).type + assert ( + y.tensor.device.type + == (torch.device(device_str) if device_arg != "none" else torch.device("cpu:0")).type + ) assert torch.allclose(y.tensor, x.tensor.to(dtype=y.tensor.dtype, device=y.tensor.device)) diff --git a/tests/creation_test.py b/tests/creation_test.py index e4eb406..1185293 100644 --- a/tests/creation_test.py +++ b/tests/creation_test.py @@ -5,40 +5,52 @@ Initialization = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor] -@pytest.mark.parametrize("x", [ - ((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), - ((True, False), ((2, 2), (3, 1)), torch.randn([4, 4])), - ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), -]) +@pytest.mark.parametrize( + "x", + [ + ((False, False), ((2, 2), (1, 3)), torch.randn([4, 4])), + ((True, False), ((2, 2), (3, 1)), torch.randn([4, 4])), + ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), + ], +) def test_creation_success(x: Initialization) -> None: GrassmannTensor(*x) -@pytest.mark.parametrize("x", [ - ((False,), ((2, 2), (1, 3)), torch.randn([4, 4])), - ((True, False, True), ((2, 2), (3, 1)), torch.randn([4, 4])), - ((False, True), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), -]) +@pytest.mark.parametrize( + "x", + [ + ((False,), ((2, 2), (1, 3)), torch.randn([4, 4])), + ((True, False, True), ((2, 2), (3, 1)), torch.randn([4, 4])), + ((False, True), ((1, 1), (2, 2), (1, 1)), torch.randn([2, 4, 2])), + ], +) def test_creation_invalid_arrow(x: Initialization) -> None: with pytest.raises(AssertionError, match="Arrow length"): GrassmannTensor(*x) -@pytest.mark.parametrize("x", [ - ((False, False), ((2, 2),), torch.randn([4, 4])), - ((True, False), ((2, 2), (1, 1), (3, 1)), torch.randn([4, 4])), - ((False, True, False), ((1, 1), (1, 1)), torch.randn([2, 4, 2])), -]) +@pytest.mark.parametrize( + "x", + [ + ((False, False), ((2, 2),), torch.randn([4, 4])), + ((True, False), ((2, 2), (1, 1), (3, 1)), torch.randn([4, 4])), + ((False, True, False), ((1, 1), (1, 1)), torch.randn([2, 4, 2])), + ], +) def test_creation_invalid_edges(x: Initialization) -> None: with pytest.raises(AssertionError, match="Edges length"): GrassmannTensor(*x) -@pytest.mark.parametrize("x", [ - ((False, False), ((2, 2), (1, 3)), torch.randn([4, 2])), - ((True, False), ((2, 2), (3, 1)), torch.randn([2, 4])), - ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([4, 4, 2])), -]) +@pytest.mark.parametrize( + "x", + [ + ((False, False), ((2, 2), (1, 3)), torch.randn([4, 2])), + ((True, False), ((2, 2), (3, 1)), torch.randn([2, 4])), + ((False, True, False), ((1, 1), (2, 2), (1, 1)), torch.randn([4, 4, 2])), + ], +) def test_creation_invalid_shape(x: Initialization) -> None: with pytest.raises(AssertionError, match="must equal sum of"): GrassmannTensor(*x) diff --git a/tests/import_test.py b/tests/import_test.py index 5a91002..20b0985 100644 --- a/tests/import_test.py +++ b/tests/import_test.py @@ -1,3 +1,4 @@ def test_import() -> None: from grassmann_tensor import GrassmannTensor + assert isinstance(GrassmannTensor, type) diff --git a/tests/matmul_test.py b/tests/matmul_test.py index d7eb048..f00316e 100644 --- a/tests/matmul_test.py +++ b/tests/matmul_test.py @@ -5,23 +5,30 @@ MatmulMatrixCase = tuple[bool, bool, tuple[int, int], tuple[int, int], tuple[int, int]] -@pytest.mark.parametrize("x", [ - (False, False, (1, 1), (1, 1), (1, 1)), - (False, True, (1, 1), (1, 1), (1, 1)), - (True, False, (1, 1), (1, 1), (1, 1)), - (True, True, (1, 1), (1, 1), (1, 1)), - (False, False, (2, 2), (2, 2), (2, 2)), - (False, True, (2, 2), (2, 2), (2, 2)), - (True, False, (2, 2), (2, 2), (2, 2)), - (True, True, (2, 2), (2, 2), (2, 2)), -]) +@pytest.mark.parametrize( + "x", + [ + (False, False, (1, 1), (1, 1), (1, 1)), + (False, True, (1, 1), (1, 1), (1, 1)), + (True, False, (1, 1), (1, 1), (1, 1)), + (True, True, (1, 1), (1, 1), (1, 1)), + (False, False, (2, 2), (2, 2), (2, 2)), + (False, True, (2, 2), (2, 2), (2, 2)), + (True, False, (2, 2), (2, 2), (2, 2)), + (True, True, (2, 2), (2, 2), (2, 2)), + ], +) def test_matmul_matrix_tf(x: MatmulMatrixCase) -> None: arrow_a, arrow_b, edge_a, edge_common, edge_b = x dim_a = sum(edge_a) dim_common = sum(edge_common) dim_b = sum(edge_b) - a = GrassmannTensor((arrow_a, True), (edge_a, edge_common), torch.randn([dim_a, dim_common])).update_mask() - b = GrassmannTensor((False, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b])).update_mask() + a = GrassmannTensor( + (arrow_a, True), (edge_a, edge_common), torch.randn([dim_a, dim_common]) + ).update_mask() + b = GrassmannTensor( + (False, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b]) + ).update_mask() c = a.matmul(b) expected = a.tensor.matmul(b.tensor) assert c.arrow == (arrow_a, arrow_b) @@ -29,26 +36,33 @@ def test_matmul_matrix_tf(x: MatmulMatrixCase) -> None: assert torch.allclose(c.tensor, expected) -@pytest.mark.parametrize("x", [ - (False, False, (1, 1), (1, 1), (1, 1)), - (False, True, (1, 1), (1, 1), (1, 1)), - (True, False, (1, 1), (1, 1), (1, 1)), - (True, True, (1, 1), (1, 1), (1, 1)), - (False, False, (2, 2), (2, 2), (2, 2)), - (False, True, (2, 2), (2, 2), (2, 2)), - (True, False, (2, 2), (2, 2), (2, 2)), - (True, True, (2, 2), (2, 2), (2, 2)), -]) +@pytest.mark.parametrize( + "x", + [ + (False, False, (1, 1), (1, 1), (1, 1)), + (False, True, (1, 1), (1, 1), (1, 1)), + (True, False, (1, 1), (1, 1), (1, 1)), + (True, True, (1, 1), (1, 1), (1, 1)), + (False, False, (2, 2), (2, 2), (2, 2)), + (False, True, (2, 2), (2, 2), (2, 2)), + (True, False, (2, 2), (2, 2), (2, 2)), + (True, True, (2, 2), (2, 2), (2, 2)), + ], +) def test_matmul_matrix_ft(x: MatmulMatrixCase) -> None: arrow_a, arrow_b, edge_a, edge_common, edge_b = x dim_a = sum(edge_a) dim_common = sum(edge_common) dim_b = sum(edge_b) - a = GrassmannTensor((arrow_a, False), (edge_a, edge_common), torch.randn([dim_a, dim_common])).update_mask() - b = GrassmannTensor((True, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b])).update_mask() + a = GrassmannTensor( + (arrow_a, False), (edge_a, edge_common), torch.randn([dim_a, dim_common]) + ).update_mask() + b = GrassmannTensor( + (True, arrow_b), (edge_common, edge_b), torch.randn([dim_common, dim_b]) + ).update_mask() c = a.matmul(b) expected = a.tensor.matmul(b.tensor) - expected[edge_a[0]:, edge_b[0]:] *= -1 + expected[edge_a[0] :, edge_b[0] :] *= -1 assert c.arrow == (arrow_a, arrow_b) assert c.edges == (edge_a, edge_b) assert torch.allclose(c.tensor, expected) diff --git a/tests/permute_test.py b/tests/permute_test.py index f7197f7..12f6949 100644 --- a/tests/permute_test.py +++ b/tests/permute_test.py @@ -2,21 +2,68 @@ import torch from grassmann_tensor import GrassmannTensor -PermuteCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor] +PermuteCase = tuple[ + tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor +] -@pytest.mark.parametrize("x", [ - ((), (), torch.tensor(6), (), torch.tensor(6)), - ((False,), ((1, 1),), torch.tensor([1, 2]), (0,), torch.tensor([1, 2])), - ((False, True), ((1, 1), (0, 0)), torch.zeros([2, 0]), (1, 0), torch.zeros([0, 2])), - ((False, True), ((1, 1), (0, 1)), torch.tensor([[0], [4]]), (1, 0), torch.tensor([[0, -4]])), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 1), torch.tensor([[1, 0], [0, 4]])), - ((True, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (1, 0), torch.tensor([[1, 0], [0, -4]])), - ((False, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0, 2, 1), torch.tensor([[[1, 0], [0, -2]], [[0, 4], [3, 0]]])), - ((True, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (1, 0, 2), torch.tensor([[[1, 0], [0, 3]], [[0, 2], [-4, 0]]])), - ((True, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (2, 1, 0), torch.tensor([[[1, 0], [0, -4]], [[0, -3], [-2, 0]]])), - ((False, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (2, 0, 1), torch.tensor([[[1, 0], [0, 4]], [[0, -2], [-3, 0]]])), -]) +@pytest.mark.parametrize( + "x", + [ + ((), (), torch.tensor(6), (), torch.tensor(6)), + ((False,), ((1, 1),), torch.tensor([1, 2]), (0,), torch.tensor([1, 2])), + ((False, True), ((1, 1), (0, 0)), torch.zeros([2, 0]), (1, 0), torch.zeros([0, 2])), + ( + (False, True), + ((1, 1), (0, 1)), + torch.tensor([[0], [4]]), + (1, 0), + torch.tensor([[0, -4]]), + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (0, 1), + torch.tensor([[1, 0], [0, 4]]), + ), + ( + (True, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (1, 0), + torch.tensor([[1, 0], [0, -4]]), + ), + ( + (False, True, True), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (0, 2, 1), + torch.tensor([[[1, 0], [0, -2]], [[0, 4], [3, 0]]]), + ), + ( + (True, True, True), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (1, 0, 2), + torch.tensor([[[1, 0], [0, 3]], [[0, 2], [-4, 0]]]), + ), + ( + (True, False, False), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (2, 1, 0), + torch.tensor([[[1, 0], [0, -4]], [[0, -3], [-2, 0]]]), + ), + ( + (False, False, False), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (2, 0, 1), + torch.tensor([[[1, 0], [0, 4]], [[0, -2], [-3, 0]]]), + ), + ], +) def test_permute(x: PermuteCase) -> None: arrow, edges, tensor, before_by_after, expected = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) @@ -24,14 +71,37 @@ def test_permute(x: PermuteCase) -> None: assert torch.allclose(result.tensor, expected) -PermuteFailCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str] +PermuteFailCase = tuple[ + tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str +] -@pytest.mark.parametrize("x", [ - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 0), "Permutation indices must be unique"), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (2, 0), "Permutation indices must cover all dimensions"), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 0, 1), "Permutation indices must be unique"), -]) +@pytest.mark.parametrize( + "x", + [ + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (0, 0), + "Permutation indices must be unique", + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (2, 0), + "Permutation indices must cover all dimensions", + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (0, 0, 1), + "Permutation indices must be unique", + ), + ], +) def test_permute_fail(x: PermuteFailCase) -> None: arrow, edges, tensor, before_by_after, message = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) @@ -41,13 +111,17 @@ def test_permute_fail(x: PermuteFailCase) -> None: def test_permute_high_order() -> None: edge = (2, 2) - a = GrassmannTensor((False, False, False, False, False, False), (edge, edge, edge, edge, edge, edge), torch.randn(4, 4, 4, 4, 4, 4)).update_mask() + a = GrassmannTensor( + (False, False, False, False, False, False), + (edge, edge, edge, edge, edge, edge), + torch.randn(4, 4, 4, 4, 4, 4), + ).update_mask() # a[i, j, k, l, m, n] -> b[l, j, i, n, k, m] b = a.permute((3, 1, 0, 5, 2, 4)) for i in range(4): for j in range(4): for k in range(4): - for l in range(4): + for l in range(4): # noqa: E741 for m in range(4): for n in range(4): p = [bool(x & 2) for x in (i, j, k, l, m, n)] @@ -57,7 +131,11 @@ def test_permute_high_order() -> None: # (l) (i j k) m n # l (j) (i) k m n # l j i (n) (k m) - sign = (p[3] & (p[0] ^ p[1] ^ p[2])) ^ (p[1] & p[0]) ^ (p[5] & (p[2] ^ p[4])) + sign = ( + (p[3] & (p[0] ^ p[1] ^ p[2])) + ^ (p[1] & p[0]) + ^ (p[5] & (p[2] ^ p[4])) + ) if sign: assert b.tensor[l, j, i, n, k, m] == -a.tensor[i, j, k, l, m, n] else: diff --git a/tests/reshape_test.py b/tests/reshape_test.py index f79517b..52c49a9 100644 --- a/tests/reshape_test.py +++ b/tests/reshape_test.py @@ -4,21 +4,33 @@ from grassmann_tensor import GrassmannTensor -@pytest.mark.parametrize("arrow", [(i, j, k, l, m) for i in [False, True] for j in [False, True] for k in [False, True] for l in [False, True] for m in [False, True]]) +@pytest.mark.parametrize( + "arrow", + [ + (i, j, k, l, m) + for i in [False, True] + for j in [False, True] + for k in [False, True] + for l in [False, True] # noqa: E741 + for m in [False, True] + ], +) @pytest.mark.parametrize("plan_range", [(i, j) for i in range(5) for j in range(5) if j > i]) def test_reshape_consistency(arrow: tuple[bool, ...], plan_range: tuple[int, int]) -> None: - l, h = plan_range + l, h = plan_range # noqa: E741 if not all(arrow[l:h]) and any(arrow[l:h]): pytest.skip("Invalid reshape plan for the given arrow configuration.") edge = (2, 2) a = GrassmannTensor(arrow, (edge, edge, edge, edge, edge), torch.randn([4, 4, 4, 4, 4])) - plan = tuple([-1] * l + [4**(h - l)] + [-1] * (5 - h)) + plan = tuple([-1] * l + [4 ** (h - l)] + [-1] * (5 - h)) b = a.reshape(plan) c = b.reshape(a.edges) assert torch.allclose(a.tensor, c.tensor) -def insert_trivial_between_elements(input_list: list[tuple[int, int] | int], p: float) -> list[tuple[int, int] | int]: +def insert_trivial_between_elements( + input_list: list[tuple[int, int] | int], p: float +) -> list[tuple[int, int] | int]: result: list[tuple[int, int] | int] = [] for i in input_list: if random.random() < p: @@ -27,15 +39,25 @@ def insert_trivial_between_elements(input_list: list[tuple[int, int] | int], p: return result -@pytest.mark.parametrize("arrow", [(i, j, k, l, m) for i in [False, True] for j in [False, True] for k in [False, True] for l in [False, True] for m in [False, True]]) +@pytest.mark.parametrize( + "arrow", + [ + (i, j, k, l, m) + for i in [False, True] + for j in [False, True] + for k in [False, True] + for l in [False, True] # noqa: E741 + for m in [False, True] + ], +) @pytest.mark.parametrize("plan_range", [(i, j) for i in range(5) for j in range(5) if j > i]) def test_reshape_trivial_edges(arrow: tuple[bool, ...], plan_range: tuple[int, int]) -> None: - l, h = plan_range + l, h = plan_range # noqa: E741 if not all(arrow[l:h]) and any(arrow[l:h]): pytest.skip("Invalid reshape plan for the given arrow configuration.") edge = (2, 2) a = GrassmannTensor(arrow, (edge, edge, edge, edge, edge), torch.randn([4, 4, 4, 4, 4])) - plan = tuple(insert_trivial_between_elements([-1] * l + [4**(h - l)] + [-1] * (5 - h), 0.5)) + plan = tuple(insert_trivial_between_elements([-1] * l + [4 ** (h - l)] + [-1] * (5 - h), 0.5)) b = a.reshape(plan) c = b.reshape(a.edges) assert a.edges == c.edges diff --git a/tests/reverse_test.py b/tests/reverse_test.py index 59cb9f8..ead3806 100644 --- a/tests/reverse_test.py +++ b/tests/reverse_test.py @@ -2,24 +2,95 @@ import torch from grassmann_tensor.tensor import GrassmannTensor -ReverseCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor] +ReverseCase = tuple[ + tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], torch.Tensor +] -@pytest.mark.parametrize("x", [ - ((), (), torch.tensor(6), (), torch.tensor(6)), - ((False, False), ((1, 1), (0, 0)), torch.zeros([2, 0]), (0,), torch.zeros([2, 0])), - ((False, False), ((1, 1), (0, 1)), torch.tensor([[0], [4]]), (0,), torch.tensor([[0], [4]])), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (), torch.tensor([[1, 0], [0, 4]])), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0,), torch.tensor([[1, 0], [0, 4]])), - ((True, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0,), torch.tensor([[1, 0], [0, -4]])), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (1,), torch.tensor([[1, 0], [0, 4]])), - ((False, True), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (1,), torch.tensor([[1, 0], [0, -4]])), - ((False, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0,), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]])), - ((True, False, False), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0,), torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]])), - ((False, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (2,), torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]])), - ((True, False, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0, 1), torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]])), - ((True, True, True), ((1, 1), (1, 1), (1, 1)), torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), (0, 1), torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]])), -]) +@pytest.mark.parametrize( + "x", + [ + ((), (), torch.tensor(6), (), torch.tensor(6)), + ((False, False), ((1, 1), (0, 0)), torch.zeros([2, 0]), (0,), torch.zeros([2, 0])), + ( + (False, False), + ((1, 1), (0, 1)), + torch.tensor([[0], [4]]), + (0,), + torch.tensor([[0], [4]]), + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (), + torch.tensor([[1, 0], [0, 4]]), + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (0,), + torch.tensor([[1, 0], [0, 4]]), + ), + ( + (True, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (0,), + torch.tensor([[1, 0], [0, -4]]), + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (1,), + torch.tensor([[1, 0], [0, 4]]), + ), + ( + (False, True), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (1,), + torch.tensor([[1, 0], [0, -4]]), + ), + ( + (False, False, False), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (0,), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + ), + ( + (True, False, False), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (0,), + torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]]), + ), + ( + (False, True, True), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (2,), + torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]]), + ), + ( + (True, False, True), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (0, 1), + torch.tensor([[[1, 0], [0, 2]], [[0, -3], [-4, 0]]]), + ), + ( + (True, True, True), + ((1, 1), (1, 1), (1, 1)), + torch.tensor([[[1, 0], [0, 2]], [[0, 3], [4, 0]]]), + (0, 1), + torch.tensor([[[1, 0], [0, -2]], [[0, -3], [4, 0]]]), + ), + ], +) def test_reverse(x: ReverseCase) -> None: arrow, edges, tensor, reverse_by, expected = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) @@ -27,13 +98,30 @@ def test_reverse(x: ReverseCase) -> None: assert torch.allclose(result.tensor, expected) -ReverseFailCase = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str] +ReverseFailCase = tuple[ + tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor, tuple[int, ...], str +] -@pytest.mark.parametrize("x", [ - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (0, 0), "Indices must be unique"), - ((False, False), ((1, 1), (1, 1)), torch.tensor([[1, 0], [0, 4]]), (2,), "Indices must be within tensor dimensions"), -]) +@pytest.mark.parametrize( + "x", + [ + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (0, 0), + "Indices must be unique", + ), + ( + (False, False), + ((1, 1), (1, 1)), + torch.tensor([[1, 0], [0, 4]]), + (2,), + "Indices must be within tensor dimensions", + ), + ], +) def test_reverse_fail(x: ReverseFailCase) -> None: arrow, edges, tensor, reverse_by, message = x grassmann_tensor = GrassmannTensor(arrow, edges, tensor) diff --git a/tests/update_mask_test.py b/tests/update_mask_test.py index 84ea25f..fe52f15 100644 --- a/tests/update_mask_test.py +++ b/tests/update_mask_test.py @@ -5,33 +5,35 @@ Initialization = tuple[tuple[bool, ...], tuple[tuple[int, int], ...], torch.Tensor] -@pytest.fixture(params=[ - ( - (), - (), - torch.randn([], dtype=torch.float32), - ), - ( - (False, True), - ((2, 2), (0, 2)), - torch.randn([4, 2], dtype=torch.float32), - ), - ( - (False, True), - ((2, 2), (0, 0)), - torch.randn([4, 0], dtype=torch.float32), - ), - ( - (True, False), - ((2, 2), (3, 1)), - torch.randn([4, 4], dtype=torch.float32), - ), - ( - (False, True, False), - ((1, 1), (2, 2), (1, 1)), - torch.randn([2, 4, 2], dtype=torch.float32), - ), -]) +@pytest.fixture( + params=[ + ( + (), + (), + torch.randn([], dtype=torch.float32), + ), + ( + (False, True), + ((2, 2), (0, 2)), + torch.randn([4, 2], dtype=torch.float32), + ), + ( + (False, True), + ((2, 2), (0, 0)), + torch.randn([4, 0], dtype=torch.float32), + ), + ( + (True, False), + ((2, 2), (3, 1)), + torch.randn([4, 4], dtype=torch.float32), + ), + ( + (False, True, False), + ((1, 1), (2, 2), (1, 1)), + torch.randn([2, 4, 2], dtype=torch.float32), + ), + ] +) def x(request: pytest.FixtureRequest) -> Initialization: return request.param From a215d07c7e5c381c82eb786117eaec55b666fbd7 Mon Sep 17 00:00:00 2001 From: Hao Zhang Date: Tue, 2 Sep 2025 20:30:27 +0800 Subject: [PATCH 8/8] chore: Remove useless pylint annotation. --- grassmann_tensor/tensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/grassmann_tensor/tensor.py b/grassmann_tensor/tensor.py index 7905353..16160ee 100644 --- a/grassmann_tensor/tensor.py +++ b/grassmann_tensor/tensor.py @@ -244,8 +244,6 @@ def reshape(self, new_shape: tuple[int | tuple[int, int], ...]) -> GrassmannTens # 5. Apply the sign for merging # 6. Reorder the indices for merging - # pylint: disable=too-many-branches, too-many-locals, too-many-statements, too-many-nested-blocks - arrow: list[bool] = [] edges: list[tuple[int, int]] = [] shape: list[int] = []