From e468f96a25b2bc185efd93ef0e4af74a19ea8ed0 Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Fri, 5 Apr 2024 15:51:30 +0200 Subject: [PATCH 1/7] add promote_ope function (#154) --- pyqtorch/utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index 43cbd3e7..a7b3e4ad 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -7,6 +7,7 @@ from torch import Tensor from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, DEFAULT_REAL_DTYPE +from pyqtorch.primitive import I State = Tensor Operator = Tensor @@ -142,3 +143,31 @@ def density_mat(state: Tensor) -> Tensor: state = torch.permute(state, batch_first_perm).reshape(batch_size, 2**n_qubits) undo_perm = (1, 2, 0) return torch.permute(torch.einsum("bi,bj->bij", (state, state.conj())), undo_perm) + + +def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: + """ + Promotes `operator` to the size of the circuit. + + Args: + operator (Tensor): The operator tensor to be promoted. + target (int): The target qubit index. + n_qubits (int): Number of qubits in the circuit. + + Returns: + Tensor: The promoted operator tensor. + + Raises: + ValueError: If `target` is not within the range of qubits. + """ + if target > n_qubits: + raise ValueError("The target must be a register qubit") + + qubits_support = torch.arange(0, n_qubits) + for support in qubits_support: + if target > support: + operator = torch.kron(I(support).unitary(), operator.contiguous()) + # Add .contiguous() because kron does not support the transpose (dagger) + elif target < support: + operator = torch.kron(operator.contiguous(), I(support).unitary()) + return operator \ No newline at end of file From 6606021c0ec812f5a66c66e37081d585573787de Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Fri, 5 Apr 2024 17:27:55 +0200 Subject: [PATCH 2/7] Modify the target range value (#154) --- pyqtorch/utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index a7b3e4ad..a9e0a3a7 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -147,27 +147,29 @@ def density_mat(state: Tensor) -> Tensor: def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: """ - Promotes `operator` to the size of the circuit. + Promotes `operator` to the size of the circuit (number of qubits and batch). + Targeting the first qubit implies target = 0, so target > n_qubits - 1. Args: operator (Tensor): The operator tensor to be promoted. - target (int): The target qubit index. + target (int): The index of the target qubit to which the operator is applied. + Targeting the first qubit implies target = 0, so target > n_qubits - 1. n_qubits (int): Number of qubits in the circuit. Returns: Tensor: The promoted operator tensor. Raises: - ValueError: If `target` is not within the range of qubits. + ValueError: If `target` is outside the valid range of qubits. """ - if target > n_qubits: - raise ValueError("The target must be a register qubit") + if target > n_qubits - 1 : + raise ValueError("The target must be a valid qubit index within the circuit's range.") qubits_support = torch.arange(0, n_qubits) for support in qubits_support: if target > support: operator = torch.kron(I(support).unitary(), operator.contiguous()) - # Add .contiguous() because kron does not support the transpose (dagger) + # Add.contiguous() because kron does not support the transpose (dagger) elif target < support: operator = torch.kron(operator.contiguous(), I(support).unitary()) return operator \ No newline at end of file From 639bbdb1d69ce98132baf1395636511bd8f4d489 Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Tue, 9 Apr 2024 10:33:51 +0200 Subject: [PATCH 3/7] Add test_promote func (#155) --- pyqtorch/apply.py | 10 ++++------ pyqtorch/primitive.py | 12 +++++++----- pyqtorch/utils.py | 4 ++-- tests/test_digital.py | 13 ++++++++++++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pyqtorch/apply.py b/pyqtorch/apply.py index 859f7b82..f7f5b151 100644 --- a/pyqtorch/apply.py +++ b/pyqtorch/apply.py @@ -5,20 +5,18 @@ from numpy import array from numpy.typing import NDArray -from torch import einsum - -from pyqtorch.utils import Operator, State +from torch import Tensor, einsum ABC_ARRAY: NDArray = array(list(ABC)) def apply_operator( - state: State, - operator: Operator, + state: Tensor, + operator: Tensor, qubits: Tuple[int, ...] | list[int], n_qubits: int = None, batch_size: int = None, -) -> State: +) -> Tensor: """Applies an operator, i.e. a single tensor of shape [2, 2, ...], on a given state of shape [2 for _ in range(n_qubits)] for a given set of (target and control) qubits. diff --git a/pyqtorch/primitive.py b/pyqtorch/primitive.py index bac9e4ba..2af104bb 100644 --- a/pyqtorch/primitive.py +++ b/pyqtorch/primitive.py @@ -7,7 +7,7 @@ from pyqtorch.apply import apply_operator from pyqtorch.matrices import OPERATIONS_DICT, _controlled, _dagger -from pyqtorch.utils import Operator, State, product_state +from pyqtorch.utils import product_state class Primitive(torch.nn.Module): @@ -40,15 +40,17 @@ def extra_repr(self) -> str: def param_type(self) -> None: return self._param_type - def unitary(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> Operator: + def unitary(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> torch.Tensor: return self.pauli.unsqueeze(2) - def forward(self, state: State, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> State: + def forward( + self, state: torch.Tensor, values: dict[str, torch.Tensor] | torch.Tensor = {} + ) -> torch.Tensor: return apply_operator( state, self.unitary(values), self.qubit_support, len(state.size()) - 1 ) - def dagger(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> Operator: + def dagger(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> torch.Tensor: return _dagger(self.unitary(values)) @property @@ -85,7 +87,7 @@ class I(Primitive): # noqa: E742 def __init__(self, target: int): super().__init__(OPERATIONS_DICT["I"], target) - def forward(self, state: State, values: dict[str, torch.Tensor] = None) -> State: + def forward(self, state: torch.Tensor, values: dict[str, torch.Tensor] = None) -> torch.Tensor: return state diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index a9e0a3a7..40c72ec7 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -162,7 +162,7 @@ def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: Raises: ValueError: If `target` is outside the valid range of qubits. """ - if target > n_qubits - 1 : + if target > n_qubits - 1: raise ValueError("The target must be a valid qubit index within the circuit's range.") qubits_support = torch.arange(0, n_qubits) @@ -172,4 +172,4 @@ def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: # Add.contiguous() because kron does not support the transpose (dagger) elif target < support: operator = torch.kron(operator.contiguous(), I(support).unitary()) - return operator \ No newline at end of file + return operator diff --git a/tests/test_digital.py b/tests/test_digital.py index d67b470a..6c6cdfc0 100644 --- a/tests/test_digital.py +++ b/tests/test_digital.py @@ -12,7 +12,8 @@ from pyqtorch.apply import apply_operator from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, IMAT, ZMAT from pyqtorch.parametric import Parametric -from pyqtorch.utils import ATOL, density_mat, product_state, random_state +from pyqtorch.primitive import I +from pyqtorch.utils import ATOL, density_mat, product_state, promote_ope, random_state state_000 = product_state("000") state_001 = product_state("001") @@ -322,3 +323,13 @@ def test_dm(n_qubits: Tensor, batch_size: Tensor) -> None: dm = density_mat(state_cat) assert dm.size() == torch.Size([2**n_qubits, 2**n_qubits, batch_size]) assert torch.allclose(dm, dm_proj) + + +random_number = torch.randint(1, 6, (8, 2)) +sorted_numbers, _ = torch.sort(random_number, dim=1) + + +@pytest.mark.parametrize("target,n_qubits", sorted_numbers) +def test_promote(target: int, n_qubits: int) -> None: + operator: Tensor = promote_ope(I(target).unitary(), target, n_qubits) + assert operator.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) From 2bac1b01af80e96b0be96560fc32dd7e912a3ef5 Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Tue, 9 Apr 2024 10:57:06 +0200 Subject: [PATCH 4/7] Modify test_promote func (#155) --- tests/test_digital.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_digital.py b/tests/test_digital.py index 6c6cdfc0..80bf8512 100644 --- a/tests/test_digital.py +++ b/tests/test_digital.py @@ -12,7 +12,7 @@ from pyqtorch.apply import apply_operator from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, IMAT, ZMAT from pyqtorch.parametric import Parametric -from pyqtorch.primitive import I +from pyqtorch.primitive import I, X from pyqtorch.utils import ATOL, density_mat, product_state, promote_ope, random_state state_000 = product_state("000") @@ -325,11 +325,15 @@ def test_dm(n_qubits: Tensor, batch_size: Tensor) -> None: assert torch.allclose(dm, dm_proj) -random_number = torch.randint(1, 6, (8, 2)) -sorted_numbers, _ = torch.sort(random_number, dim=1) +size = (5, 2) +random_param = torch.randperm(size[0] * size[1]) +random_param = random_param.view(size) +random_param = torch.sort(random_param, dim=1)[0] -@pytest.mark.parametrize("target,n_qubits", sorted_numbers) +@pytest.mark.parametrize("target,n_qubits", random_param) def test_promote(target: int, n_qubits: int) -> None: - operator: Tensor = promote_ope(I(target).unitary(), target, n_qubits) - assert operator.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) + I_prom = promote_ope(I(0).unitary(), target, n_qubits) + assert I_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) + X_prom = promote_ope(X(0).unitary(), target, n_qubits) + assert X_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) From ab95bb9417b1c186925ac771dd6dde08d4a54bc8 Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Mon, 15 Apr 2024 12:57:10 +0200 Subject: [PATCH 5/7] Modify variable names (#154) --- pyqtorch/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index 40c72ec7..159c8aa2 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -165,11 +165,11 @@ def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: if target > n_qubits - 1: raise ValueError("The target must be a valid qubit index within the circuit's range.") - qubits_support = torch.arange(0, n_qubits) - for support in qubits_support: - if target > support: - operator = torch.kron(I(support).unitary(), operator.contiguous()) - # Add.contiguous() because kron does not support the transpose (dagger) - elif target < support: - operator = torch.kron(operator.contiguous(), I(support).unitary()) + qubits = torch.arange(0, n_qubits) + for qubit in qubits: + if target > qubit: + operator = torch.kron(I(qubit).unitary(), operator.contiguous()) + # Add .contiguous() because kron does not support the transpose (dagger) + elif target < qubit: + operator = torch.kron(operator.contiguous(), I(qubit).unitary()) return operator From b1ce1d7b28fb3e5128200dbe86f2420049766f95 Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Mon, 15 Apr 2024 17:49:27 +0200 Subject: [PATCH 6/7] Modify after review (#155) --- pyqtorch/primitive.py | 13 ++++++------- pyqtorch/utils.py | 9 +++++---- tests/test_digital.py | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyqtorch/primitive.py b/pyqtorch/primitive.py index 2af104bb..f6683377 100644 --- a/pyqtorch/primitive.py +++ b/pyqtorch/primitive.py @@ -4,6 +4,7 @@ from typing import Any, Tuple import torch +from torch import Tensor from pyqtorch.apply import apply_operator from pyqtorch.matrices import OPERATIONS_DICT, _controlled, _dagger @@ -11,7 +12,7 @@ class Primitive(torch.nn.Module): - def __init__(self, pauli: torch.Tensor, target: int) -> None: + def __init__(self, pauli: Tensor, target: int) -> None: super().__init__() self.target: int = target self.qubit_support: Tuple[int, ...] = (target,) @@ -40,17 +41,15 @@ def extra_repr(self) -> str: def param_type(self) -> None: return self._param_type - def unitary(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> torch.Tensor: + def unitary(self, values: dict[str, Tensor] | Tensor = {}) -> Tensor: return self.pauli.unsqueeze(2) - def forward( - self, state: torch.Tensor, values: dict[str, torch.Tensor] | torch.Tensor = {} - ) -> torch.Tensor: + def forward(self, state: Tensor, values: dict[str, Tensor] | Tensor = {}) -> Tensor: return apply_operator( state, self.unitary(values), self.qubit_support, len(state.size()) - 1 ) - def dagger(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> torch.Tensor: + def dagger(self, values: dict[str, Tensor] | Tensor = {}) -> Tensor: return _dagger(self.unitary(values)) @property @@ -87,7 +86,7 @@ class I(Primitive): # noqa: E742 def __init__(self, target: int): super().__init__(OPERATIONS_DICT["I"], target) - def forward(self, state: torch.Tensor, values: dict[str, torch.Tensor] = None) -> torch.Tensor: + def forward(self, state: Tensor, values: dict[str, Tensor] = None) -> Tensor: return state diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index 159c8aa2..b91b44a1 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -7,7 +7,6 @@ from torch import Tensor from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, DEFAULT_REAL_DTYPE -from pyqtorch.primitive import I State = Tensor Operator = Tensor @@ -145,7 +144,7 @@ def density_mat(state: Tensor) -> Tensor: return torch.permute(torch.einsum("bi,bj->bij", (state, state.conj())), undo_perm) -def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: +def promote_op(operator: Tensor, target: int, n_qubits: int) -> Tensor: """ Promotes `operator` to the size of the circuit (number of qubits and batch). Targeting the first qubit implies target = 0, so target > n_qubits - 1. @@ -166,10 +165,12 @@ def promote_ope(operator: Tensor, target: int, n_qubits: int) -> Tensor: raise ValueError("The target must be a valid qubit index within the circuit's range.") qubits = torch.arange(0, n_qubits) + # Define I instead of importing it from pyqtorch.primitive to avoid circular import + identity = torch.tensor([[[1.0 + 0.0j], [0.0 + 0.0j]], [[0.0 + 0.0j], [1.0 + 0.0j]]]) for qubit in qubits: if target > qubit: - operator = torch.kron(I(qubit).unitary(), operator.contiguous()) + operator = torch.kron(identity, operator.contiguous()) # Add .contiguous() because kron does not support the transpose (dagger) elif target < qubit: - operator = torch.kron(operator.contiguous(), I(qubit).unitary()) + operator = torch.kron(operator.contiguous(), identity) return operator diff --git a/tests/test_digital.py b/tests/test_digital.py index 80bf8512..ad5d0661 100644 --- a/tests/test_digital.py +++ b/tests/test_digital.py @@ -13,7 +13,7 @@ from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, IMAT, ZMAT from pyqtorch.parametric import Parametric from pyqtorch.primitive import I, X -from pyqtorch.utils import ATOL, density_mat, product_state, promote_ope, random_state +from pyqtorch.utils import ATOL, density_mat, product_state, promote_op, random_state state_000 = product_state("000") state_001 = product_state("001") @@ -333,7 +333,7 @@ def test_dm(n_qubits: Tensor, batch_size: Tensor) -> None: @pytest.mark.parametrize("target,n_qubits", random_param) def test_promote(target: int, n_qubits: int) -> None: - I_prom = promote_ope(I(0).unitary(), target, n_qubits) + I_prom = promote_op(I(0).unitary(), target, n_qubits) assert I_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) - X_prom = promote_ope(X(0).unitary(), target, n_qubits) + X_prom = promote_op(X(0).unitary(), target, n_qubits) assert X_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) From 05fa8a3a748c356a7dd7d6bc6059bb492bd7af10 Mon Sep 17 00:00:00 2001 From: EthanObadia Date: Tue, 16 Apr 2024 14:01:42 +0200 Subject: [PATCH 7/7] Modify after review #155 --- pyqtorch/utils.py | 16 ++++++++-------- tests/test_digital.py | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index b91b44a1..ebd56900 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -145,6 +145,8 @@ def density_mat(state: Tensor) -> Tensor: def promote_op(operator: Tensor, target: int, n_qubits: int) -> Tensor: + from pyqtorch.primitive import I + """ Promotes `operator` to the size of the circuit (number of qubits and batch). Targeting the first qubit implies target = 0, so target > n_qubits - 1. @@ -163,14 +165,12 @@ def promote_op(operator: Tensor, target: int, n_qubits: int) -> Tensor: """ if target > n_qubits - 1: raise ValueError("The target must be a valid qubit index within the circuit's range.") - qubits = torch.arange(0, n_qubits) - # Define I instead of importing it from pyqtorch.primitive to avoid circular import - identity = torch.tensor([[[1.0 + 0.0j], [0.0 + 0.0j]], [[0.0 + 0.0j], [1.0 + 0.0j]]]) + qubits = qubits[qubits != target] for qubit in qubits: - if target > qubit: - operator = torch.kron(identity, operator.contiguous()) - # Add .contiguous() because kron does not support the transpose (dagger) - elif target < qubit: - operator = torch.kron(operator.contiguous(), identity) + operator = torch.where( + target > qubit, + torch.kron(I(target).unitary(), operator.contiguous()), + torch.kron(operator.contiguous(), I(target).unitary()), + ) return operator diff --git a/tests/test_digital.py b/tests/test_digital.py index ad5d0661..f49df716 100644 --- a/tests/test_digital.py +++ b/tests/test_digital.py @@ -9,10 +9,10 @@ from torch import Tensor import pyqtorch as pyq -from pyqtorch.apply import apply_operator -from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, IMAT, ZMAT +from pyqtorch.apply import apply_ope_ope, apply_operator +from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, IMAT, ZMAT, _dagger from pyqtorch.parametric import Parametric -from pyqtorch.primitive import I, X +from pyqtorch.primitive import H, I, S, T, X, Y, Z from pyqtorch.utils import ATOL, density_mat, product_state, promote_op, random_state state_000 = product_state("000") @@ -327,13 +327,16 @@ def test_dm(n_qubits: Tensor, batch_size: Tensor) -> None: size = (5, 2) random_param = torch.randperm(size[0] * size[1]) -random_param = random_param.view(size) +random_param = random_param.reshape(size) random_param = torch.sort(random_param, dim=1)[0] @pytest.mark.parametrize("target,n_qubits", random_param) -def test_promote(target: int, n_qubits: int) -> None: - I_prom = promote_op(I(0).unitary(), target, n_qubits) - assert I_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) - X_prom = promote_op(X(0).unitary(), target, n_qubits) - assert X_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) +@pytest.mark.parametrize("operator", [I, X, Y, Z, H, T, S]) +def test_promote(target: int, n_qubits: int, operator: Tensor) -> None: + op_prom = promote_op(operator(target).unitary(), target, n_qubits) + assert op_prom.size() == torch.Size([2**n_qubits, 2**n_qubits, 1]) + assert torch.allclose( + apply_ope_ope(op_prom, _dagger(op_prom), target), + torch.eye(2**n_qubits, dtype=torch.cdouble).unsqueeze(2), + )