diff --git a/pyproject.toml b/pyproject.toml index 9a75de2..dd61aa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "cirq-core == 1.4.1", "genQC == 0.1.0", "numpy >= 1.23", - "pennylane == 0.39.0", + "pennylane == 0.42.3", "pytket == 2.4.1", "pytket-qiskit == 0.68.0", "pytket-cirq == 0.40.0", diff --git a/quick/backend/qiskit_backends/fake_ibm_backend.py b/quick/backend/qiskit_backends/fake_ibm_backend.py index e78d3d9..8fda1ca 100644 --- a/quick/backend/qiskit_backends/fake_ibm_backend.py +++ b/quick/backend/qiskit_backends/fake_ibm_backend.py @@ -21,6 +21,7 @@ import numpy as np from numpy.typing import NDArray +import warnings from qiskit.primitives import BackendSamplerV2 as BackendSampler # type: ignore from qiskit_aer import AerSimulator # type: ignore @@ -113,7 +114,7 @@ def __init__( self._op_backend = AerSimulator.from_backend(backend, device="GPU", method="unitary") else: if self.device == "GPU" and available_devices["GPU"] is None: - print("Warning: GPU acceleration is not available. Defaulted to CPU.") + warnings.warn("Warning: GPU acceleration is not available. Defaulted to CPU.") self._counts_backend = BackendSampler(backend=AerSimulator.from_backend(backend)) self._op_backend = AerSimulator.from_backend(backend, method="unitary") diff --git a/quick/circuit/ansatz.py b/quick/circuit/ansatz.py index bb5e00f..749a759 100644 --- a/quick/circuit/ansatz.py +++ b/quick/circuit/ansatz.py @@ -48,7 +48,7 @@ class Ansatz: ```python from quick.circuit import Ansatz, QiskitCircuit - from quick.circuit.circuit_utils import reshape, flatten + from quick.circuit.utils import reshape, flatten from quick.random import generate_random_state from scipy.optimize import minimize diff --git a/quick/circuit/circuit.py b/quick/circuit/circuit.py index 80ac95e..fd519af 100644 --- a/quick/circuit/circuit.py +++ b/quick/circuit/circuit.py @@ -29,7 +29,7 @@ from numpy.typing import NDArray from types import NotImplementedType from typing import ( - Any, Literal, overload, SupportsFloat, SupportsIndex, TYPE_CHECKING + Any, Literal, overload, SupportsFloat, SupportsIndex, TypeAlias, TYPE_CHECKING ) import qiskit # type: ignore @@ -40,7 +40,7 @@ if TYPE_CHECKING: from quick.backend import Backend -from quick.circuit.circuit_utils import ( +from quick.circuit.utils import ( multiplexed_rz_angles, decompose_multiplexor_rotations, extract_single_qubits_and_diagonal, @@ -49,7 +49,7 @@ from quick.circuit.dag import DAGCircuit from quick.circuit.from_framework import FromCirq, FromPennyLane, FromQiskit, FromTKET from quick.predicates import is_unitary_matrix -from quick.primitives import Bra, Ket, Operator +from quick.primitives import Statevector, Operator from quick.synthesis.gate_decompositions.multi_controlled_decomposition import MCRX, MCRY, MCRZ from quick.synthesis.statepreparation import Isometry from quick.synthesis.unitarypreparation import ( @@ -58,6 +58,8 @@ EPSILON = 1e-15 +CIRCUIT_LOG: TypeAlias = list[dict[str, Any]] + """ Set the frozensets for the keys to be used: - Decorator `Circuit.gatemethod()` - Method `Circuit.vertical_reverse()` @@ -87,7 +89,11 @@ } # List of 1Q gates wrapped by individual frameworks -GATES = Literal["I", "X", "Y", "Z", "H", "S", "Sdg", "T", "Tdg", "RX", "RY", "RZ", "Phase", "U3"] +GATES = Literal[ + "I", "X", "Y", "Z", "H", "S", "Sdg", + "T", "Tdg", "RX", "RY", "RZ", "Phase", + "U3" +] # List of self-adjoint gates SELF_ADJ_GATES = frozenset([ @@ -100,6 +106,20 @@ # these gates cannot be decomposed further PRIMITIVE_GATES = frozenset(["U3", "CX", "GlobalPhase", "measure"]) +# List of gates that are compatible/convertible with `.control()` +CONTROLLABLE_GATES = frozenset([ + "I", "X", "Y", "Z", "H", "S", "Sdg", "T", "Tdg", + "RX", "RY", "RZ", "Phase", "XPow", "YPow", "ZPow", + "RXX", "RYY", "RZZ", "U3", "SWAP" +]) + +# Controlled versions of controllable gates +CONTROLLED_GATES = frozenset([ + *CONTROLLABLE_GATES, + *(f"C{gate}" for gate in CONTROLLABLE_GATES), + *(f"MC{gate}" for gate in CONTROLLABLE_GATES) +]) + # Constants PI = np.pi PI_DOUBLE = 2 * PI @@ -443,10 +463,10 @@ def decompose_last( Usage ----- - >>> def NewGate(qubit_indices: int | Sequence[int]): + >>> def NewGate(self, qubit_indices: int | Sequence[int]): >>> gate = self.process_gate_params(gate="NewGate", params=locals()) - >>> with circuit.decompose_last(): - >>> circuit.X(qubit_indices) + >>> with self.decompose_last(): + >>> self.X(qubit_indices) """ # If the gate is parameterized, and its rotation is effectively zero, return # as no operation is needed @@ -4634,7 +4654,6 @@ def _Diagonal( if isinstance(qubit_indices, int): qubit_indices = [qubit_indices] - # Check if the number of diagonal entries is a power of 2 num_qubits = np.log2(len(diagnoal)) if num_qubits < 1 or not int(num_qubits) == num_qubits: @@ -4801,7 +4820,6 @@ def Multiplexor( if not gate.shape == (2, 2): raise ValueError(f"The dimension of a gate is not equal to 2x2. Received {gate.shape}.") - # Check if number of gates in gate_list is a positive power of two num_controls = int( np.log2( len(single_qubit_gates) @@ -4992,14 +5010,14 @@ def QFT( def initialize( self, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, qubit_indices: int | Sequence[int] ) -> None: """ Initialize the state of the circuit. Parameters ---------- - `state` : NDArray[np.complex128] | quick.primitives.Bra | quick.primitives.Ket + `state` : NDArray[np.complex128] | quick.primitives.Statevector The state to initialize the circuit to. `qubit_indices` : int | Sequence[int] The index of the qubit(s) to apply the gate to. @@ -5007,7 +5025,7 @@ def initialize( Raises ------ TypeError - - If the state is not a numpy array or a Bra/Ket object. + - If the state is not a numpy array or a Statevector object. - If the qubit indices are not integers or a sequence of integers. ValueError - If the compression percentage is not in the range [0, 100]. @@ -5021,10 +5039,8 @@ def initialize( ----- >>> circuit.initialize([1, 0], qubit_indices=0) """ - # Initialize the state preparation schema isometry = Isometry(output_framework=type(self)) - # Prepare the state self = isometry.apply_state( circuit=self, state=state, @@ -5067,10 +5083,8 @@ def unitary( ... [0, 1, 0, 0], ... [1, 0, 0, 0]], qubit_indices=[0, 1]) """ - # Initialize the unitary preparation schema unitary_preparer = ShannonDecomposition(output_framework=type(self)) - # Prepare the unitary matrix self = unitary_preparer.apply_unitary( circuit=self, unitary=unitary_matrix, @@ -5088,29 +5102,26 @@ def vertical_reverse(self) -> None: @staticmethod def _horizontal_reverse( - circuit_log: list[dict[str, Any]], + circuit_log: CIRCUIT_LOG, adjoint: bool = True - ) -> list[dict[str, Any]]: + ) -> CIRCUIT_LOG: """ Perform a horizontal reverse operation. Parameters ---------- - `circuit_log` : list[dict[str, Any]] + `circuit_log` : CIRCUIT_LOG The circuit log to reverse. `adjoint` : bool, optional, default=True Whether or not to apply the adjoint of the circuit. Returns ------- - list[dict[str, Any]] + CIRCUIT_LOG The reversed circuit log. """ - # Reverse the order of the operations circuit_log = circuit_log[::-1] - # If adjoint is True, then multiply the angles by -1 if adjoint: - # Iterate over every operation, and change the index accordingly for i, operation in enumerate(circuit_log): if "angle" in operation: operation["angle"] = -operation["angle"] @@ -5212,18 +5223,12 @@ def add( else: operation[key] = list(qubit_indices)[operation[key]] # type: ignore - # Iterate over the gate log and apply corresponding gates in the new framework for gate_info in circuit_log: - # Extract gate name and remove it from gate_info for kwargs gate_name = gate_info.pop("gate", None) - - # Extract gate definition and remove it from gate_info for kwargs gate_definition = gate_info.pop("definition", None) - # Use the gate mapping to apply the corresponding gate with remaining kwargs getattr(self, gate_name)(**gate_info) - # Re-insert gate name and definition into gate_info if needed elsewhere gate_info["gate"] = gate_name gate_info["definition"] = gate_definition @@ -5328,29 +5333,7 @@ def get_depth(self) -> int: ----- >>> circuit.get_depth() """ - circuit = self.copy() - - # Transpile the circuit to both simplify and optimize it - circuit.transpile() - - # Get the depth of the circuit - depth = circuit.get_dag().get_depth() - - return depth - - def get_width(self) -> int: - """ Get the width of the circuit. - - Returns - ------- - `width` : int - The width of the circuit. - - Usage - ----- - >>> circuit.get_width() - """ - return self.num_qubits + return self.get_dag().get_depth() @abstractmethod def get_unitary(self) -> NDArray[np.complex128]: @@ -5610,6 +5593,59 @@ def remove_measurements( return self._remove_measurements() + def decompose_gate( + self, + target_gates: str | Sequence[str] + ) -> Circuit: + """ Decompose the specified gates in the circuit. + + Parameters + ---------- + `gates` : str | Sequence[str] + The gates to decompose. + + Returns + ------- + `circuit` : quick.circuit.Circuit + The circuit with the decomposed gates. + + Usage + ----- + >>> new_circuit = circuit.decompose_gate("CX") + >>> new_circuit = circuit.decompose_gate(["Z", "Phase"]) + """ + circuit = type(self)(self.num_qubits) + + # We cannot decompose primitive gates and to avoid losing them + # or getting stuck in an infinite loop we simply remove them + # from the target gates + target_gates_set = frozenset(set(target_gates) - PRIMITIVE_GATES) + + circuit_log_copy = copy.deepcopy(self.circuit_log) + + while True: + gates = set([operation["gate"] for operation in circuit_log_copy]) + + if gates.isdisjoint(target_gates_set): + break + + if gates.issubset(PRIMITIVE_GATES): + break + + for operation in circuit_log_copy: + if operation["gate"] in target_gates_set: + circuit.circuit_log.extend(operation["definition"]) + else: + circuit.circuit_log.append(operation) + + circuit_log_copy = circuit.circuit_log + circuit.circuit_log = [] + + circuit.circuit_log = circuit_log_copy + circuit.update() + + return circuit + def decompose( self, reps: int = 1, @@ -5638,46 +5674,39 @@ def decompose( if all([operation["definition"] == [] for operation in self.circuit_log]): return self.copy() - # Create a new circuit to store the decomposed gates circuit = type(self)(self.num_qubits) # Create a copy of the circuit log to use as placeholder for each layer of decomposition circuit_log_copy = copy.deepcopy(self.circuit_log) # Iterate over the circuit log, and use the `definition` key to define the decomposition - # Continue until the circuit log is fully decomposed + # Continue until the circuit log is fully decomposed or until the number of reps is reached if full: - while True: - gates = set([operation["gate"] for operation in circuit_log_copy]) + reps = -1 - if gates.issubset(PRIMITIVE_GATES): - break + current_rep = 0 - for operation in circuit_log_copy: - if operation["definition"] != []: - for op in operation["definition"]: - circuit.circuit_log.append(op) - else: - circuit.circuit_log.append(operation) + while True: + if current_rep == reps: + break - circuit_log_copy = circuit.circuit_log - circuit.circuit_log = [] + gates = set([operation["gate"] for operation in circuit_log_copy]) - # Iterate over the circuit log, and use the `definition` key to define the decomposition - # Each rep will decompose the circuit one layer further - else: - for _ in range(reps): - for operation in circuit_log_copy: - if operation["definition"] != []: - for op in operation["definition"]: - circuit.circuit_log.append(op) - else: - circuit.circuit_log.append(operation) - circuit_log_copy = circuit.circuit_log - circuit.circuit_log = [] + if gates.issubset(PRIMITIVE_GATES): + break - circuit.circuit_log = circuit_log_copy + for operation in circuit_log_copy: + if operation["definition"] != []: + circuit.circuit_log.extend(operation["definition"]) + else: + circuit.circuit_log.append(operation) + + circuit_log_copy = circuit.circuit_log + circuit.circuit_log = [] + current_rep += 1 + + circuit.circuit_log = circuit_log_copy circuit.update() return circuit @@ -5830,21 +5859,14 @@ def convert( if not issubclass(circuit_framework, Circuit): raise TypeError("The circuit framework must be a subclass of `quick.circuit.Circuit`.") - # Define the new circuit using the provided framework converted_circuit = circuit_framework(self.num_qubits) - # Iterate over the gate log and apply corresponding gates in the new framework for gate_info in self.circuit_log: - # Extract gate name and remove it from gate_info for kwargs gate_name = gate_info.pop("gate") - - # Extract gate definition and remove it from gate_info for kwargs gate_definition = gate_info.pop("definition", None) - # Use the gate mapping to apply the corresponding gate with remaining kwargs getattr(converted_circuit, gate_name)(**gate_info) - # Re-insert gate name and definition into gate_info if needed elsewhere gate_info["gate"] = gate_name gate_info["definition"] = gate_definition @@ -5873,23 +5895,22 @@ def control( `controlled_circuit` : quick.circuit.Circuit The circuit as a controlled gate. """ - # Create a copy of the circuit - circuit = self.copy() + # To provide compatibility with gates that are not directly + # controllable we decompose such gates, which adds support + # for user-defined gates + gates = set([operation["gate"] for operation in self.circuit_log]) + + circuit = self.decompose_gate(gates - CONTROLLED_GATES) # type: ignore # When a target gate has global phase, we need to account for that by resetting # the global phase, and then applying it to the control indices using the Phase # or MCPhase gates depending on the number of control indices circuit.circuit_log = [op for op in circuit.circuit_log if op["gate"] != "GlobalPhase"] - # Define a controlled circuit controlled_circuit = type(circuit)(num_qubits=circuit.num_qubits + num_controls) - # Iterate over the gate log and apply corresponding gates in the new framework for gate_info in circuit.circuit_log: - # Extract gate name and remove it from gate_info for kwargs gate_name = gate_info.pop("gate") - - # Extract gate definition and remove it from gate_info for kwargs gate_definition = gate_info.pop("definition", None) # Change the gate name from single qubit and controlled to multi-controlled @@ -5911,15 +5932,11 @@ def control( if isinstance(control_indices, int): control_indices = [control_indices] - # Add control indices gate_info["control_indices"] = list(range(num_controls)) + \ [idx for idx in control_indices if idx not in range(num_controls)] - # Use the gate mapping to apply the corresponding gate with remaining kwargs - # Add the control indices as the first indices given the number of control qubits getattr(controlled_circuit, gate_name)(**gate_info) - # Re-insert gate name and definition into gate_info if needed elsewhere gate_info["gate"] = gate_name gate_info["definition"] = gate_definition diff --git a/quick/circuit/gate_matrix/__init__.py b/quick/circuit/gate_matrix/__init__.py index 2ca9d05..ef8df5a 100644 --- a/quick/circuit/gate_matrix/__init__.py +++ b/quick/circuit/gate_matrix/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. __all__ = [ - "Gate", "PauliX", "PauliY", "PauliZ", @@ -33,7 +32,6 @@ "CT" ] -from quick.circuit.gate_matrix.gate import Gate from quick.circuit.gate_matrix.single_qubit_gates import ( PauliX, PauliY, PauliZ, Hadamard, S, T, RX, RY, RZ, U3, Phase ) diff --git a/quick/circuit/gate_matrix/controlled_qubit_gates.py b/quick/circuit/gate_matrix/controlled_qubit_gates.py index a658059..4abaa6a 100644 --- a/quick/circuit/gate_matrix/controlled_qubit_gates.py +++ b/quick/circuit/gate_matrix/controlled_qubit_gates.py @@ -23,18 +23,355 @@ "CZ", "CH", "CS", - "CT" + "CSdg", + "CT", + "CTdg", + "CRX", + "CRY", + "CRZ", + "CPhase", + "CU3", + "MCX", + "MCY", + "MCZ", + "MCH", + "MCS", + "MCSdg", + "MCT", + "MCTdg", + "MCRX", + "MCRY", + "MCRZ", + "MCPhase", + "MCU3" ] -from quick.circuit.gate_matrix import Gate from quick.circuit.gate_matrix.single_qubit_gates import ( - PauliX, PauliY, PauliZ, Hadamard, S, T + PauliX, PauliY, PauliZ, Hadamard, S, Sdg, T, Tdg, + RX, RY, RZ, Phase, U3 ) +from quick.primitives import Operator -CX: Gate = PauliX().control(1) -CY: Gate = PauliY().control(1) -CZ: Gate = PauliZ().control(1) -CH: Gate = Hadamard().control(1) -CS: Gate = S().control(1) -CT: Gate = T().control(1) \ No newline at end of file +CX = PauliX.control(1) +CY = PauliY.control(1) +CZ = PauliZ.control(1) +CH = Hadamard.control(1) +CS = S.control(1) +CSdg = Sdg.control(1) +CT = T.control(1) +CTdg = Tdg.control(1) + +def CRX(theta: float) -> Operator: + """ Generate the controlled RX rotation gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the controlled RX rotation gate. + """ + return RX(theta).control(1) + +def CRY(theta: float) -> Operator: + """ Generate the controlled RY rotation gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the controlled RY rotation gate. + """ + return RY(theta).control(1) + +def CRZ(theta: float) -> Operator: + """ Generate the controlled RZ rotation gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the controlled RZ rotation gate. + """ + return RZ(theta).control(1) + +def CPhase(theta: float) -> Operator: + """ Generate the controlled Phase gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The phase angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the controlled Phase gate. + """ + return Phase(theta).control(1) + +def CU3( + theta: float, + phi: float, + lam: float + ) -> Operator: + """ Generate the controlled U3 gate given angle parameters + theta, phi, and lam. + + Parameters + ---------- + `theta` : float + The theta angle in radians. + `phi` : float + The phi angle in radians. + `lam` : float + The lambda angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the controlled U3 gate. + """ + return U3(theta, phi, lam).control(1) + +def MCX(num_controls: int) -> Operator: + """ Generate the multi-controlled X gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled X gate. + """ + return PauliX.control(num_controls) + +def MCY(num_controls: int) -> Operator: + """ Generate the multi-controlled Y gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled Y gate. + """ + return PauliY.control(num_controls) + +def MCZ(num_controls: int) -> Operator: + """ Generate the multi-controlled Z gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled Z gate. + """ + return PauliZ.control(num_controls) + +def MCH(num_controls: int) -> Operator: + """ Generate the multi-controlled H gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled H gate. + """ + return Hadamard.control(num_controls) + +def MCS(num_controls: int) -> Operator: + """ Generate the multi-controlled S gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled S gate. + """ + return S.control(num_controls) + +def MCSdg(num_controls: int) -> Operator: + """ Generate the multi-controlled Sdg gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled Sdg gate. + """ + return Sdg.control(num_controls) + +def MCT(num_controls: int) -> Operator: + """ Generate the multi-controlled T gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled T gate. + """ + return T.control(num_controls) + +def MCTdg(num_controls: int) -> Operator: + """ Generate the multi-controlled Tdg gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled Tdg gate. + """ + return Tdg.control(num_controls) + +def MCRX( + num_controls: int, + theta: float + ) -> Operator: + """ Generate the multi-controlled RX gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled RX gate. + """ + return RX(theta).control(num_controls) + +def MCRY( + num_controls: int, + theta: float + ) -> Operator: + """ Generate the multi-controlled RY gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled RY gate. + """ + return RY(theta).control(num_controls) + +def MCRZ( + num_controls: int, + theta: float + ) -> Operator: + """ Generate the multi-controlled RZ gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled RZ gate. + """ + return RZ(theta).control(num_controls) + +def MCPhase( + num_controls: int, + theta: float + ) -> Operator: + """ Generate the multi-controlled Phase gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled Phase gate. + """ + return Phase(theta).control(num_controls) + +def MCU3( + num_controls: int, + theta: float, + phi: float, + lam: float + ) -> Operator: + """ Generate the multi-controlled U3 gate given the number of control qubits. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + `theta` : float + The theta angle in radians. + `phi` : float + The phi angle in radians. + `lam` : float + The lambda angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the multi-controlled U3 gate. + """ + return U3(theta, phi, lam).control(num_controls) \ No newline at end of file diff --git a/quick/circuit/gate_matrix/gate.py b/quick/circuit/gate_matrix/gate.py deleted file mode 100644 index 9a3073e..0000000 --- a/quick/circuit/gate_matrix/gate.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2023-2025 Qualition Computing LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://github.com/Qualition/quick/blob/main/LICENSE -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Module for generating the matrix representation of a quantum gate. -""" - -from __future__ import annotations - -__all__ = ["Gate"] - -import numpy as np -from numpy.typing import NDArray -from typing import Literal - -from quick.predicates import is_unitary_matrix - -# Constant -ZERO_PROJECTOR = np.array([ - [1, 0], - [0, 0] -]) -ONE_PROJECTOR = np.array([ - [0, 0], - [0, 1] -]) - - -class Gate: - """ `quick.gate_matrix.Gate` class represents a quantum gate. This class is used to - generate the matrix representation of a quantum gate for testing and classical simulation - purposes. - - Parameters - ---------- - `name`: str - The name of the gate. - `matrix`: NDArray[np.complex128] - The matrix representation of the gate. - - Attributes - ---------- - `name`: str - The name of the gate. - `matrix`: NDArray[np.complex128] - The matrix representation of the gate. - - Raises - ------ - ValueError - - If the matrix is not unitary. - - Usage - ----- - >>> gate = Gate("H", np.array([[1, 1], - ... [1, -1]]) / np.sqrt(2)) - """ - def __init__( - self, - name: str, - matrix: NDArray[np.complex128] - ) -> None: - """ Initialize a `quick.gate_matrix.Gate` instance. - """ - self.name = name - self.matrix = matrix - if not is_unitary_matrix(matrix): - raise ValueError("The matrix must be unitary.") - self.num_qubits = int(np.log2(matrix.shape[0])) - self.ordering = "MSB" - - def adjoint(self) -> NDArray[np.complex128]: - """ Generate the adjoint of the gate. - - Returns - ------- - NDArray[np.complex128] - The adjoint of the gate. - """ - return self.matrix.T.conj() - - def control( - self, - num_control_qubits: int - ) -> Gate: - """ Generate the matrix representation of a controlled version of the gate. - - Parameters - ---------- - `num_control_qubits`: int - The number of control qubits. - - Returns - ------- - `controlled_gate` : quick.gate_matrix.Gate - The controlled gate. - - Raises - ------ - TypeError - - If the number of control qubits is not an integer. - ValueError - - If the number of control qubits is less than 1. - """ - if not isinstance(num_control_qubits, int): - raise TypeError("The number of control qubits must be an integer.") - - if num_control_qubits < 1: - raise ValueError("The number of control qubits must be greater than 0.") - - controlled_matrix = np.kron(ZERO_PROJECTOR, np.eye(2 ** num_control_qubits)) + \ - np.kron(ONE_PROJECTOR, self.matrix) - controlled_gate = Gate(f"C-{self.name}", controlled_matrix.astype(complex)) - - return controlled_gate - - def change_mapping( - self, - ordering: Literal["MSB", "LSB"] - ) -> None: - """ Change the mapping of the qubits in the matrix representation of the gate. - - Parameters - ---------- - `ordering`: Literal["MSB", "LSB"] - The new qubit ordering. - - Returns - ------- - `reordered_matrix` : NDArray[np.complex128] - The new matrix with LSB conversion. - - Raises - ------ - ValueError - - If the ordering is not "MSB" or "LSB". - """ - if ordering not in ["MSB", "LSB"]: - raise ValueError("The ordering must be either 'MSB' or 'LSB'.") - - if ordering == self.ordering: - return - - # Create a new matrix to store the reordered elements - dims = [2] * (self.num_qubits * 2) - reordered_matrix = self.matrix.reshape(dims).transpose().reshape( - (2**self.num_qubits, 2**self.num_qubits) - ) - - self.matrix = reordered_matrix - self.ordering = ordering \ No newline at end of file diff --git a/quick/circuit/gate_matrix/single_qubit_gates.py b/quick/circuit/gate_matrix/single_qubit_gates.py index 7d3ea1f..20db165 100644 --- a/quick/circuit/gate_matrix/single_qubit_gates.py +++ b/quick/circuit/gate_matrix/single_qubit_gates.py @@ -23,186 +23,187 @@ "PauliZ", "Hadamard", "S", + "Sdg", "T", + "Tdg", "RX", "RY", "RZ", - "U3", - "Phase" + "Phase", + "U3" ] import numpy as np -from quick.circuit.gate_matrix import Gate - - -class PauliX(Gate): - """ `quick.gate_matrix.PauliX` class represents the Pauli-X gate. - """ - def __init__(self) -> None: - """ Initialize a `quick.gate_matrix.PauliX` instance. - """ - super().__init__( - "X", - np.array([ - [0, 1], - [1, 0] - ]) - ) - -class PauliY(Gate): - """ `quick.gate_matrix.PauliY` class represents the Pauli-Y gate. - """ - def __init__(self) -> None: - """ Initialize a `quick.gate_matrix.PauliY` instance. - """ - super().__init__( - "Y", - np.array([ - [0, -1j], - [1j, 0] - ]) - ) - -class PauliZ(Gate): - """ `quick.gate_matrix.PauliZ` class represents the Pauli-Z gate. - """ - def __init__(self) -> None: - """ Initialize a `quick.gate_matrix.PauliZ` instance. - """ - super().__init__( - "Z", - np.array([ - [1, 0], - [0, -1] - ]) - ) - -class Hadamard(Gate): - """ `quick.gate_matrix.Hadamard` class represents the Hadamard gate. - """ - def __init__(self) -> None: - """ Initialize a `quick.gate_matrix.Hadamard` instance. - """ - super().__init__( - "H", - np.array([ - [1, 1], - [1, -1] - ]) / np.sqrt(2) - ) - -class S(Gate): - """ `quick.gate_matrix.S` class represents the S gate. - """ - def __init__(self) -> None: - """ Initialize a `quick.gate_matrix.S` instance. - """ - super().__init__( - "S", - np.array([ - [1, 0], - [0, 1j] - ]) - ) - -class T(Gate): - """ `quick.gate_matrix.T` class represents the T gate. - """ - def __init__(self) -> None: - """ Initialize a `quick.gate_matrix.T` instance. - """ - super().__init__( - "T", - np.array([ - [1, 0], - [0, np.exp(1j * np.pi / 4)] - ]) - ) - -class RX(Gate): - """ `quick.gate_matrix.RX` class represents the RX gate. +from quick.primitives import Operator + + +PauliX = Operator( + label="X", + data=np.array([ + [0, 1], + [1, 0] + ]) +) + +PauliY = Operator( + label="Y", + data=np.array([ + [0, -1j], + [1j, 0] + ]) +) + +PauliZ = Operator( + label="Z", + data=np.array([ + [1, 0], + [0, -1] + ]) +) + +Hadamard = Operator( + label="H", + data=np.array([ + [1, 1], + [1, -1] + ]) / np.sqrt(2) +) + +S = Operator( + label="S", + data=np.array([ + [1, 0], + [0, 1j] + ]) +) + +Sdg = S.adjoint() + +T = Operator( + label="T", + data=np.array([ + [1, 0], + [0, np.exp(1j * np.pi / 4)] + ]) +) + +Tdg = T.adjoint() + +def RX(theta: float) -> Operator: + """ Generate the RX rotation gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the RX rotation gate. """ - def __init__( - self, - theta: float - ) -> None: - """ Initialize a `quick.gate_matrix.RX` instance. - """ - super().__init__( - f"RX({theta})", - np.array([ - [np.cos(theta / 2), -1j * np.sin(theta / 2)], - [-1j * np.sin(theta / 2), np.cos(theta / 2)] - ]) - ) - -class RY(Gate): - """ `quick.gate_matrix.RY` class represents the RY gate. + return Operator( + label=f"RX({theta})", + data=np.array([ + [np.cos(theta / 2), -1j * np.sin(theta / 2)], + [-1j * np.sin(theta / 2), np.cos(theta / 2)] + ]) + ) + +def RY(theta: float) -> Operator: + """ Generate the RY rotation gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the RY rotation gate. """ - def __init__( - self, - theta: float - ) -> None: - """ Initialize a `quick.gate_matrix.RY` instance. - """ - super().__init__( - f"RY({theta})", - np.array([ - [np.cos(theta / 2), -np.sin(theta / 2)], - [np.sin(theta / 2), np.cos(theta / 2)] - ]) - ) - -class RZ(Gate): - """ `quick.gate_matrix.RZ` class represents the RZ gate. + return Operator( + label=f"RY({theta})", + data=np.array([ + [np.cos(theta / 2), -np.sin(theta / 2)], + [np.sin(theta / 2), np.cos(theta / 2)] + ]) + ) + +def RZ(theta: float) -> Operator: + """ Generate the RZ rotation gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The rotation angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the RZ rotation gate. """ - def __init__( - self, - theta: float - ) -> None: - """ Initialize a `quick.gate_matrix.RZ` instance. - """ - super().__init__( - f"RZ({theta})", - np.array([ - [np.exp(-1j * theta / 2), 0], - [0, np.exp(1j * theta / 2)] - ]) - ) - -class U3(Gate): - """ `quick.gate_matrix.U3` class represents the U3 gate. + return Operator( + label=f"RZ({theta})", + data=np.array([ + [np.exp(-1j * theta / 2), 0], + [0, np.exp(1j * theta / 2)] + ]) + ) + +def Phase(theta: float) -> Operator: + """ Generate the Phase gate given angle parameter + theta. + + Parameters + ---------- + `theta` : float + The phase angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the Phase gate. """ - def __init__( - self, - theta: float, - phi: float, - lam: float - ) -> None: - """ Initialize a `quick.gate_matrix.U3` instance. - """ - super().__init__( - f"U3({theta}, {phi}, {lam})", - np.array([ - [np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)], - [np.exp(1j * phi) * np.sin(theta / 2), np.exp(1j * (phi + lam)) * np.cos(theta / 2)] - ]) - ) - -class Phase(Gate): - """ `quick.gate_matrix.Phase` class represents the Phase gate. + return Operator( + label=f"Phase({theta})", + data=np.array([ + [1, 0], + [0, np.exp(1j * theta)] + ]) + ) + +def U3( + theta: float, + phi: float, + lam: float + ) -> Operator: + """ Generate the U3 gate given angle parameters + theta, phi, and lam. + + Parameters + ---------- + `theta` : float + The theta angle in radians. + `phi` : float + The phi angle in radians. + `lam` : float + The lambda angle in radians. + + Returns + ------- + quick.primitives.Operator + The matrix representation of the U3 gate. """ - def __init__( - self, - theta: float - ) -> None: - """ Initialize a `quick.gate_matrix.Phase` instance. - """ - super().__init__( - f"Phase({theta})", - np.array([ - [1, 0], - [0, np.exp(1j * theta)] - ]) - ) \ No newline at end of file + return Operator( + label=f"U3({theta}, {phi}, {lam})", + data=np.array([ + [np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)], + [np.exp(1j * phi) * np.sin(theta / 2), np.exp(1j * (phi + lam)) * np.cos(theta / 2)] + ]) + ) \ No newline at end of file diff --git a/quick/circuit/pennylanecircuit.py b/quick/circuit/pennylanecircuit.py index c5f79cf..b1f6124 100644 --- a/quick/circuit/pennylanecircuit.py +++ b/quick/circuit/pennylanecircuit.py @@ -158,8 +158,7 @@ def _gate_mapping( self.circuit.append( qml.ControlledQubitUnitary( gate_operation, - control_wires=controls, - wires=target_index + wires=controls + [target_index] ) ) return @@ -234,6 +233,8 @@ def get_counts( np.random.seed(0) + num_qubits_to_measure = len(self.measured_qubits) + if len(self.measured_qubits) == 0: raise ValueError("At least one qubit must be measured.") @@ -245,7 +246,7 @@ def compile_circuit() -> qml.measurements.CountsMp: qml.apply(op) - return qml.counts(wires=self.measured_qubits, all_outcomes=True) + return qml.counts(wires=self.measured_qubits) if backend is None: device = qml.device(self.device.name, wires=self.num_qubits, shots=num_shots) @@ -254,6 +255,13 @@ def compile_circuit() -> qml.measurements.CountsMp: list(result.keys())[i]: int(list(result.values())[i]) for i in range(len(result)) } + for i in range(2**num_qubits_to_measure): + basis = format(int(i),"0{}b".format(num_qubits_to_measure)) + if basis not in counts: + counts[basis] = 0 + else: + counts[basis] = int(counts[basis]) + # Sort the counts by their keys (basis states) # This is simply for readability counts = dict(sorted(counts.items())) diff --git a/quick/circuit/qiskitcircuit.py b/quick/circuit/qiskitcircuit.py index c091b37..1875a4f 100644 --- a/quick/circuit/qiskitcircuit.py +++ b/quick/circuit/qiskitcircuit.py @@ -216,7 +216,7 @@ def get_counts( # This is to counter https://github.com/Qiskit/qiskit/issues/13162 circuit.transpile() - # If no backend is provided, use the AerSimualtor + # If no backend is provided, use the AerSimulator base_backend: BackendSampler = BackendSampler(backend=AerSimulator()) result = base_backend.run([circuit.circuit], shots=num_shots).result() diff --git a/quick/circuit/circuit_utils.py b/quick/circuit/utils.py similarity index 100% rename from quick/circuit/circuit_utils.py rename to quick/circuit/utils.py diff --git a/quick/compiler/shende_compiler.py b/quick/compiler/shende_compiler.py index 358c326..b95c864 100644 --- a/quick/compiler/shende_compiler.py +++ b/quick/compiler/shende_compiler.py @@ -25,15 +25,15 @@ from typing import TypeAlias from quick.circuit import Circuit -from quick.primitives import Bra, Ket, Operator +from quick.primitives import Statevector, Operator from quick.synthesis.statepreparation import StatePreparation, Isometry from quick.synthesis.unitarypreparation import UnitaryPreparation, ShannonDecomposition """ Type aliases for the primitives to be compiled: -- `PRIMITIVE` is a single primitive object, which can be a `Bra`, `Ket`, `Operator`,or a `numpy.ndarray`. +- `PRIMITIVE` is a single primitive object, which can be a `Statevector`, `Operator`,or a `numpy.ndarray`. - `PRIMITIVES` is a list of tuples containing the primitive object and the qubits they need to be applied to. """ -PRIMITIVE: TypeAlias = Bra | Ket | Operator | NDArray[np.complex128] +PRIMITIVE: TypeAlias = Statevector | Operator | NDArray[np.complex128] PRIMITIVES: TypeAlias = list[tuple[PRIMITIVE, Sequence[int]]] @@ -105,13 +105,13 @@ def __init__( def state_preparation( self, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, ) -> Circuit: """ Prepare a quantum state. Parameters ---------- - `state` : NDArray[np.complex128] | quick.primitives.Bra | quick.primitives.Ket + `state` : NDArray[np.complex128] | quick.primitives.Statevector The quantum state to be prepared. Returns @@ -161,11 +161,11 @@ def _check_primitive(primitive: PRIMITIVE) -> None: ValueError - If the primitive object is invalid. """ - if not isinstance(primitive, (Bra, Ket, Operator, np.ndarray)): + if not isinstance(primitive, (Statevector, Operator, np.ndarray)): raise TypeError("Invalid primitive object.") if isinstance(primitive, np.ndarray): - if len(primitive.flatten()) < 2: + if len(primitive.ravel()) < 2: raise ValueError("Invalid primitive object.") elif primitive.ndim not in [1, 2]: raise ValueError("Invalid primitive object.") @@ -246,13 +246,13 @@ def _compile_primitive( """ self._check_primitive(primitive) - if isinstance(primitive, (Bra, Ket)): + if isinstance(primitive, Statevector): return self.state_preparation(primitive) elif isinstance(primitive, Operator): return self.unitary_preparation(primitive) elif isinstance(primitive, np.ndarray): if primitive.ndim == 1: - return self.state_preparation(Ket(primitive)) + return self.state_preparation(Statevector(primitive)) else: return self.unitary_preparation(Operator(primitive)) @@ -286,8 +286,8 @@ def compile( ----- >>> primitive = Operator(unitary_matrix) >>> circuit = compiler.compile(primitive) - >>> primitives = [(Bra(bra_vector), [0, 1]), - ... (Ket(ket_vector), [2, 3])] + >>> primitives = [(Operator(unitary_matrix), [0, 1]), + ... (Statevector(statevector), [2, 3])] >>> circuit = compiler.compile(primitives) """ if isinstance(primitives, list): diff --git a/quick/metrics/__init__.py b/quick/metrics/__init__.py index 2a80faa..1b6e1ed 100644 --- a/quick/metrics/__init__.py +++ b/quick/metrics/__init__.py @@ -17,7 +17,8 @@ "calculate_shannon_entropy", "calculate_entanglement_entropy", "calculate_entanglement_entropy_slope", - "calculate_hilbert_schmidt_test" + "calculate_hilbert_schmidt_test", + "calculate_frobenius_distance" ] from quick.metrics.metrics import ( @@ -25,5 +26,6 @@ calculate_shannon_entropy, calculate_entanglement_entropy, calculate_entanglement_entropy_slope, - calculate_hilbert_schmidt_test + calculate_hilbert_schmidt_test, + calculate_frobenius_distance ) \ No newline at end of file diff --git a/quick/metrics/metrics.py b/quick/metrics/metrics.py index a6ba4c0..1e13477 100644 --- a/quick/metrics/metrics.py +++ b/quick/metrics/metrics.py @@ -28,9 +28,9 @@ import numpy as np from numpy.typing import NDArray import quimb.tensor as qtn # type: ignore -from qiskit.quantum_info import partial_trace # type: ignore from quick.predicates import is_density_matrix, is_statevector, is_unitary_matrix +from quick.primitives import Statevector def _calculate_1d_entanglement_range(mps: qtn.MatrixProductState) -> list[tuple[int, int]]: @@ -222,23 +222,19 @@ def calculate_entanglement_entropy_slope(statevector: NDArray[np.complex128]) -> ----- >>> entanglement_entropy_slope = calculate_entanglement_entropy_slope(statevector) """ - if not is_statevector(statevector): - raise ValueError("The input must be a statevector.") - - num_qubits = int( - np.ceil( - np.log2(len(statevector)) - ) - ) + if not isinstance(statevector, Statevector): + state = Statevector(statevector) + else: + state = statevector - max_k = num_qubits // 2 + max_k = state.num_qubits // 2 entropies = np.empty(max_k, dtype=np.float64) for k in range(1, max_k + 1): # Trace out rest of the qubits to extract the # reduced density matrix for the first k qubits - rho_A = partial_trace(statevector, list(range(k, num_qubits))) # type: ignore - S = calculate_entanglement_entropy(rho_A.data) + rho = state.partial_trace(list(range(k, state.num_qubits))) + S = calculate_entanglement_entropy(np.array(rho)) entropies[k - 1] = S # We use half of the entropies to calculate the slope @@ -299,4 +295,36 @@ def calculate_hilbert_schmidt_test( ) )**2 - return chst \ No newline at end of file + return chst + +def calculate_frobenius_distance( + matrix_1: NDArray[np.complex128], + matrix_2: NDArray[np.complex128] + ) -> float: + """ Calculate the Frobenius distance between two matrices. + + Parameters + ---------- + `matrix_1` : NDArray[np.complex128] + The first matrix. + `matrix_2` : NDArray[np.complex128] + The second matrix. + + Returns + ------- + float + The Frobenius distance between the two matrices. + + Raises + ------ + ValueError + - If the matrices are not of the same shape. + + Usage + ----- + >>> frobenius_distance = calculate_frobenius_distance(matrix_1, matrix_2) + """ + if matrix_1.shape != matrix_2.shape: + raise ValueError("The matrices must be of the same shape.") + + return float(np.linalg.norm(matrix_1 - matrix_2, 'fro')) \ No newline at end of file diff --git a/quick/predicates/__init__.py b/quick/predicates/__init__.py index 7423163..ae673fe 100644 --- a/quick/predicates/__init__.py +++ b/quick/predicates/__init__.py @@ -13,8 +13,15 @@ # limitations under the License. __all__ = [ + "is_power", + "is_normalized", "is_statevector", "is_square_matrix", + "is_orthogonal_matrix", + "is_real_matrix", + "is_special_matrix", + "is_special_orthogonal_matrix", + "is_special_unitary_matrix", "is_diagonal_matrix", "is_symmetric_matrix", "is_identity_matrix", @@ -22,12 +29,22 @@ "is_hermitian_matrix", "is_positive_semidefinite_matrix", "is_isometry", - "is_density_matrix" + "is_density_matrix", + "is_product_matrix", + "is_locally_equivalent", + "is_supercontrolled" ] from quick.predicates.predicates import ( + is_power, + is_normalized, is_statevector, is_square_matrix, + is_orthogonal_matrix, + is_real_matrix, + is_special_matrix, + is_special_orthogonal_matrix, + is_special_unitary_matrix, is_diagonal_matrix, is_symmetric_matrix, is_identity_matrix, @@ -35,5 +52,8 @@ is_hermitian_matrix, is_positive_semidefinite_matrix, is_isometry, - is_density_matrix + is_density_matrix, + is_product_matrix, + is_locally_equivalent, + is_supercontrolled ) \ No newline at end of file diff --git a/quick/predicates/predicates.py b/quick/predicates/predicates.py index c96ed7f..242fed7 100644 --- a/quick/predicates/predicates.py +++ b/quick/predicates/predicates.py @@ -18,8 +18,15 @@ from __future__ import annotations __all__ = [ + "is_power", + "is_normalized", "is_statevector", "is_square_matrix", + "is_orthogonal_matrix", + "is_real_matrix", + "is_special_matrix", + "is_special_orthogonal_matrix", + "is_special_unitary_matrix", "is_diagonal_matrix", "is_symmetric_matrix", "is_identity_matrix", @@ -27,7 +34,10 @@ "is_hermitian_matrix", "is_positive_semidefinite_matrix", "is_isometry", - "is_density_matrix" + "is_density_matrix", + "is_product_matrix", + "is_locally_equivalent", + "is_supercontrolled" ] import numpy as np @@ -38,7 +48,7 @@ RTOL_DEFAULT = 1e-5 -def _is_power( +def is_power( base: int, number: int ) -> bool: @@ -57,7 +67,34 @@ def _is_power( True if the number is a power of the base, False otherwise. """ result = math.log(number) / math.log(base) - return result == math.floor(result) + return bool(result == math.floor(result)) + +def is_normalized( + statevector: NDArray[np.complex128], + rtol: float = RTOL_DEFAULT, + atol: float = ATOL_DEFAULT + ) -> bool: + """ Test if an array is normalized. + + Parameters + ---------- + `statevector` : NDArray[np.complex128] + The input statevector. + `rtol` : float, optional, default=RTOL_DEFAULT + The relative tolerance parameter. + `atol` : float, optional, default=ATOL_DEFAULT + The absolute tolerance parameter. + + Returns + ------- + bool + True if the array is normalized, False otherwise. + + Usage + ----- + >>> is_normalized(np.array([1, 0])) + """ + return bool(np.isclose(np.linalg.norm(statevector), 1.0, rtol=rtol, atol=atol)) def is_statevector( statevector: NDArray[np.complex128], @@ -97,15 +134,15 @@ def is_statevector( if system_size < 2: raise ValueError("System size must be greater than or equal to 2.") - if not _is_power(system_size, len(statevector)): + if not is_power(system_size, len(statevector)): return False if statevector.ndim == 2: if statevector.shape[1] == 1: - statevector = statevector.flatten() + statevector = statevector.ravel() - return ( - bool(np.isclose(np.linalg.norm(statevector), 1.0, rtol=rtol, atol=atol)) + return bool( + is_normalized(statevector, rtol=rtol, atol=atol) and statevector.ndim == 1 and len(statevector) > 1 ) @@ -130,7 +167,152 @@ def is_square_matrix(matrix: NDArray[np.complex128]) -> bool: if matrix.ndim != 2: return False shape = matrix.shape - return shape[0] == shape[1] + return bool(shape[0] == shape[1]) + +def is_orthogonal_matrix( + matrix: NDArray[np.complex128], + rtol: float = RTOL_DEFAULT, + atol: float = ATOL_DEFAULT + ) -> bool: + """ Test if an array is an orthogonal matrix. + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The input matrix. + `rtol` : float, optional, default=RTOL_DEFAULT + The relative tolerance parameter. + `atol` : float, optional, default=ATOL_DEFAULT + The absolute tolerance parameter. + + Returns + ------- + bool + True if the matrix is an orthogonal matrix, False otherwise. + + Usage + ----- + >>> is_orthogonal_matrix(np.eye(2)) + """ + return bool(np.allclose(matrix.T, np.linalg.inv(matrix), rtol=rtol, atol=atol)) + +def is_real_matrix( + matrix: NDArray[np.complex128], + rtol: float = RTOL_DEFAULT, + atol: float = ATOL_DEFAULT + ) -> bool: + """ Test if an array is a real matrix. + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The input matrix. + `rtol` : float, optional, default=RTOL_DEFAULT + The relative tolerance parameter. + `atol` : float, optional, default=ATOL_DEFAULT + The absolute tolerance parameter. + + Returns + ------- + bool + True if the matrix is a real matrix, False otherwise. + + Usage + ----- + >>> is_real_matrix(np.eye(2)) + """ + return bool(np.allclose(matrix, matrix.real, rtol=rtol, atol=atol)) + +def is_special_matrix( + matrix: NDArray[np.complex128], + rtol: float = RTOL_DEFAULT, + atol: float = ATOL_DEFAULT + ) -> bool: + """ Test if an array is a special matrix (i.e., has determinant 1). + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The input matrix. + `rtol` : float, optional, default=RTOL_DEFAULT + The relative tolerance parameter. + `atol` : float, optional, default=ATOL_DEFAULT + The absolute tolerance parameter. + + Returns + ------- + bool + True if the matrix is a special matrix, False otherwise. + + Usage + ----- + >>> is_special_matrix(np.eye(2)) + """ + if not is_square_matrix(matrix): + return False + + det = np.linalg.det(matrix) + return bool(np.isclose(det, 1.0, rtol=rtol, atol=atol)) + +def is_special_orthogonal_matrix( + matrix: NDArray[np.float64], + rtol: float = RTOL_DEFAULT, + atol: float = ATOL_DEFAULT + ) -> bool: + """ Test if an array is a special orthogonal matrix. + + Parameters + ---------- + `matrix` : NDArray[np.float64] + The input matrix. + `rtol` : float, optional, default=RTOL_DEFAULT + The relative tolerance parameter. + `atol` : float, optional, default=ATOL_DEFAULT + The absolute tolerance parameter. + + Returns + ------- + bool + True if the matrix is a special orthogonal matrix, False otherwise. + + Usage + ----- + >>> is_so(np.eye(2)) + """ + return bool( + is_special_matrix(matrix.astype(complex), rtol=rtol, atol=atol) + and is_orthogonal_matrix(matrix.astype(complex), rtol=rtol, atol=atol) + ) + +def is_special_unitary_matrix( + matrix: NDArray[np.complex128], + rtol: float = RTOL_DEFAULT, + atol: float = ATOL_DEFAULT + ) -> bool: + """ Test if an array is a special unitary matrix. + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The input matrix. + `rtol` : float, optional, default=RTOL_DEFAULT + The relative tolerance parameter. + `atol` : float, optional, default=ATOL_DEFAULT + The absolute tolerance parameter. + + Returns + ------- + bool + True if the matrix is a special unitary matrix, False otherwise. + + Usage + ----- + >>> is_su(np.eye(2)) + """ + return bool( + is_special_matrix(matrix, rtol=rtol, atol=atol) + and is_unitary_matrix(matrix, rtol=rtol, atol=atol) + ) def is_diagonal_matrix( matrix: NDArray[np.complex128], @@ -160,7 +342,7 @@ def is_diagonal_matrix( if not is_square_matrix(matrix): return False - return np.allclose(matrix, np.diag(np.diagonal(matrix)), rtol=rtol, atol=atol) + return bool(np.allclose(matrix, np.diag(np.diagonal(matrix)), rtol=rtol, atol=atol)) def is_symmetric_matrix( matrix: NDArray[np.complex128], @@ -190,7 +372,7 @@ def is_symmetric_matrix( if not is_square_matrix(matrix): return False - return np.allclose(matrix, matrix.T, rtol=rtol, atol=atol) + return bool(np.allclose(matrix, matrix.T, rtol=rtol, atol=atol)) def is_identity_matrix( matrix: NDArray[np.complex128], @@ -231,7 +413,7 @@ def is_identity_matrix( matrix = np.exp(-1j * theta) * matrix identity = np.eye(len(matrix)) - return np.allclose(matrix, identity, rtol=rtol, atol=atol) + return bool(np.allclose(matrix, identity, rtol=rtol, atol=atol)) def is_unitary_matrix( matrix: NDArray[np.complex128], @@ -262,7 +444,7 @@ def is_unitary_matrix( return False matrix = matrix.conj().T @ matrix - return is_identity_matrix(matrix, ignore_phase=False, rtol=rtol, atol=atol) + return bool(is_identity_matrix(matrix, ignore_phase=False, rtol=rtol, atol=atol)) def is_hermitian_matrix( matrix: NDArray[np.complex128], @@ -292,7 +474,7 @@ def is_hermitian_matrix( if not is_square_matrix(matrix): return False - return np.allclose(matrix, matrix.conj().T, rtol=rtol, atol=atol) + return bool(np.allclose(matrix, matrix.conj().T, rtol=rtol, atol=atol)) def is_positive_semidefinite_matrix( matrix: NDArray[np.complex128], @@ -359,7 +541,7 @@ def is_isometry( identity = np.eye(matrix.shape[1]) matrix = matrix.conj().T @ matrix - return np.allclose(matrix, identity, rtol=rtol, atol=atol) + return bool(np.allclose(matrix, identity, rtol=rtol, atol=atol)) def is_density_matrix( rho: NDArray[np.complex128], @@ -386,11 +568,123 @@ def is_density_matrix( ----- >>> is_density_matrix(np.eye(2)) """ - if not ( + if not bool( is_hermitian_matrix(rho, rtol=rtol, atol=atol) and is_positive_semidefinite_matrix(rho, rtol=rtol, atol=atol) and np.isclose(np.trace(rho), 1.0, rtol=rtol, atol=atol) ): return False - return True \ No newline at end of file + return True + +def is_product_matrix(matrix: NDArray[np.complex128]) -> bool: + """ Test if a two-qubit unitary is a product matrix. + + A two-qubit gate is a product matrix if it can be expressed as + the Kronecker product of two single-qubit gates. + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The input two-qubit unitary matrix. + + Returns + ------- + bool + True if the matrix is a product matrix, False otherwise. + + Raises + ------ + ValueError + - If the input matrix is not a two-qubit unitary. + + Usage + ----- + >>> from quick.synthesis.gate_decompositions.two_qubit_decomposition import swap + >>> is_product_matrix(swap()) + False + """ + from quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl import weyl_coordinates + + if not is_unitary_matrix(matrix) or matrix.shape != (4, 4): + raise ValueError("Input matrix must be a 4x4 unitary matrix.") + + x, y, z = weyl_coordinates(matrix) + + return bool(np.isclose(x, 0) and np.isclose(y, 0) and np.isclose(z, 0)) + +def is_locally_equivalent( + matrix1: NDArray[np.complex128], + matrix2: NDArray[np.complex128] + ) -> bool: + """ Test if two two-qubit unitaries are locally equivalent. + Two two-qubit gates are locally equivalent if they differ only + by single-qubit gates. + + Parameters + `matrix1` : NDArray[np.complex128] + The first input two-qubit unitary matrix. + `matrix2` : NDArray[np.complex128] + The second input two-qubit unitary matrix. + + Returns + ------- + bool + True if the matrices are locally equivalent, False otherwise. + + Raises + ------ + ValueError + - If either input matrix is not a two-qubit unitary. + + Usage + ----- + >>> is_locally_equivalent(cx, cz) + """ + from quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl import weyl_coordinates + + if not is_unitary_matrix(matrix1) or matrix1.shape != (4, 4): + raise ValueError("First input matrix must be a 4x4 unitary matrix.") + if not is_unitary_matrix(matrix2) or matrix2.shape != (4, 4): + raise ValueError("Second input matrix must be a 4x4 unitary matrix.") + + x1, y1, z1 = weyl_coordinates(matrix1) + x2, y2, z2 = weyl_coordinates(matrix2) + + return bool(np.isclose(x1, x2) and np.isclose(y1, y2) and np.isclose(z1, z2)) + +def is_supercontrolled(matrix: NDArray[np.complex128]) -> bool: + """ Test if a two-qubit unitary is a supercontrolled gate. + + A two-qubit gate is supercontrolled if its Weyl coordinates + are of the form (pi/4, alpha, 0) up to local equivalence. + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The input two-qubit unitary matrix. + + Returns + ------- + bool + True if the matrix is a supercontrolled gate, False otherwise. + + Raises + ------ + ValueError + - If the input matrix is not a two-qubit unitary. + + Usage + ----- + >>> from quick.synthesis.gate_decompositions.two_qubit_decomposition import cnot + >>> is_supercontrolled(cnot()) + True + """ + from quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl import weyl_coordinates + + if not is_unitary_matrix(matrix) or matrix.shape != (4, 4): + raise ValueError("Input matrix must be a 4x4 unitary matrix.") + + x, _, z = weyl_coordinates(matrix) + + return bool(np.isclose(x, np.pi / 4) and np.isclose(z, 0)) \ No newline at end of file diff --git a/quick/primitives/__init__.py b/quick/primitives/__init__.py index 5a192ab..5378590 100644 --- a/quick/primitives/__init__.py +++ b/quick/primitives/__init__.py @@ -13,11 +13,9 @@ # limitations under the License. __all__ = [ - "Bra", - "Ket", + "Statevector", "Operator" ] -from quick.primitives.bra import Bra -from quick.primitives.ket import Ket +from quick.primitives.statevector import Statevector from quick.primitives.operator import Operator \ No newline at end of file diff --git a/quick/primitives/bra.py b/quick/primitives/bra.py deleted file mode 100644 index 5feb566..0000000 --- a/quick/primitives/bra.py +++ /dev/null @@ -1,616 +0,0 @@ -# Copyright 2023-2025 Qualition Computing LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://github.com/Qualition/quick/blob/main/LICENSE -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Bra vector class for representing bra states. -""" - -from __future__ import annotations - -__all__ = ["Bra"] - -import numpy as np -from numpy.typing import NDArray -from typing import Any, Literal, overload, SupportsFloat, TypeAlias - -import quick.primitives.operator as operator -import quick.primitives.ket as ket - -# `Scalar` is a type alias that represents a scalar value that can be either -# a real number or a complex number. -Scalar: TypeAlias = SupportsFloat | complex - - -class Bra: - """ `quick.primitives.Bra` is a class that represents a quantum bra vector. Bra vectors are - complex, row vectors with a magnitude of 1 which represent quantum states. The bra vectors are - the complex conjugates of the ket vectors. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The bra vector data. The data will be normalized to 2-norm and padded if necessary. - `label` : str, optional - The label of the bra vector. - - Attributes - ---------- - `label` : str, optional, default="Ψ" - The label of the bra vector. - `data` : NDArray[np.complex128] - The bra vector data. - `norm_scale` : np.float64 - The normalization scale. - `normalized` : bool - Whether the bra vector is normalized to 2-norm or not. - `shape` : Tuple[int, int] - The shape of the bra vector. - `num_qubits` : int - The number of qubits represented by the bra vector. - - Raises - ------ - ValueError - - If the data is a scalar or an operator. - - Usage - ----- - >>> data = np.array([1, 2, 3, 4]) - >>> bra = Bra(data) - """ - def __init__( - self, - data: NDArray[np.complex128], - label: str | None = None - ) -> None: - """ Initialize a `quick.primitives.Bra` instance. - """ - if label is None: - self.label = "\N{GREEK CAPITAL LETTER PSI}" - else: - self.label = label - - self.norm_scale = np.linalg.norm(data) - self.data = data - self.shape = data.shape - self.num_qubits = int(np.ceil(np.log2(len(data.flatten())))) - self.is_normalized() - self.is_padded() - self.to_bra(data) - - @staticmethod - def check_normalization(data: NDArray[np.complex128]) -> bool: - """ Check if a data is normalized to 2-norm. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - - Returns - ------- - bool - Whether the vector is normalized to 2-norm or not. - - Usage - ----- - >>> data = np.array([1, 2, 3, 4]) - >>> check_normalization(data) - """ - # Check whether the data is normalized to 2-norm - sum_check = np.sum(np.power(data, 2)) - - # Check if the sum of squared of the data elements is equal to - # 1 with 1e-8 tolerance - return bool(np.isclose(sum_check, 1.0, atol=1e-08)) - - def is_normalized(self) -> None: - """ Check if a `quick.primitives.Bra` instance is normalized to 2-norm. - - Usage - ----- - >>> data.is_normalized() - """ - self.normalized = self.check_normalization(self.data) - - @staticmethod - def normalize_data( - data: NDArray[np.complex128], - norm_scale: np.float64 - ) -> NDArray[np.complex128]: - """ Normalize the data to 2-norm, and return the normalized data. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - `norm_scale` : np.float64 - The normalization scale. - - Returns - ------- - NDArray[np.complex128] - The 2-norm normalized data. - - Usage - ----- - >>> data = np.array([[1, 2], - ... [3, 4]]) - >>> norm_scale = np.linalg.norm(data.flatten()) - >>> normalize_data(data, norm_scale) - """ - return np.multiply(data, 1/norm_scale) - - def normalize(self) -> None: - """ Normalize a `quick.primitives.Bra` instance to 2-norm. - """ - if self.normalized: - return - - self.data = self.normalize_data(self.data, self.norm_scale) - self.normalized = True - - @staticmethod - def check_padding(data: NDArray[np.complex128]) -> bool: - """ Check if a data is normalized to 2-norm. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - - Returns - ------- - bool - Whether the vector is normalized to 2-norm or not. - - Usage - ----- - >>> data = np.array([[1, 2], [3, 4]]) - >>> check_padding(data) - """ - return (data.shape[0] & (data.shape[0]-1) == 0) and data.shape[0] != 0 - - def is_padded(self) -> None: - """ Check if a `quick.data.Data` instance is padded to a power of 2. - - Usage - ----- - >>> data.is_padded() - """ - self.padded = self.check_padding(self.data) - - @staticmethod - def pad_data( - data: NDArray[np.complex128], - target_size: int - ) -> tuple[NDArray[np.complex128], tuple[int, ...]]: - """ Pad data with zeros up to the nearest power of 2, and return - the padded data. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data to be padded. - `target_size` : int - The target size to pad the data to. - - Returns - ------- - `padded_data` : NDArray[np.complex128] - The padded data. - `data_shape` : (tuple[int, ...]) - The updated shape. - - Usage - ----- - >>> data = np.array([1, 2, 3]) - >>> pad_data(data, 4) - """ - padded_data = np.pad( - data, (0, int(target_size - len(data))), - mode="constant" - ) - updated_shape = padded_data.shape - - return padded_data, updated_shape - - def pad(self) -> None: - """ Pad a `quick.data.Data` instance. - - Usage - ----- - >>> data.pad() - """ - if self.padded: - return - - self.data, self.shape = self.pad_data(self.data, np.exp2(self.num_qubits)) - self.padded = True - - def to_quantumstate(self) -> None: - """ Converts a `quick.data.Data` instance to a quantum state. - - Usage - ----- - >>> data.to_quantumstate() - """ - if not self.normalized: - self.normalize() - - if not self.padded: - self.pad() - - def to_bra( - self, - data: NDArray[np.complex128] - ) -> None: - """ Convert the data to a bra vector. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - - Raises - ------ - ValueError - - If the data is a scalar or an operator. - - Usage - ----- - >>> data = np.array([1, 2, 3, 4]) - >>> to_bra(data) - """ - if data.ndim == 0: - raise ValueError("Cannot convert a scalar to a bra.") - elif data.ndim == 1: - if data.shape[0] == 1: - raise ValueError("Cannot convert a scalar to a bra.") - else: - self.data = data - self.shape = self.data.shape - elif data.ndim == 2: - if data.shape[0] == 1: - if data.shape[1] == 1: - raise ValueError("Cannot convert a scalar to a bra.") - else: - self.data = data.reshape(1, -1)[0] - self.shape = self.data.shape - else: - raise ValueError("Cannot convert an operator to a bra.") - else: - raise ValueError("Cannot convert a N-dimensional array to a bra.") - - self.data = self.data.astype(np.complex128) - - # Normalize and pad the data to satisfy the quantum state requirements - self.to_quantumstate() - - def to_ket(self) -> ket.Ket: - """ Convert the bra vector to a ket vector. - - Returns - ------- - quick.primitives.Ket - The ket vector. - - Usage - ----- - >>> bra.to_ket() - """ - return ket.Ket(self.data.conj().reshape(1, -1)[0]) - - def compress( - self, - compression_percentage: float - ) -> None: - """ Compress a `quick.data.Data` instance. - - Parameters - ---------- - `compression_percentage` : float - The percentage of compression. - - Usage - ----- - >>> data.compress(50) - """ - data_sort_ind = np.argsort(np.abs(self.data)) - - # Set the smallest absolute values of data to zero according to compression parameter - cutoff = int((compression_percentage / 100.0) * len(self.data)) - for i in data_sort_ind[:cutoff]: - self.data[i] = 0 - - def change_indexing( - self, - index_type: Literal["row", "snake"] - ) -> None: - """ Change the indexing of a `quick.primitives.Bra` instance. - - Parameters - ---------- - `index_type` : Literal["row", "snake"] - The new indexing type, being "row" or "snake". - - Raises - ------ - ValueError - - If the index type is not supported. - - Usage - ----- - >>> data.change_indexing("snake") - """ - if index_type == "snake": - if self.num_qubits >= 3: - # Convert the bra vector to a matrix (image) - self.data = self.data.reshape(2, -1) - # Reverse the elements in odd rows - self.data[1::2, :] = self.data[1::2, ::-1] - - self.data = self.data.flatten() - elif index_type == "row": - self.data = self.data - else: - raise ValueError("Index type not supported.") - - def _check__mul__( - self, - other: Any - ) -> None: - """ Check if the multiplication is valid. - - Parameters - ---------- - `other` : Any - The other object to multiply with. - - Raises - ------ - ValueError - - If the two vectors are incompatible. - - If the the bra and operator are incompatible. - NotImplementedError - - If the `other` type is incompatible. - """ - if isinstance(other, (SupportsFloat, complex)): - return - elif isinstance(other, ket.Ket): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot contract two incompatible vectors.") - elif isinstance(other, operator.Operator): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot multiply two incompatible vectors.") - else: - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - - def __eq__( - self, - other: object - ) -> bool: - """ Check if two bra vectors are equal. - - Parameters - ---------- - `other` : object - The other bra vector. - - Returns - ------- - bool - Whether the two bra vectors are equal. - - Usage - ----- - >>> bra1 = Bra(np.array([1+0j, 0+0j])) - >>> bra2 = Bra(np.array([1+0j, 0+0j])) - >>> bra1 == bra2 - """ - if isinstance(other, Bra): - return bool(np.all(np.isclose(self.data, other.data, atol=1e-10, rtol=0))) - - raise NotImplementedError(f"Equality with {type(other)} is not supported.") - - def __len__(self) -> int: - """ Return the length of the bra vector. - - Returns - ------- - int - The length of the bra vector. - - Usage - ----- - >>> len(bra) - """ - return len(self.data) - - def __add__( - self, - other: Bra - ) -> Bra: - """ Superpose two bra states together. - - Parameters - ---------- - `other` : quick.primitives.Bra - The other bra state. - - Returns - ------- - quick.primitives.Bra - The superposed bra state. - - Raises - ------ - ValueError - - If the two bra states are incompatible. - - Usage - ----- - >>> bra1 = Bra(np.array([1+0j, 0+0j])) - >>> bra2 = Bra(np.array([1+0j, 0+0j])) - >>> bra1 + bra2 - """ - if isinstance(other, Bra): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot add two incompatible vectors.") - return Bra((self.data + other.data).astype(np.complex128)) - - raise NotImplementedError(f"Addition with {type(other)} is not supported.") - - @overload - def __mul__( - self, - other: Scalar - ) -> Bra: - ... - - @overload - def __mul__( - self, - other: ket.Ket - ) -> Scalar: - ... - - @overload - def __mul__( - self, - other: operator.Operator - ) -> Bra: - ... - - def __mul__( - self, - other: Scalar | ket.Ket | operator.Operator - ) -> Scalar | Bra: - """ Multiply the bra by a scalar, a ket, or an operator. - - The multiplication of a bra with a ket is defined as: - - ⟨ψ'|ψ⟩ = s, where s is a scalar - - The multiplication of a bra with an operator is defined as: - - ⟨ψ|A = ⟨ψ'| - - Notes - ----- - The multiplication of a bra with a scalar does not change the bra. This is because - the norm of the bra is preserved, and the scalar is multiplied with each element of the - bra. We provide the scalar multiplication for completeness. - - Parameters - ---------- - `other` : quick.primitives.Scalar | quick.primitives.Ket | quick.primitives.Operator - The other object to multiply the bra by. - - Returns - ------- - quick.primitives.Scalar | quick.primitives.Bra - The result of the multiplication. - - Raises - ------ - ValueError - - If the two vectors are incompatible. - - If the operator dimensions are incompatible. - NotImplementedError - - If the `other` type is incompatible. - - Usage - ----- - >>> scalar = 2 - >>> bra = Bra(np.array([1+0j, 0+0j])) - >>> bra * scalar - >>> bra = Bra(np.array([1+0j, 0+0j])) - >>> ket = Ket(np.array([1+0j, 0+0j])) - >>> bra * ket - >>> bra = Bra(np.array([1+0j, 0+0j])) - >>> operator = Operator([[1+0j, 0+0j], - ... [0+0j, 1+0j]]) - >>> bra * operator - """ - if isinstance(other, (SupportsFloat, complex)): - return Bra((self.data * other).astype(np.complex128)) # type: ignore - elif isinstance(other, ket.Ket): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot contract two incompatible vectors.") - return np.dot(self.data, other.data).flatten()[0] - elif isinstance(other, operator.Operator): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot multiply two incompatible vectors.") - return Bra(self.data @ other.data) - else: - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - - def __rmul__( - self, - other: Scalar - ) -> Bra: - """ Multiply the bra by a scalar. - - Notes - ----- - The multiplication of a bra with a scalar does not change the bra. This is because - the norm of the bra is preserved, and the scalar is multiplied with each element of the - bra. We provide the scalar multiplication for completeness. - - Parameters - ---------- - `other` : quick.primitives.Scalar - The scalar to multiply the bra by. - - Returns - ------- - quick.primitives.Bra - The bra multiplied by the scalar. - - Usage - ----- - >>> scalar = 2 - >>> bra = Bra(np.array([1+0j, 0+0j])) - >>> scalar * bra - """ - if isinstance(other, (SupportsFloat, complex)): - return Bra((self.data * other).astype(np.complex128)) # type: ignore - - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - - def __str__(self) -> str: - """ Return the string representation of the bra vector. - - Returns - ------- - str - The string representation of the bra vector. - - Usage - ----- - >>> str(bra) - """ - return f"⟨{self.label}|" - - def __repr__(self) -> str: - """ Return the string representation of the bra vector. - - Returns - ------- - str - The string representation of the bra vector. - - Usage - ----- - >>> repr(bra) - """ - return f"{self.__class__.__name__}(data={self.data}, label={self.label})" \ No newline at end of file diff --git a/quick/primitives/contraction.py b/quick/primitives/contraction.py new file mode 100644 index 0000000..b2f6899 --- /dev/null +++ b/quick/primitives/contraction.py @@ -0,0 +1,147 @@ +# Copyright 2023-2025 Qualition Computing LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://github.com/Qualition/quick/blob/main/LICENSE +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Contraction logic for `quick.primitives` classes. +""" + +from __future__ import annotations + +__all__ = ["contract"] + +import numpy as np +from numpy.typing import NDArray +from quick.primitives import Statevector, Operator + + +def _einsum_tensor_contract( + tensor: NDArray[np.complex128], + op: Operator, + contract_indices: list[int] + ) -> NDArray[np.complex128]: + """ Perform tensor contraction using Einstein's summation convention. + + Notes + ----- + The implementation is based on LSB convention. + + Parameters + ---------- + `tensor` : NDArray[np.complex128] + The tensor the operator is being contracted with. + `op` : quick.primitives.Operator + The operator to contract with the tensor. + `contract_indices` : list[int] + The indices to contract over. + + Returns + ------- + NDArray[np.complex128] + The result of the tensor contraction. + """ + rank = tensor.ndim + + # We tag the tensor indices that will be contracted with respect to + # the order in contract indices + tensor_indices = list(range(rank)) + for i, index in enumerate(contract_indices): + tensor_indices[index] = rank + i + + # Since the order is already taken into account in tensor indices, we + # only need to ensure the indices are present in a constant order + # regardless of different permutations + # The reason we reverse the order is because the gates themselves + # are in LSB convention as well + op_contract_indices = list( + range(rank + len(contract_indices) - 1, rank - 1, -1) + ) + + # The reason we reverse the order is because the gates themselves + # are in LSB convention as well + op_free_indices = contract_indices[::-1] + + op_indices = op_free_indices + op_contract_indices + + # Reshape the passed operator `op` to be a tensor with 2M indices + # of dimension 2 to represent the qubits, resulting in a + # rank-2M tensor where M <= N and 2N is the rank of `tensor` + op_tensor = np.reshape(op.data, op.tensor_shape) + + return np.einsum(tensor, tensor_indices, op_tensor, op_indices) # type: ignore + +def contract( + tensor: Statevector | Operator, + op: NDArray[np.complex128] | Operator, + qubit_indices: list[int] + ) -> None: + """ Contract the operator with an operator on the specified + qubits in place using Einstein's Summation convention. + + Parameters + ---------- + `tensor` : quick.primitives.Statevector | quick.primitives.Operator + The tensor to apply `op` to. + `op` : NDArray[np.complex128] | quick.primitives.Operator + The operator or matrix to contract with. + `qubit_indices` : list[int] + The qubit indices to contract over. + + Raises + ------ + ValueError + ValueError + - If the operator is not unitary. + - If the number of indices is less than the number of qubits for `op`. + - If the number of qubit indices exceeds the number of qubits in `tensor`. + - If any of the qubit indices are out of range of `self`. + + Usage + ----- + >>> tensor.contract(op, [0, 1]) + """ + op_tensor = Operator(np.array(op)) + + num_indices = len(qubit_indices) + + if op_tensor.num_qubits != num_indices: + raise ValueError( + f"Operator requires {op_tensor.num_qubits} qubits. ", + f"Received {num_indices} instead." + ) + + if num_indices > tensor.num_qubits: + raise ValueError( + f"{type(tensor).__name__} supports operators with at most {tensor.num_qubits} qubits." + f"Received an operator with {num_indices} instead." + ) + + if any(i >= tensor.num_qubits for i in qubit_indices): + raise ValueError( + f"Invalid qubit index in {qubit_indices}. " + f"Valid indices are in range(0, {tensor.num_qubits})." + ) + + # Modify the indices to be MSB for correct alignment given how numpy does broadcasting + op_contract_indices = [tensor.num_qubits - 1 - i for i in qubit_indices] + + # Reshape the current operator to be a tensor with X indices + # of dimension 2 to represent the qubits, resulting in a + # rank-X tensor + # For statevectors X is the number of qubits N whereas for operators + # X is 2N + reshaped_tensor = np.reshape(tensor.data, tensor.tensor_shape) + + tensor.data = np.reshape( + _einsum_tensor_contract(reshaped_tensor, op_tensor, op_contract_indices), + tensor.shape + ) \ No newline at end of file diff --git a/quick/primitives/ket.py b/quick/primitives/ket.py deleted file mode 100644 index a7a296c..0000000 --- a/quick/primitives/ket.py +++ /dev/null @@ -1,621 +0,0 @@ -# Copyright 2023-2025 Qualition Computing LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://github.com/Qualition/quick/blob/main/LICENSE -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Ket vector class for representing ket states. -""" - -from __future__ import annotations - -__all__ = ["Ket"] - -import numpy as np -from numpy.typing import NDArray -from typing import Any, Literal, overload, SupportsFloat, TypeAlias - -import quick.primitives.operator as operator -import quick.primitives.bra as bra - -# `Scalar` is a type alias that represents a scalar value that can be either -# a real number or a complex number. -Scalar: TypeAlias = SupportsFloat | complex - - -class Ket: - """ `quick.primitives.Ket` is a class that represents a quantum ket vector. Ket vectors are - complex, column vectors with a magnitude of 1 which represent quantum states. The ket vectors are - the complex conjugates of the bra vectors. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The ket vector data. The data will be normalized to 2-norm and padded if necessary. - `label` : str, optional - The label of the ket vector. - - Attributes - ---------- - `label` : str, optional, default="Ψ" - The label of the ket vector. - `data` : NDArray[np.complex128] - The ket vector data. - `norm_scale` : np.float64 - The normalization scale. - `normalized` : bool - Whether the ket vector is normalized to 2-norm or not. - `shape` : Tuple[int, int] - The shape of the ket vector. - `num_qubits` : int - The number of qubits represented by the ket vector. - - Raises - ------ - ValueError - - If the data is a scalar or an operator. - - Usage - ----- - >>> data = np.array([1, 2, 3, 4]) - >>> ket = Ket(data) - """ - def __init__( - self, - data: NDArray[np.complex128], - label: str | None = None - ) -> None: - """ Initialize a `quick.primitives.Ket` instance. - """ - if label is None: - self.label = "\N{GREEK CAPITAL LETTER PSI}" - else: - self.label = label - - self.norm_scale = np.linalg.norm(data.flatten()) - self.data = data - self.shape = data.shape - self.num_qubits = int(np.ceil(np.log2(self.shape[0]))) - self.is_normalized() - self.is_padded() - self.to_ket(data) - - @staticmethod - def check_normalization(data: NDArray[np.complex128]) -> bool: - """ Check if a data is normalized to 2-norm. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - - Returns - ------- - bool - Whether the vector is normalized to 2-norm or not. - - Usage - ----- - >>> data = np.array([1, 2, 3, 4]) - >>> check_normalization(data) - """ - # Check whether the data is normalized to 2-norm - sum_check = np.sum(np.power(data, 2)) - - # Check if the sum of squared of the data elements is equal to - # 1 with 1e-8 tolerance - return bool(np.isclose(sum_check, 1.0, atol=1e-08)) - - def is_normalized(self) -> None: - """ Check if a `quick.primitives.Bra` instance is normalized to 2-norm. - - Usage - ----- - >>> data.is_normalized() - """ - self.normalized = self.check_normalization(self.data) - - @staticmethod - def normalize_data( - data: NDArray[np.complex128], - norm_scale: np.float64 - ) -> NDArray[np.complex128]: - """ Normalize the data to 2-norm, and return the normalized data. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - `norm_scale` : np.float64 - The normalization scale. - - Returns - ------- - NDArray[np.complex128] - The 2-norm normalized data. - - Usage - ----- - >>> data = np.array([[1, 2], - ... [3, 4]]) - >>> norm_scale = np.linalg.norm(data.flatten()) - >>> normalize_data(data, norm_scale) - """ - return np.multiply(data, 1/norm_scale) - - def normalize(self) -> None: - """ Normalize a `quick.primitives.Ket` instance to 2-norm. - """ - if self.normalized: - return - - self.data = self.normalize_data(self.data, self.norm_scale) - self.normalized = True - - @staticmethod - def check_padding(data: NDArray[np.complex128]) -> bool: - """ Check if a data is normalized to 2-norm. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - - Returns - ------- - bool - Whether the vector is normalized to 2-norm or not. - - Usage - ----- - >>> data = np.array([[1, 2], [3, 4]]) - >>> check_padding(data) - """ - return (data.shape[0] & (data.shape[0]-1) == 0) and data.shape[0] != 0 - - def is_padded(self) -> None: - """ Check if a `quick.data.Data` instance is padded to a power of 2. - - Usage - ----- - >>> data.is_padded() - """ - self.padded = self.check_padding(self.data) - - @staticmethod - def pad_data( - data: NDArray[np.complex128], - target_size: int - ) -> tuple[NDArray[np.complex128], tuple[int, ...]]: - """ Pad data with zeros up to the nearest power of 2, and return - the padded data. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data to be padded. - `target_size` : int - The target size to pad the data to. - - Returns - ------- - `padded_data` : NDArray[np.complex128] - The padded data. - `data_shape` : (tuple[int, ...]) - The updated shape. - - Usage - ----- - >>> data = np.array([[1, 2], [3, 4]]) - >>> pad_data(data) - """ - flattened_data = data.flatten() - - padded_data = np.pad( - flattened_data, (0, int(target_size - len(flattened_data))), - mode="constant" - ).reshape(-1, 1) - - updated_shape = padded_data.shape - - return padded_data, updated_shape - - def pad(self) -> None: - """ Pad a `quick.data.Data` instance. - - Usage - ----- - >>> data.pad() - """ - if self.padded: - return - - self.data, self.shape = self.pad_data(self.data, np.exp2(self.num_qubits)) - self.padded = True - - def to_quantumstate(self) -> None: - """ Converts a `quick.data.Data` instance to a quantum state. - - Usage - ----- - >>> data.to_quantumstate() - """ - if not self.normalized: - self.normalize() - - if not self.padded: - self.pad() - - def to_ket( - self, - data: NDArray[np.complex128] - ) -> None: - """ Convert the data to a ket vector. - - Parameters - ---------- - `data` : NDArray[np.complex128] - The data. - - Raises - ------ - ValueError - - If the data is a scalar or an operator. - - Usage - ----- - >>> ket.to_ket(data) - """ - if data.ndim == 0: - raise ValueError("Cannot convert a scalar to a ket.") - elif data.ndim == 1: - if data.shape[0] == 1: - raise ValueError("Cannot convert a scalar to a ket.") - else: - self.data = data.reshape(-1, 1) - elif data.ndim == 2: - if data.shape[1] == 1: - if data.shape[0] == 1: - raise ValueError("Cannot convert a scalar to a ket.") - else: - self.data = data - else: - raise ValueError("Cannot convert an operator to a ket.") - else: - raise ValueError("Cannot convert a N-dimensional array to a ket.") - - self.data = self.data.astype(np.complex128) - - # Normalize and pad the data to satisfy the quantum state requirements - self.to_quantumstate() - - def to_bra(self) -> bra.Bra: - """ Convert the ket to a bra. - - Returns - ------- - quick.primitives.Bra - The bra vector. - - Usage - ----- - >>> ket.to_bra() - """ - return bra.Bra(self.data.conj().reshape(1, -1)) # type: ignore - - def compress( - self, - compression_percentage: float - ) -> None: - """ Compress a `quick.data.Data` instance. - - Parameters - ---------- - `compression_percentage` : float - The percentage of compression. - - Usage - ----- - >>> data.compress(50) - """ - flattened_data = self.data.flatten() - data_sort_ind = np.argsort(np.abs(flattened_data)) - - # Set the smallest absolute values of data to zero according to compression parameter - cutoff = int((compression_percentage / 100.0) * len(flattened_data)) - for i in data_sort_ind[:cutoff]: - flattened_data[i] = 0 - - self.data = flattened_data.reshape(-1, 1) - - def change_indexing( - self, - index_type: Literal["row", "snake"] - ) -> None: - """ Change the indexing of a `quick.primitives.Ket` instance. - - Parameters - ---------- - `index_type` : Literal["row", "snake"] - The new indexing type, being "row" or "snake". - - Raises - ------ - ValueError - - If the index type is not supported. - - Usage - ----- - >>> data.change_indexing("snake") - """ - if index_type == "snake": - if self.num_qubits >= 3: - # Convert the bra vector to a matrix (image) - self.data = self.data.reshape(2, -1) - # Reverse the elements in odd rows - self.data[1::2, :] = self.data[1::2, ::-1] - - self.data = self.data.flatten().reshape(-1, 1) - elif index_type == "row": - self.data = self.data - else: - raise ValueError("Index type not supported.") - - def _check__mul__( - self, - other: Any - ) -> None: - """ Check if the multiplication is valid. - - Parameters - ---------- - `other` : Any - The other object to multiply with. - - Raises - ------ - ValueError - - If the two vectors are incompatible. - NotImplementedError - - If the `other` type is incompatible. - """ - if isinstance(other, (SupportsFloat, complex)): - return - elif isinstance(other, bra.Bra): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot contract two incompatible vectors.") - elif isinstance(other, Ket): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot contract two incompatible vectors.") - else: - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - - def __eq__( - self, - other: object - ) -> bool: - """ Check if two ket vectors are equal. - - Parameters - ---------- - `other` : object - The other ket vector. - - Returns - ------- - bool - Whether the two ket vectors are equal. - - Usage - ----- - >>> ket1 = Ket(np.array([1+0j, 0+0j])) - >>> ket2 = Ket(np.array([1+0j, 0+0j])) - >>> ket1 == ket2 - """ - if isinstance(other, Ket): - return bool(np.all(np.isclose(self.data.flatten(), other.data.flatten(), atol=1e-10, rtol=0))) - - raise NotImplementedError(f"Equality with {type(other)} is not supported.") - - def __len__(self) -> int: - """ Return the length of the bra vector. - - Returns - ------- - int - The length of the bra vector. - - Usage - ----- - >>> len(bra) - """ - return len(self.data.flatten()) - - def __add__( - self, - other: Ket - ) -> Ket: - """ Superpose two ket states together. - - Parameters - ---------- - `other` : quick.primitives.Ket - The other ket state. - - Returns - ------- - quick.primitives.Ket - The superposed ket state. - - Raises - ------ - NotImplementedError - - If the two vectors are incompatible. - ValueError - - If the two ket states are incompatible. - - Usage - ----- - >>> ket1 = Ket(np.array([1+0j, 0+0j])) - >>> ket2 = Ket(np.array([1+0j, 0+0j])) - >>> ket3 = ket1 + ket2 - """ - if isinstance(other, Ket): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot add two incompatible vectors.") - return Ket((self.data.flatten() + other.data.flatten()).astype(np.complex128)) - - raise NotImplementedError(f"Addition with {type(other)} is not supported.") - - @overload - def __mul__( - self, - other: Scalar - ) -> Ket: - ... - - @overload - def __mul__( - self, - other: bra.Bra - ) -> operator.Operator: - ... - - @overload - def __mul__( - self, - other: Ket - ) -> Ket: - ... - - def __mul__( - self, - other: Scalar | bra.Bra | Ket - ) -> Ket | operator.Operator: - """ Multiply the ket by a scalar, bra, or ket. - - The multiplication of a ket with a bra is defined as: - |ψ⟩⟨ψ|, which is called the projection operator and is implemented using the measurement - operator. - - The multiplication of a ket with a ket is defined as: - |ψ⟩⊗|ψ'⟩, which is called the tensor product of two quantum states. - - Notes - ----- - The multiplication of a ket with a scalar does not change the ket. This is because - the norm of the ket is preserved, and the scalar is multiplied with each element of the - ket. We provide the scalar multiplication for completeness. - - Parameters - ---------- - `other` : quick.primitives.Scalar | quick.primitives.Bra | quick.primitives.Ket - The object to multiply the ket by. - - Returns - ------- - quick.primitives.Ket | quick.primitives.Operator - The ket or operator resulting from the multiplication. - - Raises - ------ - ValueError - - If the two vectors are incompatible. - NotImplementedError - - If the `other` type is incompatible. - - Usage - ----- - >>> scalar = 2 - >>> ket = Ket(np.array([1+0j, 0+0j])) - >>> ket = ket * scalar - >>> bra = Bra(np.array([1+0j, 0+0j])) - >>> ket = Ket(np.array([1+0j, 0+0j])) - >>> operator = ket * bra - >>> ket1 = Ket(np.array([1+0j, 0+0j])) - >>> ket2 = Ket(np.array([1+0j, 0+0j])) - >>> ket3 = ket1 * ket2 - """ - if isinstance(other, (SupportsFloat, complex)): - return Ket((self.data * other).astype(np.complex128)) # type: ignore - elif isinstance(other, bra.Bra): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot contract two incompatible vectors.") - return operator.Operator(np.outer(self.data, other.data.conj())) - elif isinstance(other, Ket): - if self.num_qubits != other.num_qubits: - raise ValueError("Cannot contract two incompatible vectors.") - return Ket(np.kron(self.data, other.data)) - else: - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - - def __rmul__( - self, - other: Scalar - ) -> Ket: - """ Multiply the ket by a scalar. - - Notes - ----- - The multiplication of a ket with a scalar does not change the ket. This is because - the norm of the ket is preserved, and the scalar is multiplied with each element of the - ket. We provide the scalar multiplication for completeness. - - Parameters - ---------- - `other` : quick.primitives.Scalar - The scalar to multiply the ket by. - - Returns - ------- - quick.primitives.Ket - The ket multiplied by the scalar. - - Usage - ----- - >>> scalar = 2 - >>> ket = Ket(np.array([1+0j, 0+0j])) - >>> ket = scalar * ket - """ - if isinstance(other, (SupportsFloat, complex)): - return Ket((self.data * other).astype(np.complex128)) # type: ignore - - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - - def __str__(self) -> str: - """ Return the string representation of the ket. - - Returns - ------- - str - The string representation of the ket. - - Usage - ----- - >>> ket = Ket(np.array([1+0j, 0+0j])) - >>> str(ket) - """ - return f"|{self.label}⟩" - - def __repr__(self) -> str: - """ Return the string representation of the ket. - - Returns - ------- - str - The string representation of the ket. - - Usage - ----- - >>> ket = Ket(np.array([1+0j, 0+0j])) - >>> repr(ket) - """ - return f"{self.__class__.__name__}(data={self.data}, label={self.label})" \ No newline at end of file diff --git a/quick/primitives/operator.py b/quick/primitives/operator.py index 5ec78bf..43ac529 100644 --- a/quick/primitives/operator.py +++ b/quick/primitives/operator.py @@ -23,8 +23,8 @@ from numpy.typing import NDArray from typing import Any, overload, SupportsFloat, TypeAlias -from quick.predicates import is_square_matrix, is_unitary_matrix -import quick.primitives.ket as ket +from quick.predicates import is_unitary_matrix +import quick.primitives.statevector as statevector # `Scalar` is a type alias that represents a scalar value that can be either # a real number or a complex number. @@ -32,14 +32,15 @@ class Operator: - """ `quick.primitives.Operator` class is used to represent a quantum operator. Quantum operators - are hermitian matrices (square, unitary matrices) which represent operations applied to quantum - states (represented with qubits). + """ `quick.primitives.Operator` class is used to represent a quantum operator. + Quantum operators are unitary matrices which represent operations applied to + quantum states (represented with qubits). It uses LSB convention. Parameters ---------- `data` : NDArray[np.complex128] - The quantum operator data. If the data is not a complex type, it will be converted to complex. + The quantum operator data. If the data is not a complex type, + it will be converted to complex. `label` : str, optional The label of the quantum operator. @@ -53,13 +54,15 @@ class Operator: The shape of the quantum operator data. `num_qubits` : int The number of qubits the quantum operator acts on. + `num_control_qubits` : int + The number of qubits the operator uses as controls. + `tensor_shape` : tuple[int, ...] + The shape of the quantum operator tensor based on + qubits as the physical dimension. Raises ------ ValueError - - If the operator is not a square matrix. - - If the operator dimension is not a power of 2. - - If the operator cannot be converted to complex type. - If the operator is not unitary. Usage @@ -77,52 +80,180 @@ def __init__( """ if label is None: self.label = "\N{LATIN CAPITAL LETTER A}\N{COMBINING CIRCUMFLEX ACCENT}" - self.is_unitary(data) + else: + self.label = label + + data = np.array(data) + + if not is_unitary_matrix(data): + raise ValueError("Operator must be unitary.") + self.data = data self.shape = self.data.shape self.num_qubits = int(np.ceil(np.log2(self.shape[0]))) + self.num_control_qubits = 0 + self.tensor_shape = (2, 2) * self.num_qubits + + @classmethod + def from_matrix( + cls, + matrix: NDArray[np.complex128] + ) -> Operator: + """ Create an `quick.primitives.Operator` from a matrix. + + Parameters + ---------- + `matrix` : NDArray[np.complex128] + The matrix to create the operator from. The matrix is not + required to be unitary, but if it is not, we will approximate + it to the nearest unitary matrix using Singular Value Decomposition (SVD). + + Returns + ------- + quick.primitives.Operator + The operator created from the matrix. + """ + if is_unitary_matrix(matrix): + return cls(matrix) + + U, _, Vh = np.linalg.svd(matrix) + return cls(np.array(U @ Vh).astype(complex)) + + def conj(self) -> Operator: + """ Take the conjugate of the operator. + + Returns + ------- + quick.primitives.Operator + The conjugate of the operator. + """ + return Operator(np.conjugate(self.data), label=self.label) + + def T(self) -> Operator: + """ Take the transpose of the operator. + + Returns + ------- + quick.primitives.Operator + The transpose of the operator. + """ + return Operator(np.transpose(self.data), label=self.label) + + def adjoint(self) -> Operator: + """ Take the adjoint of the operator. + + Returns + ------- + quick.primitives.Operator + The adjoint of the operator. + """ + return self.conj().T() - @staticmethod - def is_unitary(data: NDArray[np.complex128]) -> None: - """ Check if a matrix is Hermitian. + def reverse_bits(self) -> None: + """ Reverse the order of the qubits in the operator. + This changes MSB to LSB, and vice versa. + """ + axes = tuple(range(self.num_qubits - 1, -1, -1)) + axes = axes + tuple(len(axes) + i for i in axes) + self.data = np.reshape( + np.transpose( + np.reshape(self.data, self.tensor_shape), axes + ), + self.shape + ) + + def contract( + self, + op: NDArray[np.complex128] | Operator, + qubit_indices: list[int] + ) -> None: + """ Contract the operator with an operator on the specified + qubits in place using Einstein's Summation convention. + + Notes + ----- + This implementation should be used for small systems, + as it requires significant memory and thus may not be + suitable for larger systems. + + For larger systems, consider using the more efficient + `quick.backend.QuimbBackend` which leverages optimal + tensor network contraction for simulating the circuit. + + Alternatively, consider using GPU-based simulators + present in `quick.backend` which can be faster at + scale. Parameters ---------- - `data` : NDArray[np.complex128] - The matrix to check. + `op` : NDArray[np.complex128] | Operator + The operator or matrix to contract with. + `qubit_indices` : list[int] + The qubit indices to contract over. Raises ------ ValueError - - If the matrix is not square. - - If the matrix dimension is not a power of 2. - - If the matrix cannot be converted to complex type. - - If the matrix is not unitary. + ValueError + - If the operator is not unitary. + - If the number of indices is less than the number of qubits for `op`. + - If the number of qubit indices exceeds the number of qubits in `self`. + - If any of the qubit indices are out of range of `self`. Usage ----- - >>> data = np.array([[1+0j, 0+0j], - ... [0+0j, 1+0j]]) - >>> ishermitian(data) + >>> op1.contract(op2, [0, 1]) """ - # Check if the matrix is square - if not is_square_matrix(data): - raise ValueError("Operator must be a square matrix.") - - # Check if the matrix dimension is a power of 2 - if not ((data.shape[0] & (data.shape[0] - 1) == 0) and data.shape[0] != 0): - raise ValueError("Operator dimension must be a power of 2.") - - # Check if the data type is complex - if not np.iscomplexobj(data): - try: - data = data.astype(np.complex128) - except ValueError: - raise ValueError("Cannot convert data to complex type.") - - # Check if the matrix is unitary - if not is_unitary_matrix(data): - raise ValueError("Operator must be unitary.") + from quick.primitives.contraction import contract + + contract(self, op, qubit_indices) + + def control( + self, + num_controls: int = 1 + ) -> Operator: + """ Generate the controlled version of the operator. + + Parameters + ---------- + `num_controls` : int + The number of control qubits. + + Returns + ------- + quick.primitives.Operator + The controlled version of the operator. + + Raises + ------ + ValueError + - If the number of control qubits is less than 1. + """ + if num_controls < 1: + raise ValueError( + "Number of control qubits must be at least 1." + f"Received {num_controls} instead." + ) + + self.num_control_qubits += num_controls + + zero_projector = np.array([ + [1, 0], + [0, 0] + ]) + one_projector = np.array([ + [0, 0], + [0, 1] + ]) + + controlled_operator = self.data + + for _ in range(num_controls): + control_component = np.kron(np.eye(controlled_operator.shape[0]), zero_projector).astype(np.complex128) + target_component = np.kron(controlled_operator, one_projector).astype(np.complex128) + controlled_operator = control_component + target_component + + return Operator(controlled_operator) def _check__mul__( self, @@ -138,21 +269,54 @@ def _check__mul__( Raises ------ ValueError - - If the the operator and ket are incompatible. + - If the the operator and statevector are incompatible. - If the two operators are incompatible. - NotImplementedError + TypeError - If the `other` type is incompatible. """ - if isinstance(other, (SupportsFloat, complex)): - return - elif isinstance(other, ket.Ket): + if isinstance(other, statevector.Statevector): if self.num_qubits != other.num_qubits: - raise ValueError("Cannot multiply an operator with an incompatible ket.") + raise ValueError("Cannot multiply an operator with an incompatible statevector.") elif isinstance(other, Operator): if self.num_qubits != other.num_qubits: raise ValueError("Cannot multiply two incompatible operators.") else: - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") + raise TypeError(f"Multiplication with {type(other)} is not supported.") + + def __array__(self) -> NDArray[np.complex128]: + """ Convert the `quick.primitives.Operator` to a NumPy array. + + Returns + ------- + NDArray[np.complex128] + """ + return np.array(self.data).astype(np.complex128) + + def __eq__( + self, + other: Any + ) -> bool: + """ Check if two operators are equal. + + Parameters + ---------- + `other` : Any + The other object to compare with. + + Returns + ------- + bool + True if the operators are equal, False otherwise. + + Raises + ------ + TypeError + - If the `other` type is incompatible. + """ + if not isinstance(other, Operator): + raise TypeError(f"Cannot compare {type(self)} with {type(other)}.") + + return bool(np.all(np.isclose(self.data, other.data, atol=1e-8, rtol=0))) @overload def __mul__( @@ -164,8 +328,8 @@ def __mul__( @overload def __mul__( self, - other: ket.Ket - ) -> ket.Ket: + other: statevector.Statevector + ) -> statevector.Statevector: ... @overload @@ -177,30 +341,29 @@ def __mul__( def __mul__( self, - other: Scalar | ket.Ket | Operator - ) -> Operator | ket.Ket: - """ Multiply an operator with a scalar, ket or another operator. + other: Scalar | statevector.Statevector | Operator + ) -> Operator | statevector.Statevector: + """ Multiply an operator with a number, statevector, or another operator. - The multiplication of an operator with a ket is defined as: + Notes + ----- + The multiplication of a number with the operator behaves like the global + phase shift. + + The multiplication of an operator with a statevector is defined as: - A|ψ⟩ = |ψ'⟩ The multiplication of an operator with another operator is defined as: - AB = C - Notes - ----- - The multiplication of an operator with a scalar does not change the operator. This is because - the norm of the operator is preserved, and the scalar is multiplied with each element of the - operator. We provide the scalar multiplication for completeness. - Parameters ---------- - `other` : quick.primitives.Scalar | quick.primitives.Ket | quick.primitives.Operator + `other` : quick.primitives.Statevector | quick.primitives.Operator The object to multiply with. Returns ------- - quick.primitives.Operator | quick.primitives.Ket + quick.primitives.Operator | quick.primitives.Statevector The result of the multiplication. Raises @@ -208,76 +371,59 @@ def __mul__( ValueError - If the operator and ket dimensions are incompatible. - If the operator dimensions are incompatible. - NotImplementedError + TypeError - If the `other` type is incompatible. Usage ----- - >>> scalar = 2 - >>> operator = Operator([[1+0j, 0+0j], - ... [0+0j, 1+0j]]) - >>> operator * scalar >>> operator = Operator([[1+0j, 0+0j], ... [0+0j, 1+0j]]) - >>> ket = Ket([1+0j, 0+0j]) - >>> operator * ket + >>> statevector = Statevector([1+0j, 0+0j]) + >>> operator * statevector >>> operator1 = Operator([[1+0j, 0+0j], ... [0+0j, 1+0j]]) >>> operator2 = Operator([[1+0j, 0+0j], ... [0+0j, 1+0j]]) >>> operator1 * operator2 """ - if isinstance(other, (SupportsFloat, complex)): - return Operator((self.data * other).astype(np.complex128)) # type: ignore - elif isinstance(other, ket.Ket): + if isinstance(other, Scalar): + return Operator(self.data * complex(other)) + elif isinstance(other, statevector.Statevector): if self.num_qubits != other.num_qubits: - raise ValueError("Cannot multiply an operator with an incompatible ket.") - return ket.Ket((self.data @ other.data).astype(np.complex128)) # type: ignore + raise ValueError("Cannot multiply an operator with an incompatible statevector.") + return statevector.Statevector((self.data @ other.data).astype(np.complex128)) elif isinstance(other, Operator): if self.num_qubits != other.num_qubits: raise ValueError("Cannot multiply two incompatible operators.") return Operator(self.data @ other.data) - else: - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") - def __rmul__( + raise TypeError(f"Multiplication with {type(other)} is not supported.") + + def __matmul__( self, - other: Scalar + other: Operator ) -> Operator: - """ Multiply a scalar with an operator. - - Notes - ----- - The multiplication of an operator with a scalar does not change the operator. This is because - the norm of the operator is preserved, and the scalar is multiplied with each element of the - operator. We provide the scalar multiplication for completeness. + """ Calculate the tensor product of the two operators. Parameters ---------- - `other` : quick.primitives.Scalar - The scalar to multiply with. + `other` : quick.primitives.Operator + The operator to tensor with. Returns ------- quick.primitives.Operator - The operator multiplied by the scalar. + The tensor product of the two operators. Raises ------ - NotImplementedError - - If the `other` type is incompatible - - Usage - ----- - >>> scalar = 2 - >>> operator = Operator([[1+0j, 0+0j], - ... [0+0j, 1+0j]]) - >>> scalar * operator + TypeError + - If the `other` is not a `quick.primitives.Operator` instance. """ - if isinstance(other, (SupportsFloat, complex)): - return Operator((self.data * other).astype(np.complex128)) # type: ignore + if not isinstance(other, Operator): + raise TypeError(f"Cannot tensor Operator with {type(other)}.") - raise NotImplementedError(f"Multiplication with {type(other)} is not supported.") + return Operator(np.kron(self.data, other.data)) def __str__(self) -> str: """ Return the string representation of the operator. @@ -288,7 +434,7 @@ def __str__(self) -> str: ... [0+0j, 1+0j]]) >>> print(operator) """ - return f"{self.label}" + return self.label def __repr__(self) -> str: """ Return the string representation of the operator. @@ -299,4 +445,4 @@ def __repr__(self) -> str: ... [0+0j, 1+0j]]) >>> repr(operator) """ - return f"Operator(data={self.data})" \ No newline at end of file + return f"Operator(data={self.data}, label={self.label})" \ No newline at end of file diff --git a/quick/primitives/statevector.py b/quick/primitives/statevector.py new file mode 100644 index 0000000..e03c0bd --- /dev/null +++ b/quick/primitives/statevector.py @@ -0,0 +1,728 @@ +# Copyright 2023-2025 Qualition Computing LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://github.com/Qualition/quick/blob/main/LICENSE +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Statevector class for representing quantum (Ket) state vectors. +""" + +from __future__ import annotations + +__all__ = ["Statevector"] + +import numpy as np +from numpy.typing import NDArray +from typing import Any, Literal, SupportsFloat, TypeAlias + +from quick.predicates import is_normalized +import quick.primitives.operator as operator + +# `Scalar` is a type alias that represents a scalar value that can be either +# a real number or a complex number. +Scalar: TypeAlias = SupportsFloat | complex + + +class Statevector: + """ `quick.primitives.Statevector` is a class that represents a qubit + statevector. Qubit statevectors are complex vectors with a magnitude + of 1 with 2^N elements where N is the number of qubits used to represent + the statevector. It uses LSB convention. + + Parameters + ---------- + `data` : NDArray[np.complex128] + The statevector data. The data will be normalized + to 2-norm and padded if necessary. + `label` : str, optional + The label of the statevector. + + Attributes + ---------- + `label` : str, optional, default="Ψ" + The label of the statevector. + `data` : NDArray[np.complex128] + The statevector data. + `norm_scale` : np.float64 + The normalization scale. + `normalized` : bool + Whether the statevector is normalized to 2-norm or not. + `shape` : tuple[int,] + The shape of the quantum statevector data. + `num_qubits` : int + The number of qubits represented by the statevector. + `tensor_shape` : tuple[int, ...] + The shape of the statevector tensor based on qubits + as the physical dimension. + + Raises + ------ + ValueError + - If the data is a scalar or an operator. + + Usage + ----- + >>> data = np.array([1, 2, 3, 4]) + >>> statevector = Statevector(data) + """ + def __init__( + self, + data: NDArray[np.complex128], + label: str | None = None + ) -> None: + """ Initialize a `quick.primitives.Statevector` instance. + """ + if label is None: + self.label = "\N{GREEK CAPITAL LETTER PSI}" + else: + self.label = label + + data = np.array(data) + self.validate_data(data) + self.data = data.flatten().astype(np.complex128) + self.norm_scale = np.linalg.norm(self.data) + self.num_qubits = int(np.ceil(np.log2(self.data.size))) + self.shape = (2 ** self.num_qubits,) + self.tensor_shape = (2,) * self.num_qubits + self.is_normalized() + self.is_padded() + self.to_quantumstate() + + @classmethod + def from_int( + cls, + value: int, + num_qubits: int + ) -> Statevector: + """ Create a statevector from the basis state + representation of an integer. + + Parameters + ---------- + `value` : int + The integer value to convert. + `num_qubits` : int + The number of qubits to use. + + Returns + ------- + quick.primitives.Statevector + The resulting statevector. + """ + statevector = np.zeros(2 ** num_qubits, dtype=np.complex128) + statevector[value] = 1 + return cls(statevector) + + def conj(self) -> Statevector: + """ Take the conjugate of the statevector. + + Returns + ------- + quick.primitives.Statevector + The conjugate of the statevector. + """ + return Statevector(np.conjugate(self.data), label=self.label) + + @staticmethod + def validate_data(data: NDArray[np.complex128]) -> None: + """ Validate the data to ensure it is a valid statevector. + + Parameters + ---------- + `data` : NDArray[np.complex128] + The data to validate. + + Raises + ------ + ValueError + - If the data is a scalar or an operator. + """ + if isinstance(data, Scalar) and data.size == 1: + raise ValueError("Cannot convert a scalar to a statevector.") + elif data.ndim == 0 or data.size == 1: + raise ValueError("Cannot convert a scalar to a statevector.") + elif data.ndim == 2 and data.shape[0] != 1: + raise ValueError("Cannot convert an operator to a statevector.") + + def is_normalized(self) -> None: + """ Check if a `quick.primitives.Statevector` instance is normalized to 2-norm. + + Usage + ----- + >>> statevector.is_normalized() + """ + self.normalized = is_normalized(self.data) + + @staticmethod + def normalize_data( + data: NDArray[np.complex128], + norm_scale: np.float64 + ) -> NDArray[np.complex128]: + """ Normalize the data to 2-norm, and return the normalized data. + + Parameters + ---------- + `data` : NDArray[np.complex128] + The data. + `norm_scale` : np.float64 + The normalization scale. + + Returns + ------- + NDArray[np.complex128] + The 2-norm normalized data. + + Usage + ----- + >>> data = np.array([[1, 2], + ... [3, 4]]) + >>> norm_scale = np.linalg.norm(data.flatten()) + >>> normalize_data(data, norm_scale) + """ + return np.multiply(data, 1/norm_scale) + + def normalize(self) -> None: + """ Normalize a `quick.primitives.Statevector` instance to 2-norm. + + Usage + ----- + >>> statevector.normalize() + """ + if self.normalized: + return + + self.data = self.normalize_data(self.data, self.norm_scale) + self.normalized = True + + @staticmethod + def check_padding(data: NDArray[np.complex128]) -> bool: + """ Check if a data is normalized to 2-norm. + + Parameters + ---------- + `data` : NDArray[np.complex128] + The data. + + Returns + ------- + bool + Whether the vector is normalized to 2-norm or not. + + Usage + ----- + >>> data = np.array([[1, 2], [3, 4]]) + >>> check_padding(data) + """ + len_data = len(data) + return (len_data & (len_data - 1) == 0) and len_data != 0 + + def is_padded(self) -> None: + """ Check if a `quick.primitives.Statevector` instance is padded to a power of 2. + + Usage + ----- + >>> statevector.is_padded() + """ + self.padded = self.check_padding(self.data) + + @staticmethod + def pad_data( + data: NDArray[np.complex128], + target_size: int + ) -> NDArray[np.complex128]: + """ Pad data with zeros up to the nearest power of 2, and return + the padded data. + + Parameters + ---------- + `data` : NDArray[np.complex128] + The data to be padded. + `target_size` : int + The target size to pad the data to. + + Returns + ------- + `padded_data` : NDArray[np.complex128] + The padded data. + + Usage + ----- + >>> data = np.array([1, 2, 3]) + >>> pad_data(data, 4) + """ + padded_data = np.pad( + data, (0, int(target_size - len(data))), + mode="constant" + ) + + return padded_data + + def pad(self) -> None: + """ Pad a `quick.primitives.Statevector` instance. + + Usage + ----- + >>> statevector.pad() + """ + if self.padded: + return + + self.data = self.pad_data(self.data, 2 ** self.num_qubits) + self.padded = True + + def to_quantumstate(self) -> None: + """ Ensure the statevector is in a valid quantum state. + + Usage + ----- + >>> statevector.to_quantumstate() + """ + if not self.normalized: + self.normalize() + + if not self.padded: + self.pad() + + def compress( + self, + compression_percentage: float + ) -> None: + """ Compress a `quick.primitives.Statevector` instance. + + Parameters + ---------- + `compression_percentage` : float + The percentage of compression. + + Usage + ----- + >>> statevector.compress(50) + """ + data_sort_ind = np.argsort(np.abs(self.data)) + + # Set the smallest absolute values of data to zero according to compression parameter + cutoff = int((compression_percentage / 100.0) * len(self.data)) + for i in data_sort_ind[:cutoff]: + self.data[i] = 0 + + def change_indexing( + self, + index_type: Literal["row", "snake"] + ) -> None: + """ Change the indexing of a `quick.primitives.Statevector` instance. + + Parameters + ---------- + `index_type` : Literal["row", "snake"] + The new indexing type, being "row" or "snake". + + Raises + ------ + ValueError + - If the index type is not supported. + + Usage + ----- + >>> statevector.change_indexing("snake") + """ + if index_type == "snake": + if self.num_qubits >= 3: + # Convert the statevector to a matrix (image) + self.data = self.data.reshape(2, -1) + # Reverse the elements in odd rows + self.data[1::2, :] = self.data[1::2, ::-1] + + self.data = self.data.flatten() + elif index_type == "row": + self.data = self.data + else: + raise ValueError("Index type not supported.") + + def reverse_bits(self) -> None: + """ Reverse the order of the qubits in the statevector. + This changes MSB to LSB, and vice versa. + """ + self.data = np.transpose( + np.reshape( + self.data, + self.tensor_shape + ) + ).ravel() + + def trace(self) -> float: + """ Calculate the trace of the statevector. + + Returns + ------- + float + The trace of the statevector. + + Usage + ----- + >>> statevector.trace() + """ + return float(np.sum(np.abs(self.data) ** 2)) + + def partial_trace( + self, + trace_qubit_indices: list[int] + ) -> float | NDArray[np.complex128]: + """ Calculate the partial trace of the statevector. + + Parameters + ---------- + `trace_qubit_indices` : list[int] + The indices of the qubits to trace out. + + Returns + ------- + `rho` : float | NDArray[np.complex128] + The resulting density matrix after tracing out the specified qubits. + If the `trace_qubit_indices` match the total number of qubits, then + we return the trace of the statevector. + + Raises + ------ + ValueError + - If the trace qubit indices are invalid. + + Usage + ----- + >>> statevector.partial_trace([0, 1]) + """ + for i in trace_qubit_indices: + if not 0 <= i < self.num_qubits: + raise ValueError( + f"Invalid trace qubit index {i}. " + f"Valid indices are in range(0, {self.num_qubits})." + ) + + num_traced_qubits = len(trace_qubit_indices) + + if num_traced_qubits == self.num_qubits: + return self.trace() + + traced_shape = (2**(self.num_qubits - num_traced_qubits),) * 2 + trace_systems = [self.num_qubits - 1 - i for i in trace_qubit_indices] + state = self.data.reshape(self.tensor_shape) + rho = np.tensordot(state, state.conj(), axes=(trace_systems, trace_systems)) + rho = np.reshape(rho, traced_shape) + + return rho + + def contract( + self, + op: NDArray[np.complex128] | operator.Operator, + qubit_indices: list[int] + ) -> None: + """ Contract the statevector with an operator on the specified + qubits in place using Einstein's Summation convention. + + Notes + ----- + This implementation should be used for small systems, + as it requires significant memory and thus may not be + suitable for larger systems. + + For larger systems, consider using the more efficient + `quick.backend.QuimbBackend` which leverages optimal + tensor network contraction for simulating the circuit. + + Alternatively, consider using GPU-based simulators + present in `quick.backend` which can be faster at + scale. + + Parameters + ---------- + `op` : NDArray[np.complex128] | quick.primitives.Operator + The operator to contract with. + `qubit_indices` : list[int] + The indices of the qubits to contract over. + + Raises + ------ + ValueError + - If the operator is not unitary. + - If the number of indices is less than the number of qubits for `op`. + - If the number of qubit indices exceeds the number of qubits in `self`. + - If any of the qubit indices are out of range of `self`. + + Usage + ----- + >>> statevector.contract(op, [0, 1]) + """ + from quick.primitives.contraction import contract + + contract(self, op, qubit_indices) + + def _check__mul__( + self, + other: Any + ) -> None: + """ Check if the multiplication is valid. + + Parameters + ---------- + `other` : Any + The other object to multiply with. + + Raises + ------ + ValueError + - If the two vectors are incompatible. + - If the the statevector and operator are incompatible. + TypeError + - If the `other` type is incompatible. + """ + if isinstance(other, (SupportsFloat, complex)): + return + elif isinstance(other, operator.Operator): + if self.num_qubits != other.num_qubits: + raise ValueError("Cannot multiply two incompatible vectors.") + else: + raise TypeError(f"Multiplication with {type(other)} is not supported.") + + def __array__(self) -> NDArray[np.complex128]: + """ Convert the `quick.primitives.Statevector` to a NumPy array. + + Returns + ------- + NDArray[np.complex128] + """ + return np.array(self.data).astype(np.complex128) + + def __eq__( + self, + other: object + ) -> bool: + """ Check if two statevector vectors are equal. + + Parameters + ---------- + `other` : object + The other statevector vector. + + Returns + ------- + bool + Whether the two statevector vectors are equal. + + Raises + ------ + TypeError + - If the `other` object is not a `quick.primitives.Statevector` instance. + + Usage + ----- + >>> statevector1 = Statevector(np.array([1+0j, 0+0j])) + >>> statevector2 = Statevector(np.array([1+0j, 0+0j])) + >>> statevector1 == statevector2 + """ + if not isinstance(other, Statevector): + raise TypeError( + "Statevector can only be compared with other Statevector instances. " + f"Received {type(other)} instead." + ) + + return bool(np.all(np.isclose(self.data, other.data, atol=1e-8, rtol=0))) + + def __len__(self) -> int: + """ Return the length of the statevector vector. + + Returns + ------- + int + The length of the statevector vector. + + Usage + ----- + >>> len(statevector) + """ + return len(self.data) + + def __add__( + self, + other: Statevector + ) -> Statevector: + """ Superpose two statevector states together. + + Parameters + ---------- + `other` : quick.primitives.Statevector + The other statevector state. + + Returns + ------- + quick.primitives.Statevector + The superposed statevector state. + + Raises + ------ + TypeError + - If the `other` object is not a `quick.primitives.Statevector` instance. + ValueError + - If the two statevectors are incompatible. + + Usage + ----- + >>> statevector1 = Statevector(np.array([1+0j, 0+0j])) + >>> statevector2 = Statevector(np.array([1+0j, 0+0j])) + >>> statevector1 + statevector2 + """ + if not isinstance(other, Statevector): + raise TypeError( + "Statevector can only be added to other Statevector instances. " + f"Received {type(other)} instead." + ) + + if self.num_qubits != other.num_qubits: + raise ValueError("Cannot add two incompatible vectors.") + + return Statevector((self.data + other.data).astype(np.complex128)) + + def __mul__( + self, + other: Scalar + ) -> Statevector: + """ Multiply the statevector by a scalar. + + Parameters + ---------- + `other` : Scalar + The other object to multiply the statevector by. + + Returns + ------- + quick.primitives.Statevector + The result of the multiplication. + + Raises + ------ + TypeError + - If the `other` object is not a number. + + Usage + ----- + >>> scalar = 2 + >>> statevector = Statevector(np.array([1+0j, 0+0j])) + >>> statevector * scalar + """ + if not isinstance(other, Scalar): + raise TypeError( + "Statevector can only be multiplied by a scalar. " + f"Received {type(other)} instead." + ) + + return Statevector( + (self.data * complex(other)).astype(np.complex128) + ) + + def __rmul__( + self, + other: Scalar + ) -> Statevector: + """ Multiply the statevector by a scalar. + + Parameters + ---------- + `other` : Scalar + The scalar to multiply the statevector by. + + Returns + ------- + quick.primitives.Statevector + The statevector multiplied by the scalar. + + Raises + ------ + TypeError + - If the `other` object is not a number. + + Usage + ----- + >>> scalar = 2 + >>> statevector = Statevector(np.array([1+0j, 0+0j])) + >>> scalar * statevector + """ + if not isinstance(other, Scalar): + raise TypeError( + "Statevector can only be multiplied by a scalar. " + f"Received {type(other)} instead." + ) + + return Statevector( + (self.data * complex(other)).astype(np.complex128) + ) + + def __matmul__( + self, + other: Statevector + ) -> Statevector: + """ Calculate the tensor product of the two statevectors. + + Parameters + ---------- + `other` : quick.primitives.Statevector + The statevector to tensor product with. + + Returns + ------- + quick.primitives.Statevector + The resulting statevector. + + Raises + ------ + TypeError + - If `other` is not a `quick.primitives.Statevector`. + + Usage + ----- + >>> statevector1 = Statevector(np.array([1+0j, 0+0j])) + >>> statevector2 = Statevector(np.array([1+0j, 0+0j])) + >>> statevector1 @ statevector2 + """ + if not isinstance(other, Statevector): + raise TypeError( + "Statevector can only be tensored with other Statevector instances. " + f"Received {type(other)} instead." + ) + + return Statevector( + np.kron(self.data, other.data).astype(np.complex128) + ) + + def __str__(self) -> str: + """ Return the string representation of the statevector vector. + + Returns + ------- + str + The string representation of the statevector vector. + + Usage + ----- + >>> str(statevector) + """ + return f"|{self.label}⟩" + + def __repr__(self) -> str: + """ Return the string representation of the statevector vector. + + Returns + ------- + str + The string representation of the statevector vector. + + Usage + ----- + >>> repr(statevector) + """ + return f"{self.__class__.__name__}(data={self.data}, label={self.label})" \ No newline at end of file diff --git a/quick/random/__init__.py b/quick/random/__init__.py index ad5e766..dc6dbed 100644 --- a/quick/random/__init__.py +++ b/quick/random/__init__.py @@ -15,11 +15,17 @@ __all__ = [ "generate_random_state", "generate_random_unitary", - "generate_random_density_matrix" + "generate_random_density_matrix", + "generate_random_orthogonal_matrix", + "generate_random_special_orthogonal_matrix", + "generate_random_special_unitary_matrix" ] from quick.random.random import ( generate_random_state, generate_random_unitary, - generate_random_density_matrix + generate_random_density_matrix, + generate_random_orthogonal_matrix, + generate_random_special_orthogonal_matrix, + generate_random_special_unitary_matrix ) \ No newline at end of file diff --git a/quick/random/random.py b/quick/random/random.py index a37f29d..e538e50 100644 --- a/quick/random/random.py +++ b/quick/random/random.py @@ -17,7 +17,10 @@ __all__ = [ "generate_random_state", "generate_random_unitary", - "generate_random_density_matrix" + "generate_random_density_matrix", + "generate_random_orthogonal_matrix", + "generate_random_special_orthogonal_matrix", + "generate_random_special_unitary_matrix" ] import numpy as np @@ -46,7 +49,9 @@ def _generate_ginibre_matrix( entry is sampled from the normal distribution. """ rng = np.random.default_rng() - ginibre_ensemble = rng.normal(size=(num_rows, num_columns)) + 1j * rng.normal(size=(num_rows, num_columns)) + ginibre_ensemble = rng.normal(size=(num_rows, num_columns)) + 1j * rng.normal( + size=(num_rows, num_columns) + ) return ginibre_ensemble def generate_random_state(num_qubits: int) -> NDArray[np.complex128]: @@ -123,4 +128,66 @@ def generate_random_density_matrix( density_matrix = density_matrix @ ginibre_ensemble density_matrix = density_matrix @ density_matrix.conj().T - return density_matrix / np.trace(density_matrix) \ No newline at end of file + return density_matrix / np.trace(density_matrix) + +def generate_random_orthogonal_matrix(num_qubits: int) -> NDArray[np.complex128]: + """ Generate a random orthogonal matrix for the given number of qubits. + + Parameters + ---------- + `num_qubits` : int + The number of qubits in the orthogonal matrix. + + Returns + ------- + `NDArray[np.complex128]` + The random orthogonal matrix. + """ + A = np.random.rand(2**num_qubits, 2**num_qubits) + Q, R = np.linalg.qr(A) + + d = np.sign(np.diag(R)) + d[d == 0] = 1 + Q = Q @ np.diag(d) + + return Q.astype(np.complex128) + +def generate_random_special_orthogonal_matrix(num_qubits: int) -> NDArray[np.float64]: + """ Generate a random special orthogonal matrix for the given number of qubits. + + Parameters + ---------- + `num_qubits` : int + The number of qubits in the special orthogonal matrix. + + Returns + ------- + `NDArray[np.float64]` + The random special orthogonal matrix. + """ + Q = generate_random_orthogonal_matrix(num_qubits) + + if np.linalg.det(Q) < 0: + Q[:, 0] *= -1 + + return Q.astype(np.float64) + +def generate_random_special_unitary_matrix(num_qubits: int) -> NDArray[np.complex128]: + """ Generate a random special unitary matrix for the given number of qubits. + + Parameters + ---------- + `num_qubits` : int + The number of qubits in the special unitary matrix. + + Returns + ------- + `NDArray[np.complex128]` + The random special unitary matrix. + """ + U = generate_random_unitary(num_qubits) + + det = np.linalg.det(U) + U = U / det**(1 / U.shape[0]) + + return U \ No newline at end of file diff --git a/quick/synthesis/gate_decompositions/multi_controlled_decomposition/mcsu2_real_diagonal.py b/quick/synthesis/gate_decompositions/multi_controlled_decomposition/mcsu2_real_diagonal.py index 73e6246..5081f2d 100644 --- a/quick/synthesis/gate_decompositions/multi_controlled_decomposition/mcsu2_real_diagonal.py +++ b/quick/synthesis/gate_decompositions/multi_controlled_decomposition/mcsu2_real_diagonal.py @@ -223,7 +223,7 @@ def MCRX( circuit, control_indices, target_index, - RX(theta).matrix + RX(theta).data ) def MCRY( @@ -271,7 +271,7 @@ def MCRY( circuit, control_indices, target_index, - RY(theta).matrix, + RY(theta).data, ) def MCRZ( @@ -319,5 +319,5 @@ def MCRZ( circuit, control_indices, target_index, - RZ(theta).matrix, + RZ(theta).data, ) \ No newline at end of file diff --git a/quick/synthesis/gate_decompositions/two_qubit_decomposition/two_qubit_decomposition.py b/quick/synthesis/gate_decompositions/two_qubit_decomposition/two_qubit_decomposition.py index e7f6d6d..12f8f1d 100644 --- a/quick/synthesis/gate_decompositions/two_qubit_decomposition/two_qubit_decomposition.py +++ b/quick/synthesis/gate_decompositions/two_qubit_decomposition/two_qubit_decomposition.py @@ -22,7 +22,6 @@ __all__ = ["TwoQubitDecomposition"] -import cmath from collections.abc import Sequence import math import numpy as np @@ -34,6 +33,7 @@ from quick.circuit.gate_matrix import RZ, CX from quick.primitives import Operator from quick.synthesis.gate_decompositions.one_qubit_decomposition import OneQubitDecomposition +from quick.synthesis.gate_decompositions.two_qubit_decomposition.utils import u4_to_su4 from quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl import TwoQubitWeylDecomposition from quick.synthesis.unitarypreparation import UnitaryPreparation @@ -43,6 +43,8 @@ """ Hardcoded basis gates for the KAK decomposition using the CX gate as the basis. """ +CX_BASIS = TwoQubitWeylDecomposition(CX.data) + Q0L = np.array([ [0.5+0.5j, 0.5-0.5j], [-0.5-0.5j, 0.5-0.5j] @@ -108,7 +110,7 @@ [-1j, -1] ], dtype=np.complex128) * SQRT2 -u2la: NDArray[np.complex128] = np.array([ +U2LA: NDArray[np.complex128] = np.array([ [0.5+0.5j, 0.5-0.5j], [-0.5-0.5j, 0.5-0.5j] ], dtype=np.complex128) @@ -142,25 +144,35 @@ class TwoQubitDecomposition(UnitaryPreparation): """ `quick.synthesis.unitarypreparation.TwoQubitDecomposition` is the class for decomposing two-qubit unitary matrices into one qubit - quantum gates and CX gates. + quantum gates and a fixed super-controlled basis gate, which in this + class we hardcode to be the CX gate. Notes ----- - The decomposition is based on the KAK decomposition, which decomposes a 2-qubit unitary matrix - into a sequence of three unitary matrices, each of which is a product of one-qubit gates and a - CX gate. + The decomposition is based on the KAK decomposition, which decomposes a U(4) + matrix into + + .. math:: + U = (K_1^l \otimes K_1^r) \cdot A(a, b, c) \cdot (K_2^l \otimes K_2^r) \cdot e^{i \phi} + + where :math:`A(a, b, c) = e^{(ia X \otimes X + ib Y \otimes Y + ic Z \otimes Z)}`, + :math:`K_1^l, K_1^r, K_2^l, K_2^r` are single-qubit unitaries, and :math:`\phi` is + the global phase. + + The non-local part A(a, b, c) can be expressed in terms of the basis gate + ~A(pi/4, b, 0) (super-controlled basis) using at most 3 uses of the basis gate + and some one-qubit gates. - The up to diagonal decomposition of two qubit unitaries into the product of a diagonal gate - and another unitary gate can be represented by two CX gates instead of the usual three. - This can be used when neighboring gates commute with the diagonal to potentially reduce - overall CX count. + For certain unitaries, we can use fewer basis gates. If the target unitary + is locally equivalent to ~A(0, 0, 0) then we can use 0 basis gates. If the + target unitary is locally equivalent to the basis gate then we can use 1 + basis gate. If the target unitary is locally equivalent to ~A(x, y, 0) + then we can use 2 basis gates. - To use the up to diagonal decomposition, the `apply_unitary_up_to_diagonal` method can be used. + To use the up to diagonal decomposition, the `apply_unitary_up_to_diagonal` + method can be used. - For more information on KAK decomposition, refer to the following paper: - [1] Vidal, Dawson. - A Universal Quantum Circuit for Two-qubit Transformations with 3 CNOT Gates (2003) - https://arxiv.org/pdf/quant-ph/0307177 + https://arxiv.org/pdf/1811.12926 (Passage between (B6) and (B7)) Parameters ---------- @@ -173,6 +185,8 @@ class TwoQubitDecomposition(UnitaryPreparation): The quantum circuit framework. `one_qubit_decomposition` : quick.synthesis.gate_decompositions.OneQubitDecomposition The one-qubit decomposition class. + `decompositions` : list[callable] + The list of decomposition functions. Raises ------ @@ -192,32 +206,20 @@ def __init__( super().__init__(output_framework) self.one_qubit_decomposition = OneQubitDecomposition(output_framework) - - @staticmethod - def u4_to_su4(u4: NDArray[np.complex128]) -> tuple[NDArray[np.complex128], float]: - """ Convert a general 4x4 unitary matrix to a SU(4) matrix. - - Parameters - ---------- - `u4` : NDArray[np.complex128] - The 4x4 unitary matrix. - - Returns - ------- - `su4` : NDArray[np.complex128] - The 4x4 special unitary matrix. - `phase_factor` : float - The phase factor. - """ - phase_factor = np.conj(np.linalg.det(u4) ** (-1 / u4.shape[0])) - su4: NDArray[np.complex128] = u4 / phase_factor - return su4, cmath.phase(phase_factor) + self.decompositions = [ + self._decomp0, + self._decomp1, + self._decomp2_supercontrolled, + self._decomp3_supercontrolled, + ] @staticmethod def traces(target: TwoQubitWeylDecomposition) -> list[complex]: """ Calculate the expected traces $|Tr(U \cdot U_{target}^\dagger)|$ for different number of basis gates. + https://arxiv.org/pdf/1811.12926 (B3) + Parameters ---------- `target` : quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl.TwoQubitWeylDecomposition @@ -280,7 +282,6 @@ def real_trace_transform(U: NDArray[np.complex128]) -> NDArray[np.complex128]: U[0, 0] * U[3, 3] ) - # Initialize theta and phi (they can be arbitrary) theta = 0 phi = 0 @@ -320,8 +321,11 @@ def trace_to_fidelity(trace: complex) -> float: return (4 + abs(trace) ** 2) / 20 @staticmethod - def _decomp0(weyl_decomposition: TwoQubitWeylDecomposition) -> tuple[NDArray[np.complex128], NDArray[np.complex128]]: - """ Decompose target ~Ud(x, y, z) with 0 uses of the basis gate. + def _decomp0(weyl_decomposition: TwoQubitWeylDecomposition) -> tuple[ + NDArray[np.complex128], + NDArray[np.complex128] + ]: + """ Decompose target ~A(x, y, z) with 0 uses of the basis gate. Result Ur has trace: ..math:: @@ -338,9 +342,9 @@ def _decomp0(weyl_decomposition: TwoQubitWeylDecomposition) -> tuple[NDArray[np. Returns ------- `U0r` : NDArray[np.complex128] - The right unitary matrix. + One qubit unitary gate. `U0l` : NDArray[np.complex128] - The left unitary matrix. + One qubit unitary gate. """ U0l = weyl_decomposition.K1l.dot(weyl_decomposition.K2l) U0r = weyl_decomposition.K1r.dot(weyl_decomposition.K2r) @@ -353,15 +357,13 @@ def _decomp1(weyl_decomposition: TwoQubitWeylDecomposition) -> tuple[ NDArray[np.complex128], NDArray[np.complex128] ]: - """ Decompose target ~Ud(x, y, z) with 1 uses of the basis gate ~Ud(a, b, c). + """ Decompose target ~A(x, y, z) with 1 uses of the basis gate ~A(a, b, c). Result Ur has trace: .. math:: |Tr(Ur.U_{target}^\dagger)| = 4|\cos(x-a) \cos(y-b) \cos(z-c) + i \sin(x-a) \sin(y-b) \sin(z-c)| - which is optimal for all targets and bases with z==0 or c==0. - Parameters ---------- `weyl_decomposition` : quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl.TwoQubitWeylDecomposition @@ -370,24 +372,18 @@ def _decomp1(weyl_decomposition: TwoQubitWeylDecomposition) -> tuple[ Returns ------- `U1r` : NDArray[np.complex128] - The right unitary matrix. + One qubit unitary gate. `U1l` : NDArray[np.complex128] - The left unitary matrix. + One qubit unitary gate. `U0r` : NDArray[np.complex128] - The right unitary matrix. + One qubit unitary gate. `U0l` : NDArray[np.complex128] - The left unitary matrix. + One qubit unitary gate. """ - # Get the CX gate in LSB ordering - CX.change_mapping("LSB") - - # Use the basis gate as the closest reflection in the Weyl chamber - basis = TwoQubitWeylDecomposition(CX.matrix) - - U0l = weyl_decomposition.K1l.dot(basis.K1l.T.conj()) - U0r = weyl_decomposition.K1r.dot(basis.K1r.T.conj()) - U1l = basis.K2l.T.conj().dot(weyl_decomposition.K2l) - U1r = basis.K2r.T.conj().dot(weyl_decomposition.K2r) + U0l = weyl_decomposition.K1l.dot(CX_BASIS.K1l.T.conj()) + U0r = weyl_decomposition.K1r.dot(CX_BASIS.K1r.T.conj()) + U1l = CX_BASIS.K2l.T.conj().dot(weyl_decomposition.K2l) + U1r = CX_BASIS.K2r.T.conj().dot(weyl_decomposition.K2r) return U1r, U1l, U0r, U0l @@ -400,24 +396,14 @@ def _decomp2_supercontrolled(weyl_decomposition: TwoQubitWeylDecomposition) -> t NDArray[np.complex128], NDArray[np.complex128] ]: - """ Decompose target ~Ud(x, y, z) with 2 uses of the basis gate. + """ Decompose target ~A(x, y, z) with 2 uses of the super-controlled basis gate. - For supercontrolled basis ~Ud(pi/4, b, 0), all b, result Ur has trace + For supercontrolled basis ~A(pi/4, b, 0), all b, result Ur has trace .. math:: |Tr(Ur.U_{target}^\dagger)| = 4 \cos(z) - which is the optimal approximation for basis of CX-class ``~Ud(pi/4, 0, 0)`` - or DCX-class ``~Ud(pi/4, pi/4, 0)`` and any target. - - Notes - ----- - May be sub-optimal for b!=0 (e.g. there exists exact decomposition for any target using B - ``B~Ud(pi/4, pi/8, 0)``, but not this decomposition.) - This is an exact decomposition for supercontrolled basis and target ``~Ud(x, y, 0)``. - No guarantees for non-supercontrolled basis. - Parameters ---------- `weyl_decomposition` : quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl.TwoQubitWeylDecomposition @@ -426,22 +412,22 @@ def _decomp2_supercontrolled(weyl_decomposition: TwoQubitWeylDecomposition) -> t Returns ------- `U2r` : NDArray[np.complex128] - The right unitary matrix. + One qubit unitary gate. `U2l` : NDArray[np.complex128] - The left unitary matrix. + One qubit unitary gate. `U1r` : NDArray[np.complex128] - The right unitary matrix. + One qubit unitary gate. `U1l` : NDArray[np.complex128] - The left unitary matrix. + One qubit unitary gate. `U0r` : NDArray[np.complex128] - The right unitary matrix. + One qubit unitary gate. `U0l` : NDArray[np.complex128] - The left unitary matrix. + One qubit unitary gate. """ U0l = weyl_decomposition.K1l.dot(Q0L) U0r = weyl_decomposition.K1r.dot(Q0R) - U1l = Q1LA.dot(RZ(-2 * float(weyl_decomposition.a)).matrix).dot(Q1LB) - U1r = Q1RA.dot(RZ(2 * float(weyl_decomposition.b)).matrix).dot(Q1RB) + U1l = Q1LA.dot(RZ(-2 * float(weyl_decomposition.a)).data).dot(Q1LB) + U1r = Q1RA.dot(RZ(2 * float(weyl_decomposition.b)).data).dot(Q1RB) U2l = Q2L.dot(weyl_decomposition.K2l) U2r = Q2R.dot(weyl_decomposition.K2r) @@ -458,19 +444,7 @@ def _decomp3_supercontrolled(weyl_decomposition: TwoQubitWeylDecomposition) -> t NDArray[np.complex128], NDArray[np.complex128] ]: - """ Decompose a 2-qubit unitary matrix into a sequence of one-qubit gates and CX gates. - The decomposition uses three CX gates. - - Notes - ----- - The decomposition is based on the KAK decomposition, which decomposes a 2-qubit unitary matrix - into a sequence of three unitary matrices, each of which is a product of one-qubit gates and a - CX gate. - - For more information on KAK decomposition, refer to the following paper: - - Vidal, Dawson. - A Universal Quantum Circuit for Two-qubit Transformations with 3 CNOT Gates (2003) - https://arxiv.org/pdf/quant-ph/0307177 + """ Decompose target ~A(x, y, z) with 3 uses of the super-controlled basis gate. Parameters ---------- @@ -496,13 +470,12 @@ def _decomp3_supercontrolled(weyl_decomposition: TwoQubitWeylDecomposition) -> t `U0l` : NDArray[np.complex128] The left unitary matrix. """ - # Calculate the decomposition U0l = weyl_decomposition.K1l.dot(U0L) U0r = weyl_decomposition.K1r.dot(U0R) U1l = U1L - U1r = U1RA.dot(RZ(-2 * float(weyl_decomposition.c)).matrix).dot(UR1B) - U2l = u2la.dot(RZ(-2 * float(weyl_decomposition.a)).matrix).dot(U2LB) - U2r = U2RA.dot(RZ(2 * float(weyl_decomposition.b)).matrix).dot(U2RB) + U1r = U1RA.dot(RZ(-2 * float(weyl_decomposition.c)).data).dot(UR1B) + U2l = U2LA.dot(RZ(-2 * float(weyl_decomposition.a)).data).dot(U2LB) + U2r = U2RA.dot(RZ(2 * float(weyl_decomposition.b)).data).dot(U2RB) U3l = U3L.dot(weyl_decomposition.K2l) U3r = U3R.dot(weyl_decomposition.K2r) @@ -542,8 +515,6 @@ def apply_unitary( ----- >>> circuit = two_qubit_decomposition.apply_unitary(circuit, unitary, qubit_indices) """ - # Cast the qubit indices to a list if it is an integer - # Note this is only to inform pylance that `qubit_indices` is a list if isinstance(qubit_indices, int): qubit_indices = [qubit_indices] @@ -556,39 +527,40 @@ def apply_unitary( if unitary.num_qubits != 2: raise ValueError("Two-qubit decomposition requires a 4x4 unitary matrix.") - decomposition_functions = [ - self._decomp0, - self._decomp1, - self._decomp2_supercontrolled, - self._decomp3_supercontrolled, - ] - - # Hardcoded global phase for supercontrolled basis ~Ud(pi/4, b, 0), - # all b when using CX as KAK basis - cx_basis_global_phase = -np.pi/4 - target_decomposed = TwoQubitWeylDecomposition(unitary.data) # Calculate the expected fidelities for different number of basis gates + # By default, we choose the decomposition with fidelity of 1.0, however + # if a lower fidelity is permitted we can choose a decomposition with + # fewer basis gates traces = self.traces(target_decomposed) expected_fidelities = [TwoQubitDecomposition.trace_to_fidelity(traces[i]) for i in range(4)] - best_num_basis = int(np.argmax(expected_fidelities)) - decomposition = decomposition_functions[best_num_basis](target_decomposed) - + cx_basis_global_phase = -np.pi/4 overall_global_phase = target_decomposed.global_phase - best_num_basis * cx_basis_global_phase + # Handling the global phase for the up to diagonal case which uses 2 CX gates if best_num_basis == 2: overall_global_phase += np.pi + decomposition = self.decompositions[best_num_basis](target_decomposed) + for i in range(best_num_basis): - self.one_qubit_decomposition.apply_unitary(circuit, decomposition[2 * i], qubit_indices[0]) - self.one_qubit_decomposition.apply_unitary(circuit, decomposition[2 * i + 1], qubit_indices[1]) + self.one_qubit_decomposition.apply_unitary( + circuit, decomposition[2 * i], qubit_indices[0] + ) + self.one_qubit_decomposition.apply_unitary( + circuit, decomposition[2 * i + 1], qubit_indices[1] + ) circuit.CX(qubit_indices[0], qubit_indices[1]) - self.one_qubit_decomposition.apply_unitary(circuit, decomposition[2 * best_num_basis], qubit_indices[0]) - self.one_qubit_decomposition.apply_unitary(circuit, decomposition[2 * best_num_basis + 1], qubit_indices[1]) + self.one_qubit_decomposition.apply_unitary( + circuit, decomposition[2 * best_num_basis], qubit_indices[0] + ) + self.one_qubit_decomposition.apply_unitary( + circuit, decomposition[2 * best_num_basis + 1], qubit_indices[1] + ) circuit.GlobalPhase(overall_global_phase) @@ -628,7 +600,11 @@ def apply_unitary_up_to_diagonal( Usage ----- - >>> circuit, diagonal = two_qubit_decomposition.apply_unitary_up_to_diagonal(circuit, unitary, qubit_indices) + >>> circuit, diagonal = two_qubit_decomposition.apply_unitary_up_to_diagonal( + ... circuit, + ... unitary, + ... qubit_indices + ... ) """ if isinstance(qubit_indices, int): qubit_indices = [qubit_indices] @@ -642,7 +618,7 @@ def apply_unitary_up_to_diagonal( if unitary.num_qubits != 2: raise ValueError("Two-qubit decomposition requires a 4x4 unitary matrix.") - su4, phase = TwoQubitDecomposition.u4_to_su4(unitary.data) + su4, phase = u4_to_su4(unitary.data) diagonal = TwoQubitDecomposition.real_trace_transform(su4) mapped_su4 = diagonal @ su4 diff --git a/quick/synthesis/gate_decompositions/two_qubit_decomposition/utils.py b/quick/synthesis/gate_decompositions/two_qubit_decomposition/utils.py new file mode 100644 index 0000000..02128b9 --- /dev/null +++ b/quick/synthesis/gate_decompositions/two_qubit_decomposition/utils.py @@ -0,0 +1,60 @@ +# Copyright 2023-2025 Qualition Computing LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://github.com/Qualition/quick/blob/main/LICENSE +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Utility functions for two-qubit gate decompositions. +""" + +from __future__ import annotations + +__all__ = ["u4_to_su4"] + +import cmath +import numpy as np +from numpy.typing import NDArray +import scipy.linalg # type: ignore + + +def u4_to_su4(U: NDArray[np.complex128]) -> tuple[NDArray[np.complex128], float]: + """ Convert a U(4) matrix to an SU(4) matrix by removing the global phase + such that the determinant is 1. + + Parameters + ---------- + `U` : NDArray[np.complex128] + The input U(4) matrix. + + Returns + ------- + `SU4` : NDArray[np.complex128] + The resulting SU(4) matrix. + `global_phase` : float + The global phase that was removed. + + Usage + ----- + >>> SU4, global_phase = u4_to_su4(np.eye(4)) + """ + # We need to cast to complex to avoid NaN errors + U = np.asarray(U, dtype=np.complex128) + + # The code fails with np.linalg.det + U_det = scipy.linalg.det(U) + + # For general U_N we must take to power of -1/U.shape[0] + # but since this implementation is only used for U4 we + # omit this calculation and use hardcoded -1/4 + SU4 = U * U_det ** (-0.25) + global_phase = cmath.phase(U_det) / 4 + + return SU4, global_phase \ No newline at end of file diff --git a/quick/synthesis/gate_decompositions/two_qubit_decomposition/weyl.py b/quick/synthesis/gate_decompositions/two_qubit_decomposition/weyl.py index 10032f7..eaac37a 100644 --- a/quick/synthesis/gate_decompositions/two_qubit_decomposition/weyl.py +++ b/quick/synthesis/gate_decompositions/two_qubit_decomposition/weyl.py @@ -21,12 +21,9 @@ from __future__ import annotations __all__ = [ - "transform_to_magic_basis", + "M", + "M_DAGGER", "weyl_coordinates", - "partition_eigenvalues", - "remove_global_phase", - "diagonalize_unitary_complex_symmetric", - "decompose_two_qubit_product_gate", "TwoQubitWeylDecomposition" ] @@ -38,28 +35,52 @@ import scipy.linalg # type: ignore import warnings -""" Define the M matrix from section III to -tranform the unitary matrix into the magic basis: -https://arxiv.org/pdf/quant-ph/0308006 +from quick.synthesis.gate_decompositions.two_qubit_decomposition.utils import u4_to_su4 + +""" Define the magic basis matrix from eq(3): +https://arxiv.org/pdf/quant-ph/9703041 + +There is another well-known definition: + +M = 1/np.sqrt(2) * np.array([ + [1, 0, 0, 1j], + [0, 1j, 1, 0], + [0, 1j, -1, 0], + [1, 0, 0, -1j] +], dtype=complex) + +But as far as we can tell, this choice does not affect the +decomposition. The basis M and its adjoint are stored individually unnormalized, -but such that their matrix multiplication is still the identity -This is because they are only used in unitary transformations -(so it's safe to do so), and `sqrt(0.5)` is not exactly representable -in floating point. - -Doing it this way means that every element of the matrix is stored exactly -correctly, and the multiplication is exactly the identity rather than -differing by 1ULP. +but given each has a factor of `sqrt(0.5)`, once we multiply them +together the factor becomes 0.5, so we directly store the factor +0.5 in the adjoint matrix. + +This is done to minimize the floating-point errors that arise +in the M^2 calculation which cause issues for certain OS. + +Notes +----- +The following use our definition of the magic basis: +- https://arxiv.org/pdf/quant-ph/0308006 +- https://arxiv.org/pdf/quant-ph/0011050 +- https://arxiv.org/pdf/0806.4015 +- https://arxiv.org/pdf/quant-ph/0405046 + +And the following use the other definition: +- https://arxiv.org/pdf/cond-mat/0609750 +- https://arxiv.org/pdf/quant-ph/0209120 +- https://arxiv.org/pdf/quant-ph/0507171 """ -M_UNNORMALIZED = np.array([ +M = np.array([ [1, 1j, 0, 0], [0, 0, 1j, 1], [0, 0, 1j, -1], [1, -1j, 0, 0] ], dtype=np.complex128) -M_UNNORMALIZED_DAGGER = 0.5 * M_UNNORMALIZED.conj().T +M_DAGGER = 0.5 * M.conj().T # Pauli matrices in magic basis X_MAGIC_BASIS = np.array([ @@ -88,23 +109,24 @@ def transform_to_magic_basis( U: NDArray[np.complex128], reverse: bool = False ) -> NDArray[np.complex128]: - """ Transform the 4x4 matrix `U` into the magic basis. + """ Transform the SU4 matrix `U` into the magic basis. - Notes - ----- - This method internally uses non-normalized versions of the basis - to minimize (but not eliminate) the floating-point errors that arise - during the transformation. + ..math:: + U_{magic\_basis} = M \cdot U \cdot M^\dagger - This implementation is based on the following paper: - [1] Vatan, Williams. - Optimal Quantum Circuits for General Two-Qubit Gates (2004). - https://arxiv.org/abs/quant-ph/0308006 + for the forward transformation, and + + ..math:: + U_{magic\_basis} = M^\dagger \cdot U \cdot M + + for the reverse transformation. Note that if + we do forward followed by reverse transformation + or vice versa, we get back the original matrix. Parameters ---------- `U` : NDArray[np.complex128] - The input 4-by-4 matrix to be transformed. + The SU4 matrix to be transformed. `reverse` : bool, optional, default=False If True, the transformation is done in the reverse direction. @@ -115,15 +137,16 @@ def transform_to_magic_basis( Usage ----- - >>> U_magic = transform_to_magic_basis(np.eye(4)) + >>> U_magic_basi = transform_to_magic_basis(np.eye(4)) """ if reverse: - return M_UNNORMALIZED_DAGGER @ U @ M_UNNORMALIZED - return M_UNNORMALIZED @ U @ M_UNNORMALIZED_DAGGER + return M_DAGGER @ U @ M + return M @ U @ M_DAGGER def weyl_coordinates(U: NDArray[np.complex128]) -> NDArray[np.float64]: """ Calculate the Weyl coordinates for a given two-qubit unitary matrix. - This is used for unit-testing the Weyl decomposition. + This function is used for unit-testing Weyl decomposition, and certain + predicates in `quick.predicates`. Notes ----- @@ -146,10 +169,10 @@ def weyl_coordinates(U: NDArray[np.complex128]) -> NDArray[np.float64]: ----- >>> weyl_coordinates = weyl_coordinates(np.eye(4)) """ - U /= scipy.linalg.det(U) ** (0.25) + U = u4_to_su4(U)[0] U_magic_basis = transform_to_magic_basis(U, reverse=True) - # We only need the eigenvalues of `M2 = Up.T @ Up` here, not the full diagonalization + # We only need the eigenvalues of `M_squared = Up.T @ Up` here, not the full diagonalization D = scipy.linalg.eigvals(U_magic_basis.T @ U_magic_basis) d = -np.angle(D) / 2 @@ -380,7 +403,7 @@ def decompose_two_qubit_product_gate( R /= np.sqrt(R_det) # Extract the left component - temp = np.kron(np.eye(2), R.T.conj()) + temp = np.kron(np.eye(2), R.conj().T) temp = special_unitary_matrix.dot(temp) L = temp[::2, ::2] L_det = L[0, 0] * L[1, 1] - L[0, 1] * L[1, 0] @@ -404,7 +427,21 @@ def decompose_two_qubit_product_gate( class TwoQubitWeylDecomposition: """ Decompose a two-qubit unitary matrix into the Weyl coordinates and - the product of two single-qubit unitaries. + the product of two single-qubit unitaries via KAK decomposition. + + .. math:: + U = (K_1^l \otimes K_1^r) \cdot A(a, b, c) \cdot (K_2^l \otimes K_2^r) \cdot e^{i \phi} + + where :math:`A(a, b, c) = e^{(ia X \otimes X + ib Y \otimes Y + ic Z \otimes Z)}`, + :math:`K_1^l, K_1^r, K_2^l, K_2^r` are single-qubit unitaries, and :math:`\phi` is + the global phase. + + Notes + ----- + This implementation is based on the following paper: + [1] Vatan, Williams. + Optimal Quantum Circuits for General Two-Qubit Gates (2004). + https://arxiv.org/abs/quant-ph/0308006 Parameters ---------- @@ -414,11 +451,11 @@ class TwoQubitWeylDecomposition: Attributes ---------- `a` : np.float64 - The first Weyl coordinate. + The multiplier for XX term in the canonical gate. `b` : np.float64 - The second Weyl coordinate. + The multiplier for YY term in the canonical gate. `c` : np.float64 - The third Weyl coordinate. + The multiplier for ZZ term in the canonical gate. `K1l` : NDArray[np.complex128] The left component of the first single-qubit unitary. `K1r` : NDArray[np.complex128] @@ -430,119 +467,72 @@ class TwoQubitWeylDecomposition: `global_phase` : float The global phase. """ - def __init__(self, unitary_matrix: NDArray[np.complex128]) -> None: + def __init__( + self, + unitary_matrix: NDArray[np.complex128] + ) -> None: """ Initialize a `quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl. TwoQubitWeylDecomposition` instance. """ - self.a, self.b, self.c, self.K1l, self.K1r, self.K2l, self.K2r, self.global_phase = self.decompose_unitary( - unitary_matrix - ) - - @staticmethod - def decompose_unitary(unitary_matrix: NDArray[np.complex128]) -> tuple[ - np.float64, - np.float64, - np.float64, - NDArray[np.complex128], - NDArray[np.complex128], - NDArray[np.complex128], - NDArray[np.complex128], - float - ]: - """ Decompose a two-qubit unitary matrix into the Weyl coordinates and the - product of two single-qubit unitaries. - - Notes - ----- - M2 diagnolization may be wrong due to floating point errors, but it will work - correctly in Linux. Should you encounter a failure in the code, and the result - fails to encode the correct unitary matrix, please report it at - https://github.com/Qualition/quick/issues/11 - - with the unitary matrix that caused the failure. - - Parameters - ---------- - `unitary_matrix` : NDArray[np.complex128] - The input 4-by-4 unitary matrix. - - Returns - ------- - `a` : np.float64 - The first Weyl coordinate. - `b` : np.float64 - The second Weyl coordinate. - `c` : np.float64 - The third Weyl coordinate. - `K1l` : NDArray[np.complex128] - The left component of the first single-qubit unitary. - `K1r` : NDArray[np.complex128] - The right component of the first single-qubit unitary. - `K2l` : NDArray[np.complex128] - The left component of the second single-qubit unitary. - `K2r` : NDArray[np.complex128] - The right component of the second single-qubit unitary. - `global_phase` : float - The global phase. - - Raises - ------ - ValueError - - If the determinant of the right or left component is - not in the expected range. - - If the decomposition fails due to a deviation from the - expected unitary matrix. - - Usage - ----- - >>> a, b, c, K1l, K1r, K2l, K2r, global_phase = TwoQubitWeylDecomposition.decompose_unitary(np.eye(4)) - """ - # Make U be in SU(4) - U = np.array(unitary_matrix, dtype=np.complex128, copy=True) - U_det = scipy.linalg.det(U) - U *= U_det ** (-0.25) - global_phase = cmath.phase(U_det) / 4 + U, global_phase = u4_to_su4(unitary_matrix) + # Transforming U into the magic basis has two remarkable properties: + # 1) If U is in SO(4) (real, U.T=U, det U=1) then U_magic_basis is in SU(2)xSU(2) + # which means we can decompose it into two single-qubit unitaries + # 2) The magic basis diagonalizes the canonical gate A (as in A from KAK) U_magic_basis = transform_to_magic_basis(U, reverse=True) - M2 = np.round(U_magic_basis.T.dot(U_magic_basis), decimals=15) - D, P = diagonalize_unitary_complex_symmetric(M2) + # Construct type AI global Cartan involution + # To minimize the floating point issues arising in different OS + # we round M^2 + # This is a fix for known issues in Windows and MacOS + # See https://github.com/Qualition/quick/issues/11 + M_squared = np.round(U_magic_basis.T.dot(U_magic_basis), decimals=15) + + # Diagonalize M^2 into D and P where D is complex diagonal + # and P is real-symmetric unitary + # M^2 = P.diag(D).P^T + D, P = diagonalize_unitary_complex_symmetric(M_squared) # Given P is a real-symmetric unitary matrix we only use transpose - if not np.allclose(P.dot(np.diag(D)).dot(P.T), M2, rtol=0, atol=1e-13): + if not np.allclose(P.dot(np.diag(D)).dot(P.T), M_squared, rtol=0, atol=1e-13): warnings.warn( - "Failed to diagonalize M2." + "Failed to diagonalize M_squared. " "Kindly report this at https://github.com/Qualition/quick/issues/11: " f"U: {U}" ) - d = -np.angle(D) / 2 - d[3] = -d[0] - d[1] - d[2] - weyl_coordinates = np.mod((d[:3] + d[3]) / 2, PI_DOUBLE) + # We want M which is P.sqrt(diag(D)).P^T so we take the square root of D + D_sqrt = -np.angle(D) / 2 + D_sqrt[3] = -D_sqrt[0] - D_sqrt[1] - D_sqrt[2] + weyl_coordinates = np.mod((D_sqrt[:3] + D_sqrt[3]) / 2, PI_DOUBLE) # Reorder the eigenvalues to get in the Weyl chamber weyl_coordinates_temp = np.mod(weyl_coordinates, PI2) - np.minimum(weyl_coordinates_temp, PI2 - weyl_coordinates_temp, weyl_coordinates_temp) + weyl_coordinates_temp = np.minimum(weyl_coordinates_temp, PI2 - weyl_coordinates_temp) order = np.argsort(weyl_coordinates_temp)[[1, 2, 0]] weyl_coordinates = weyl_coordinates[order] - d[:3] = d[order] + D_sqrt[:3] = D_sqrt[order] P[:, :3] = P[:, order] - # Fix the sign of P to be in SO(4) + # Sometimes computing the diagonalization of M_squared gives a P with determinant -1 as + # opposed to +1 which results in P not being in SO(4) + # To fix this we can negate the last column of P if np.real(scipy.linalg.det(P)) < 0: - P[:, -1] = -P[:, -1] + P[:, -1] *= -1 # Find K1, K2 so that U = K1.A.K2, with K being product of single-qubit unitaries - K1 = transform_to_magic_basis(U_magic_basis @ P @ np.diag(np.exp(1j * d))) + # SU2 x SU2 + K1 = transform_to_magic_basis(U_magic_basis @ P @ np.diag(np.exp(1j * D_sqrt))) K2 = transform_to_magic_basis(P.T) K1l, K1r, phase_l = decompose_two_qubit_product_gate(K1) K2l, K2r, phase_r = decompose_two_qubit_product_gate(K2) global_phase += phase_l + phase_r - K1l = K1l.copy() - - # Flip into Weyl chamber + # Flip into Weyl chamber such that pi/4 >= a >= b >= |c| >= 0 + # This is because the maximal entangling power of A is symmetric + # around pi/4 and pi/2-periodic in a, b, and c if weyl_coordinates[0] > PI2: weyl_coordinates[0] -= 3 * PI2 K1l = K1l.dot(Y_MAGIC_BASIS) @@ -588,4 +578,11 @@ def decompose_unitary(unitary_matrix: NDArray[np.complex128]) -> tuple[ a, b, c = weyl_coordinates[1], weyl_coordinates[0], weyl_coordinates[2] - return a, b, c, K1l, K1r, K2l, K2r, global_phase \ No newline at end of file + self.a = a + self.b = b + self.c = c + self.K1l = K1l + self.K1r = K1r + self.K2l = K2l + self.K2r = K2r + self.global_phase = global_phase \ No newline at end of file diff --git a/quick/synthesis/statepreparation/isometry.py b/quick/synthesis/statepreparation/isometry.py index f406cec..f4a8616 100644 --- a/quick/synthesis/statepreparation/isometry.py +++ b/quick/synthesis/statepreparation/isometry.py @@ -29,10 +29,10 @@ if TYPE_CHECKING: from quick.circuit import Circuit -from quick.circuit.circuit_utils import extract_single_qubits_and_diagonal -from quick.primitives import Bra, Ket +from quick.circuit.utils import extract_single_qubits_and_diagonal +from quick.primitives import Statevector from quick.synthesis.statepreparation import StatePreparation -from quick.synthesis.statepreparation.statepreparation_utils import ( +from quick.synthesis.statepreparation.utils import ( a, b, k_s, @@ -78,6 +78,8 @@ class Isometry(StatePreparation): ------ TypeError - If the output framework is not a subclass of `quick.circuit.Circuit`. + ValueError + - If the state is not a valid quantum state. Usage ----- @@ -86,22 +88,23 @@ class Isometry(StatePreparation): def apply_state( self, circuit: Circuit, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, qubit_indices: int | Sequence[int], compression_percentage: float = 0.0, index_type: Literal["row", "snake"] = "row" ) -> Circuit: - if not isinstance(state, (np.ndarray, Bra, Ket)): + if not isinstance(state, (np.ndarray, Statevector)): try: state = np.array(state).astype(complex) except (ValueError, TypeError): - raise TypeError(f"The state must be a numpy array or a Bra/Ket object. Received {type(state)} instead.") + raise TypeError( + "The state must be a numpy array or a Statevector object. " + f"Received {type(state)} instead." + ) if isinstance(state, np.ndarray): - state = Ket(state) - elif isinstance(state, Bra): - state = state.to_ket() + state = Statevector(state) if isinstance(qubit_indices, SupportsIndex): qubit_indices = [qubit_indices] @@ -279,11 +282,7 @@ def disentangle( # We must reverse the circuit to prepare the state, # as the circuit is uncomputing from the target state # to the zero state - # If the state is a bra, we will apply a horizontal reverse - # which will nullify this reverse, thus we will first check - # if the state is not a bra before applying the reverse - if not isinstance(state, Bra): - isometry_circuit.horizontal_reverse() + isometry_circuit.horizontal_reverse() circuit.add(isometry_circuit, qubit_indices) diff --git a/quick/synthesis/statepreparation/mottonen.py b/quick/synthesis/statepreparation/mottonen.py index 94c8e09..91fce8f 100644 --- a/quick/synthesis/statepreparation/mottonen.py +++ b/quick/synthesis/statepreparation/mottonen.py @@ -26,9 +26,9 @@ if TYPE_CHECKING: from quick.circuit import Circuit -from quick.primitives import Bra, Ket +from quick.primitives import Statevector from quick.synthesis.statepreparation import StatePreparation -from quick.synthesis.statepreparation.statepreparation_utils import ( +from quick.synthesis.statepreparation.utils import ( compute_alpha_y, compute_alpha_z, compute_control_indices, compute_m ) @@ -70,22 +70,23 @@ class Mottonen(StatePreparation): def apply_state( self, circuit: Circuit, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, qubit_indices: int | Sequence[int], compression_percentage: float = 0.0, index_type: Literal["row", "snake"] = "row" ) -> Circuit: - if not isinstance(state, (np.ndarray, Bra, Ket)): + if not isinstance(state, (np.ndarray, Statevector)): try: state = np.array(state).astype(complex) except (ValueError, TypeError): - raise TypeError(f"The state must be a numpy array or a Bra/Ket object. Received {type(state)} instead.") + raise TypeError( + "The state must be a numpy array or a Statevector object. " + f"Received {type(state)} instead." + ) if isinstance(state, np.ndarray): - state = Ket(state) - elif isinstance(state, Bra): - state = state.to_ket() + state = Statevector(state) if isinstance(qubit_indices, SupportsIndex): qubit_indices = [qubit_indices] @@ -165,9 +166,6 @@ def k_controlled_uniform_rotation( # to retrieve the LSB ordering mottonen_circuit.vertical_reverse() - if isinstance(state, Bra): - mottonen_circuit.horizontal_reverse() - circuit.add(mottonen_circuit, qubit_indices) return circuit \ No newline at end of file diff --git a/quick/synthesis/statepreparation/shende.py b/quick/synthesis/statepreparation/shende.py index 51ea24a..1f02d91 100644 --- a/quick/synthesis/statepreparation/shende.py +++ b/quick/synthesis/statepreparation/shende.py @@ -26,9 +26,9 @@ if TYPE_CHECKING: from quick.circuit import Circuit -from quick.primitives import Bra, Ket +from quick.primitives import Statevector from quick.synthesis.statepreparation import StatePreparation -from quick.synthesis.statepreparation.statepreparation_utils import rotations_to_disentangle +from quick.synthesis.statepreparation.utils import rotations_to_disentangle class Shende(StatePreparation): @@ -68,22 +68,20 @@ class Shende(StatePreparation): def apply_state( self, circuit: Circuit, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, qubit_indices: int | Sequence[int], compression_percentage: float = 0.0, index_type: Literal["row", "snake"] = "row" ) -> Circuit: - if not isinstance(state, (np.ndarray, Bra, Ket)): + if not isinstance(state, (np.ndarray, Statevector)): try: state = np.array(state).astype(complex) except (ValueError, TypeError): - raise TypeError(f"The state must be a numpy array or a Bra/Ket object. Received {type(state)} instead.") + raise TypeError(f"The state must be a numpy array or a Statevector object. Received {type(state)} instead.") if isinstance(state, np.ndarray): - state = Ket(state) - elif isinstance(state, Bra): - state = state.to_ket() + state = Statevector(state) if isinstance(qubit_indices, SupportsIndex): qubit_indices = [qubit_indices] @@ -240,11 +238,7 @@ def disentangle( # We must reverse the circuit to prepare the state, # as the circuit is uncomputing from the target state # to the zero state - # If the state is a bra, we will apply a horizontal reverse - # which will nullify this reverse, thus we will first check - # if the state is not a bra before applying the reverse - if not isinstance(state, Bra): - disentangling_circuit.horizontal_reverse() + disentangling_circuit.horizontal_reverse() circuit.add(disentangling_circuit, qubit_indices) diff --git a/quick/synthesis/statepreparation/statepreparation.py b/quick/synthesis/statepreparation/statepreparation.py index e9d01a0..12ea854 100644 --- a/quick/synthesis/statepreparation/statepreparation.py +++ b/quick/synthesis/statepreparation/statepreparation.py @@ -29,7 +29,7 @@ import quick if TYPE_CHECKING: from quick.circuit import Circuit -from quick.primitives import Bra, Ket +from quick.primitives import Statevector class StatePreparation(ABC): @@ -67,7 +67,7 @@ def __init__( def prepare_state( self, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, compression_percentage: float = 0.0, index_type: Literal["row", "snake"] = "row" ) -> Circuit: @@ -75,7 +75,7 @@ def prepare_state( Parameters ---------- - `state` : NDArray[np.complex128] | quick.primitives.Bra | quick.primitives.Ket + `state` : NDArray[np.complex128] | quick.primitives.Statevector The quantum state to prepare. `compression_percentage` : float, optional, default=0.0 Number between 0 an 100, where 0 is no compression and 100 all statevector values are 0. @@ -90,16 +90,19 @@ def prepare_state( Raises ------ TypeError - - If the state is not a numpy array or a Bra/Ket object. + - If the state is not a numpy array or a Statevector object. """ - if not isinstance(state, (np.ndarray, Bra, Ket)): + if not isinstance(state, (np.ndarray, Statevector)): try: state = np.array(state).astype(complex) except (ValueError, TypeError): - raise TypeError(f"The state must be a numpy array or a Bra/Ket object. Received {type(state)} instead.") + raise TypeError( + "The state must be a numpy array or a Statevector object. " + f"Received {type(state)} instead." + ) if isinstance(state, np.ndarray): - state = Ket(state) + state = Statevector(state) num_qubits = state.num_qubits circuit = self.output_framework(num_qubits) @@ -110,7 +113,7 @@ def prepare_state( def apply_state( self, circuit: Circuit, - state: NDArray[np.complex128] | Bra | Ket, + state: NDArray[np.complex128] | Statevector, qubit_indices: int | Sequence[int], compression_percentage: float = 0.0, index_type: Literal["row", "snake"] = "row" @@ -121,7 +124,7 @@ def apply_state( ---------- `circuit` : quick.circuit.Circuit The quantum circuit to which the state is applied. - `state` : NDArray[np.complex128] | quick.primitives.Bra | quick.primitives.Ket + `state` : NDArray[np.complex128] | quick.primitives.Statevector The quantum state to apply. `qubit_indices` : int | Sequence[int] The qubit indices to which the state is applied. @@ -138,7 +141,7 @@ def apply_state( Raises ------ TypeError - - If the state is not a numpy array or a Bra/Ket object. + - If the state is not a numpy array or a Statevector object. - If the qubit indices are not integers or a sequence of integers. ValueError - If the compression percentage is not in the range [0, 100]. diff --git a/quick/synthesis/statepreparation/statepreparation_utils.py b/quick/synthesis/statepreparation/utils.py similarity index 100% rename from quick/synthesis/statepreparation/statepreparation_utils.py rename to quick/synthesis/statepreparation/utils.py diff --git a/quick/synthesis/unitarypreparation/shannon_decomposition.py b/quick/synthesis/unitarypreparation/shannon_decomposition.py index e5fccbb..0382cba 100644 --- a/quick/synthesis/unitarypreparation/shannon_decomposition.py +++ b/quick/synthesis/unitarypreparation/shannon_decomposition.py @@ -29,16 +29,11 @@ import quick if TYPE_CHECKING: from quick.circuit import Circuit -from quick.circuit.circuit_utils import decompose_multiplexor_rotations +from quick.circuit.utils import decompose_multiplexor_rotations from quick.predicates import is_hermitian_matrix from quick.primitives import Operator from quick.synthesis.unitarypreparation import UnitaryPreparation -# Constants -QUBIT_KEYS = frozenset([ - "qubit_index", "control_index", "target_index" -]) - class ShannonDecomposition(UnitaryPreparation): """ `quick.ShannonDecomposition` is the class for preparing quantum operators @@ -396,6 +391,8 @@ def a2_optimization( `a2_qsd_blocks` : list[list[int]] List of blocks to apply A.2 optimization to. """ + from quick.circuit.circuit import ALL_QUBIT_KEYS as QUBIT_KEYS + # If there are no blocks, or only one block which means # no neighbors to merge diagonal into, then return if len(a2_qsd_blocks) < 2: @@ -428,11 +425,11 @@ def a2_optimization( if block_index == 0: for operation in circuit_1.circuit_log: for key in set(operation.keys()).intersection(QUBIT_KEYS): - operation[key] = 0 if operation[key] == qubit_indices[0] else 1 + operation[key] = 0 if operation[key] == qubit_indices[0] else 1 # type: ignore for operation in circuit_2.circuit_log: for key in set(operation.keys()).intersection(QUBIT_KEYS): - operation[key] = 0 if operation[key] == qubit_indices[0] else 1 + operation[key] = 0 if operation[key] == qubit_indices[0] else 1 # type: ignore circuit_1.update() circuit_2.update() @@ -456,7 +453,7 @@ def a2_optimization( for block in qsd_blocks: # type: ignore for operation in block: # type: ignore for key in set(operation.keys()).intersection(QUBIT_KEYS): - operation[key] = qubit_indices[0] if operation[key] == 0 else qubit_indices[1] + operation[key] = qubit_indices[0] if operation[key] == 0 else qubit_indices[1] # type: ignore # Reconstruct the circuit with the modified blocks in alternating order circuit.reset() diff --git a/quick/synthesis/unitarypreparation/unitarypreparation_utils.py b/quick/synthesis/unitarypreparation/utils.py similarity index 100% rename from quick/synthesis/unitarypreparation/unitarypreparation_utils.py rename to quick/synthesis/unitarypreparation/utils.py diff --git a/tests/circuit/test_circuit_base.py b/tests/circuit/test_circuit_base.py index c3634c8..9aa9551 100644 --- a/tests/circuit/test_circuit_base.py +++ b/tests/circuit/test_circuit_base.py @@ -650,34 +650,15 @@ def test_get_depth( # Define the `quick.circuit.Circuit` instance circuit = circuit_framework(4) - # Apply the MCX gate circuit.MCX([0, 1], [2, 3]) - # Get the depth of the circuit, and ensure it is correct + assert circuit.get_depth() == 1 + + circuit.transpile() depth = circuit.get_depth() assert depth == 25 - @pytest.mark.parametrize("circuit_framework", CIRCUIT_FRAMEWORKS) - def test_get_width( - self, - circuit_framework: type[Circuit] - ) -> None: - """ Test the width of the circuit. - - Parameters - ---------- - `circuit_framework`: type[quick.circuit.Circuit] - The circuit framework to test. - """ - # Define the `quick.circuit.Circuit` instance - circuit = circuit_framework(4) - - # Get the width of the circuit, and ensure it is correct - width = circuit.get_width() - - assert width == 4 - @pytest.mark.parametrize("circuit_framework", CIRCUIT_FRAMEWORKS) def test_get_instructions( self, diff --git a/tests/circuit/test_circuit_decompose.py b/tests/circuit/test_circuit_decompose.py index 305208b..fd19164 100644 --- a/tests/circuit/test_circuit_decompose.py +++ b/tests/circuit/test_circuit_decompose.py @@ -279,6 +279,15 @@ def test_primitive_gates( num_qubits: int ) -> None: """ Test the decomposition of the primitive gates. + + Parameters + ---------- + `framework` : type[quick.circuit.Circuit] + The circuit framework to use. + `gate_func` : Callable[[Circuit], None] + The gate function to apply. + `num_qubits` : int + The number of qubits in the circuit. """ # Define the circuit and apply the gate circuit: Circuit = framework(num_qubits) @@ -294,4 +303,79 @@ def test_primitive_gates( checker_circuit: Circuit = framework(num_qubits) gate_func(checker_circuit) + assert decomposed_circuit == checker_circuit + + @pytest.mark.parametrize("framework", CIRCUIT_FRAMEWORKS) + def test_decompose_gates( + self, + framework: type[Circuit] + ) -> None: + """ Test specific gate decomposition using `decompose_gates` method. + + Parameters + ---------- + `framework` : type[quick.circuit.Circuit] + """ + circuit: Circuit = framework(3) + circuit.UCRZ([0.1, 0.2, 0.3, 0.4], [0, 1], 2) + circuit.H(0) + circuit.Z(0) + + decomposed_circuit = circuit.decompose_gate(["UCRZ", "RZ", "Z"]) + + checker_circuit: Circuit = framework(3) + + checker_circuit.RX(angle=1.5707963267948966, qubit_indices=2) + checker_circuit.RY(angle=-0.25, qubit_indices=2) + checker_circuit.RX(angle=-1.5707963267948966, qubit_indices=2) + checker_circuit.CX(control_index=0, target_index=2) + checker_circuit.RX(angle=1.5707963267948966, qubit_indices=2) + checker_circuit.RY(angle=0.05000000000000002, qubit_indices=2) + checker_circuit.RX(angle=-1.5707963267948966, qubit_indices=2) + checker_circuit.CX(control_index=1, target_index=2) + checker_circuit.RX(angle=1.5707963267948966, qubit_indices=2) + checker_circuit.RX(angle=-1.5707963267948966, qubit_indices=2) + checker_circuit.CX(control_index=0, target_index=2) + checker_circuit.RX(angle=1.5707963267948966, qubit_indices=2) + checker_circuit.RY(angle=0.1, qubit_indices=2) + checker_circuit.RX(angle=-1.5707963267948966, qubit_indices=2) + checker_circuit.CX(control_index=1, target_index=2) + checker_circuit.H(qubit_indices=0) + checker_circuit.Phase(angle=3.141592653589793, qubit_indices=0) + + assert decomposed_circuit == checker_circuit + + @pytest.mark.parametrize("framework", CIRCUIT_FRAMEWORKS) + @pytest.mark.parametrize("gate_func, num_qubits", GATE_TYPES) + def test_decompose_gates_primitive_gates( + self, + framework: type[Circuit], + gate_func, + num_qubits: int + ) -> None: + """ Test the decomposition of the primitive gates with `decompose_gates` method. + + Parameters + ---------- + `framework` : type[quick.circuit.Circuit] + The circuit framework to use. + `gate_func` : Callable[[Circuit], None] + The gate function to apply. + `num_qubits` : int + The number of qubits in the circuit. + """ + # Define the circuit and apply the gate + circuit: Circuit = framework(num_qubits) + gate_func(circuit) + + assert circuit.circuit_log[-1]["definition"] == [] + + # Decompose the circuit + decomposed_circuit = circuit.decompose_gate(circuit.circuit_log[-1]["gate"]) + + # Check the decomposed circuit + # Assert it's unchanged + checker_circuit: Circuit = framework(num_qubits) + gate_func(checker_circuit) + assert decomposed_circuit == checker_circuit \ No newline at end of file diff --git a/tests/circuit/test_circuit_to_controlled.py b/tests/circuit/test_circuit_to_controlled.py index 587e94a..608d1cc 100644 --- a/tests/circuit/test_circuit_to_controlled.py +++ b/tests/circuit/test_circuit_to_controlled.py @@ -1438,4 +1438,35 @@ def test_global_phase_in_target( check_multiple_controlled_circuit.MCPhase(0.5, 0, 1) assert single_controlled_circuit == check_single_controlled_circuit - assert multiple_controlled_circuit == check_multiple_controlled_circuit \ No newline at end of file + assert multiple_controlled_circuit == check_multiple_controlled_circuit + + @pytest.mark.parametrize("circuit_framework", CIRCUIT_FRAMEWORKS) + def test_unitary_control( + self, + circuit_framework: type[Circuit] + ) -> None: + """ Test the `.control()` method with a unitary. + + Parameters + ---------- + `circuit_framework` : type[quick.circuit.Circuit] + The framework to convert the circuit to. + """ + from quick.random import generate_random_unitary + from quick.primitives import Operator + from numpy.testing import assert_almost_equal + + unitary = generate_random_unitary(3) + + circuit = circuit_framework(num_qubits=3) + circuit.unitary(unitary, [0, 1, 2]) + + single_controlled_circuit = circuit.control(1) + multiple_controlled_circuit = circuit.control(2) + + operator = Operator(unitary) + single_controlled_checker = operator.control(1) + multiple_controlled_checker = operator.control(2) + + assert_almost_equal(single_controlled_circuit.get_unitary(), single_controlled_checker.data) + assert_almost_equal(multiple_controlled_circuit.get_unitary(), multiple_controlled_checker.data) \ No newline at end of file diff --git a/tests/circuit/test_cirq_circuit.py b/tests/circuit/test_cirq_circuit.py index a269f5b..de15caf 100644 --- a/tests/circuit/test_cirq_circuit.py +++ b/tests/circuit/test_cirq_circuit.py @@ -587,12 +587,12 @@ def test_Tdg( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RX(np.pi/4).matrix], - [1, 0, 1/3, RX(1/3).matrix], - [1, 0, -1/4, RX(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RX(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RX(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).matrix, RX(np.pi/4).matrix))] + [1, 0, np.pi/4, RX(np.pi/4).data], + [1, 0, 1/3, RX(1/3).data], + [1, 0, -1/4, RX(-1/4).data], + [2, 1, np.pi/4, np.kron(RX(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RX(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).data, RX(np.pi/4).data))] ]) def test_RX( self, @@ -610,12 +610,12 @@ def test_RX( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RY(np.pi/4).matrix], - [1, 0, 1/3, RY(1/3).matrix], - [1, 0, -1/4, RY(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RY(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RY(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).matrix, RY(np.pi/4).matrix))] + [1, 0, np.pi/4, RY(np.pi/4).data], + [1, 0, 1/3, RY(1/3).data], + [1, 0, -1/4, RY(-1/4).data], + [2, 1, np.pi/4, np.kron(RY(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RY(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).data, RY(np.pi/4).data))] ]) def test_RY( self, @@ -633,12 +633,12 @@ def test_RY( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RZ(np.pi/4).matrix], - [1, 0, 1/3, RZ(1/3).matrix], - [1, 0, -1/4, RZ(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RZ(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RZ(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).matrix, RZ(np.pi/4).matrix))] + [1, 0, np.pi/4, RZ(np.pi/4).data], + [1, 0, 1/3, RZ(1/3).data], + [1, 0, -1/4, RZ(-1/4).data], + [2, 1, np.pi/4, np.kron(RZ(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RZ(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).data, RZ(np.pi/4).data))] ]) def test_RZ( self, @@ -656,12 +656,12 @@ def test_RZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, Phase(np.pi/4).matrix], - [1, 0, 1/3, Phase(1/3).matrix], - [1, 0, -1/4, Phase(-1/4).matrix], - [2, 1, np.pi/4, np.kron(Phase(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(Phase(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).matrix, Phase(np.pi/4).matrix))] + [1, 0, np.pi/4, Phase(np.pi/4).data], + [1, 0, 1/3, Phase(1/3).data], + [1, 0, -1/4, Phase(-1/4).data], + [2, 1, np.pi/4, np.kron(Phase(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(Phase(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).data, Phase(np.pi/4).data))] ]) def test_Phase( self, @@ -826,22 +826,22 @@ def test_RZZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angles, expected", [ - [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).matrix], - [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).matrix, np.eye(2))], - [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).matrix, np.eye(4))], - [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).matrix], + [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).data], + [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).data, np.eye(2))], + [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).data, np.eye(4))], + [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).data], [3, [0, 2], (np.pi/2, np.pi/3, np.pi/4), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, + U3(np.pi/2, np.pi/3, np.pi/4).data, np.kron( np.eye(2), - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data ) )], [3, [0, 1], (np.pi/2, np.pi/3, np.pi/4), np.kron( np.eye(2), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data, + U3(np.pi/2, np.pi/3, np.pi/4).data ) ) ] diff --git a/tests/circuit/test_control_state.py b/tests/circuit/test_control_state.py index 246e0c3..7dc79cd 100644 --- a/tests/circuit/test_control_state.py +++ b/tests/circuit/test_control_state.py @@ -1474,14 +1474,14 @@ def test_Multiplexor_control_state( The circuit framework to test. """ gates = [ - RX(np.pi/2).matrix, - RX(np.pi/3).matrix, - RX(np.pi/4).matrix, - RX(np.pi/5).matrix, - RX(np.pi/6).matrix, - RX(np.pi/7).matrix, - RX(np.pi/8).matrix, - RX(np.pi/9).matrix + RX(np.pi/2).data, + RX(np.pi/3).data, + RX(np.pi/4).data, + RX(np.pi/5).data, + RX(np.pi/6).data, + RX(np.pi/7).data, + RX(np.pi/8).data, + RX(np.pi/9).data ] # Given control state "0" diff --git a/tests/circuit/test_pennylane_circuit.py b/tests/circuit/test_pennylane_circuit.py index d56cbcc..6a620e3 100644 --- a/tests/circuit/test_pennylane_circuit.py +++ b/tests/circuit/test_pennylane_circuit.py @@ -584,12 +584,12 @@ def test_Tdg( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RX(np.pi/4).matrix], - [1, 0, 1/3, RX(1/3).matrix], - [1, 0, -1/4, RX(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RX(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RX(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).matrix, RX(np.pi/4).matrix))] + [1, 0, np.pi/4, RX(np.pi/4).data], + [1, 0, 1/3, RX(1/3).data], + [1, 0, -1/4, RX(-1/4).data], + [2, 1, np.pi/4, np.kron(RX(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RX(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).data, RX(np.pi/4).data))] ]) def test_RX( self, @@ -607,12 +607,12 @@ def test_RX( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RY(np.pi/4).matrix], - [1, 0, 1/3, RY(1/3).matrix], - [1, 0, -1/4, RY(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RY(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RY(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).matrix, RY(np.pi/4).matrix))] + [1, 0, np.pi/4, RY(np.pi/4).data], + [1, 0, 1/3, RY(1/3).data], + [1, 0, -1/4, RY(-1/4).data], + [2, 1, np.pi/4, np.kron(RY(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RY(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).data, RY(np.pi/4).data))] ]) def test_RY( self, @@ -630,12 +630,12 @@ def test_RY( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RZ(np.pi/4).matrix], - [1, 0, 1/3, RZ(1/3).matrix], - [1, 0, -1/4, RZ(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RZ(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RZ(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).matrix, RZ(np.pi/4).matrix))] + [1, 0, np.pi/4, RZ(np.pi/4).data], + [1, 0, 1/3, RZ(1/3).data], + [1, 0, -1/4, RZ(-1/4).data], + [2, 1, np.pi/4, np.kron(RZ(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RZ(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).data, RZ(np.pi/4).data))] ]) def test_RZ( self, @@ -653,12 +653,12 @@ def test_RZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, Phase(np.pi/4).matrix], - [1, 0, 1/3, Phase(1/3).matrix], - [1, 0, -1/4, Phase(-1/4).matrix], - [2, 1, np.pi/4, np.kron(Phase(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(Phase(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).matrix, Phase(np.pi/4).matrix))] + [1, 0, np.pi/4, Phase(np.pi/4).data], + [1, 0, 1/3, Phase(1/3).data], + [1, 0, -1/4, Phase(-1/4).data], + [2, 1, np.pi/4, np.kron(Phase(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(Phase(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).data, Phase(np.pi/4).data))] ]) def test_Phase( self, @@ -823,22 +823,22 @@ def test_RZZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angles, expected", [ - [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).matrix], - [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).matrix, np.eye(2))], - [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).matrix, np.eye(4))], - [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).matrix], + [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).data], + [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).data, np.eye(2))], + [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).data, np.eye(4))], + [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).data], [3, [0, 2], (np.pi/2, np.pi/3, np.pi/4), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, + U3(np.pi/2, np.pi/3, np.pi/4).data, np.kron( np.eye(2), - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data ) )], [3, [0, 1], (np.pi/2, np.pi/3, np.pi/4), np.kron( np.eye(2), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data, + U3(np.pi/2, np.pi/3, np.pi/4).data ) ) ] diff --git a/tests/circuit/test_qiskit_circuit.py b/tests/circuit/test_qiskit_circuit.py index 1bcabef..2160ee9 100644 --- a/tests/circuit/test_qiskit_circuit.py +++ b/tests/circuit/test_qiskit_circuit.py @@ -586,12 +586,12 @@ def test_Tdg( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RX(np.pi/4).matrix], - [1, 0, 1/3, RX(1/3).matrix], - [1, 0, -1/4, RX(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RX(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RX(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).matrix, RX(np.pi/4).matrix))] + [1, 0, np.pi/4, RX(np.pi/4).data], + [1, 0, 1/3, RX(1/3).data], + [1, 0, -1/4, RX(-1/4).data], + [2, 1, np.pi/4, np.kron(RX(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RX(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).data, RX(np.pi/4).data))] ]) def test_RX( self, @@ -609,12 +609,12 @@ def test_RX( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RY(np.pi/4).matrix], - [1, 0, 1/3, RY(1/3).matrix], - [1, 0, -1/4, RY(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RY(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RY(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).matrix, RY(np.pi/4).matrix))] + [1, 0, np.pi/4, RY(np.pi/4).data], + [1, 0, 1/3, RY(1/3).data], + [1, 0, -1/4, RY(-1/4).data], + [2, 1, np.pi/4, np.kron(RY(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RY(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).data, RY(np.pi/4).data))] ]) def test_RY( self, @@ -632,12 +632,12 @@ def test_RY( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RZ(np.pi/4).matrix], - [1, 0, 1/3, RZ(1/3).matrix], - [1, 0, -1/4, RZ(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RZ(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RZ(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).matrix, RZ(np.pi/4).matrix))] + [1, 0, np.pi/4, RZ(np.pi/4).data], + [1, 0, 1/3, RZ(1/3).data], + [1, 0, -1/4, RZ(-1/4).data], + [2, 1, np.pi/4, np.kron(RZ(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RZ(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).data, RZ(np.pi/4).data))] ]) def test_RZ( self, @@ -655,12 +655,12 @@ def test_RZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, Phase(np.pi/4).matrix], - [1, 0, 1/3, Phase(1/3).matrix], - [1, 0, -1/4, Phase(-1/4).matrix], - [2, 1, np.pi/4, np.kron(Phase(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(Phase(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).matrix, Phase(np.pi/4).matrix))] + [1, 0, np.pi/4, Phase(np.pi/4).data], + [1, 0, 1/3, Phase(1/3).data], + [1, 0, -1/4, Phase(-1/4).data], + [2, 1, np.pi/4, np.kron(Phase(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(Phase(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).data, Phase(np.pi/4).data))] ]) def test_Phase( self, @@ -825,22 +825,22 @@ def test_RZZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angles, expected", [ - [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).matrix], - [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).matrix, np.eye(2))], - [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).matrix, np.eye(4))], - [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).matrix], + [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).data], + [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).data, np.eye(2))], + [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).data, np.eye(4))], + [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).data], [3, [0, 2], (np.pi/2, np.pi/3, np.pi/4), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, + U3(np.pi/2, np.pi/3, np.pi/4).data, np.kron( np.eye(2), - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data ) )], [3, [0, 1], (np.pi/2, np.pi/3, np.pi/4), np.kron( np.eye(2), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data, + U3(np.pi/2, np.pi/3, np.pi/4).data ) ) ] diff --git a/tests/circuit/test_quimb_circuit.py b/tests/circuit/test_quimb_circuit.py index 4f0ad2f..224dead 100644 --- a/tests/circuit/test_quimb_circuit.py +++ b/tests/circuit/test_quimb_circuit.py @@ -584,12 +584,12 @@ def test_Tdg( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RX(np.pi/4).matrix], - [1, 0, 1/3, RX(1/3).matrix], - [1, 0, -1/4, RX(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RX(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RX(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).matrix, RX(np.pi/4).matrix))] + [1, 0, np.pi/4, RX(np.pi/4).data], + [1, 0, 1/3, RX(1/3).data], + [1, 0, -1/4, RX(-1/4).data], + [2, 1, np.pi/4, np.kron(RX(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RX(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).data, RX(np.pi/4).data))] ]) def test_RX( self, @@ -607,12 +607,12 @@ def test_RX( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RY(np.pi/4).matrix], - [1, 0, 1/3, RY(1/3).matrix], - [1, 0, -1/4, RY(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RY(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RY(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).matrix, RY(np.pi/4).matrix))] + [1, 0, np.pi/4, RY(np.pi/4).data], + [1, 0, 1/3, RY(1/3).data], + [1, 0, -1/4, RY(-1/4).data], + [2, 1, np.pi/4, np.kron(RY(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RY(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).data, RY(np.pi/4).data))] ]) def test_RY( self, @@ -630,12 +630,12 @@ def test_RY( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RZ(np.pi/4).matrix], - [1, 0, 1/3, RZ(1/3).matrix], - [1, 0, -1/4, RZ(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RZ(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RZ(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).matrix, RZ(np.pi/4).matrix))] + [1, 0, np.pi/4, RZ(np.pi/4).data], + [1, 0, 1/3, RZ(1/3).data], + [1, 0, -1/4, RZ(-1/4).data], + [2, 1, np.pi/4, np.kron(RZ(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RZ(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).data, RZ(np.pi/4).data))] ]) def test_RZ( self, @@ -653,12 +653,12 @@ def test_RZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, Phase(np.pi/4).matrix], - [1, 0, 1/3, Phase(1/3).matrix], - [1, 0, -1/4, Phase(-1/4).matrix], - [2, 1, np.pi/4, np.kron(Phase(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(Phase(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).matrix, Phase(np.pi/4).matrix))] + [1, 0, np.pi/4, Phase(np.pi/4).data], + [1, 0, 1/3, Phase(1/3).data], + [1, 0, -1/4, Phase(-1/4).data], + [2, 1, np.pi/4, np.kron(Phase(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(Phase(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).data, Phase(np.pi/4).data))] ]) def test_Phase( self, @@ -823,22 +823,22 @@ def test_RZZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angles, expected", [ - [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).matrix], - [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).matrix, np.eye(2))], - [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).matrix, np.eye(4))], - [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).matrix], + [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).data], + [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).data, np.eye(2))], + [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).data, np.eye(4))], + [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).data], [3, [0, 2], (np.pi/2, np.pi/3, np.pi/4), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, + U3(np.pi/2, np.pi/3, np.pi/4).data, np.kron( np.eye(2), - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data ) )], [3, [0, 1], (np.pi/2, np.pi/3, np.pi/4), np.kron( np.eye(2), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data, + U3(np.pi/2, np.pi/3, np.pi/4).data ) ) ] diff --git a/tests/circuit/test_tket_circuit.py b/tests/circuit/test_tket_circuit.py index 60049d8..dd0a414 100644 --- a/tests/circuit/test_tket_circuit.py +++ b/tests/circuit/test_tket_circuit.py @@ -586,12 +586,12 @@ def test_Tdg( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RX(np.pi/4).matrix], - [1, 0, 1/3, RX(1/3).matrix], - [1, 0, -1/4, RX(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RX(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RX(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).matrix, RX(np.pi/4).matrix))] + [1, 0, np.pi/4, RX(np.pi/4).data], + [1, 0, 1/3, RX(1/3).data], + [1, 0, -1/4, RX(-1/4).data], + [2, 1, np.pi/4, np.kron(RX(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RX(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RX(np.pi/4).data, RX(np.pi/4).data))] ]) def test_RX( self, @@ -609,12 +609,12 @@ def test_RX( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RY(np.pi/4).matrix], - [1, 0, 1/3, RY(1/3).matrix], - [1, 0, -1/4, RY(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RY(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RY(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).matrix, RY(np.pi/4).matrix))] + [1, 0, np.pi/4, RY(np.pi/4).data], + [1, 0, 1/3, RY(1/3).data], + [1, 0, -1/4, RY(-1/4).data], + [2, 1, np.pi/4, np.kron(RY(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RY(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RY(np.pi/4).data, RY(np.pi/4).data))] ]) def test_RY( self, @@ -632,12 +632,12 @@ def test_RY( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, RZ(np.pi/4).matrix], - [1, 0, 1/3, RZ(1/3).matrix], - [1, 0, -1/4, RZ(-1/4).matrix], - [2, 1, np.pi/4, np.kron(RZ(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(RZ(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).matrix, RZ(np.pi/4).matrix))] + [1, 0, np.pi/4, RZ(np.pi/4).data], + [1, 0, 1/3, RZ(1/3).data], + [1, 0, -1/4, RZ(-1/4).data], + [2, 1, np.pi/4, np.kron(RZ(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(RZ(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(RZ(np.pi/4).data, RZ(np.pi/4).data))] ]) def test_RZ( self, @@ -655,12 +655,12 @@ def test_RZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angle, expected", [ - [1, 0, np.pi/4, Phase(np.pi/4).matrix], - [1, 0, 1/3, Phase(1/3).matrix], - [1, 0, -1/4, Phase(-1/4).matrix], - [2, 1, np.pi/4, np.kron(Phase(np.pi/4).matrix, np.eye(2))], - [3, 2, 1/3, np.kron(Phase(1/3).matrix, np.eye(4))], - [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).matrix, Phase(np.pi/4).matrix))] + [1, 0, np.pi/4, Phase(np.pi/4).data], + [1, 0, 1/3, Phase(1/3).data], + [1, 0, -1/4, Phase(-1/4).data], + [2, 1, np.pi/4, np.kron(Phase(np.pi/4).data, np.eye(2))], + [3, 2, 1/3, np.kron(Phase(1/3).data, np.eye(4))], + [3, [0, 1], np.pi/4, np.kron(np.eye(2), np.kron(Phase(np.pi/4).data, Phase(np.pi/4).data))] ]) def test_Phase( self, @@ -825,22 +825,22 @@ def test_RZZ( assert_almost_equal(circuit.get_unitary(), expected, 8) @pytest.mark.parametrize("num_qubits, qubit_indices, angles, expected", [ - [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).matrix], - [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).matrix, np.eye(2))], - [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).matrix, np.eye(4))], - [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).matrix], + [1, 0, (np.pi/2, np.pi/3, np.pi/4), U3(np.pi/2, np.pi/3, np.pi/4).data], + [2, 1, (np.pi/2, -np.pi/3, np.pi/4), np.kron(U3(np.pi/2, -np.pi/3, np.pi/4).data, np.eye(2))], + [3, 2, (np.pi/2, np.pi/3, -np.pi/4), np.kron(U3(np.pi/2, np.pi/3, -np.pi/4).data, np.eye(4))], + [1, 0, (1/3, 1/4, 1/5), U3(1/3, 1/4, 1/5).data], [3, [0, 2], (np.pi/2, np.pi/3, np.pi/4), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, + U3(np.pi/2, np.pi/3, np.pi/4).data, np.kron( np.eye(2), - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data ) )], [3, [0, 1], (np.pi/2, np.pi/3, np.pi/4), np.kron( np.eye(2), np.kron( - U3(np.pi/2, np.pi/3, np.pi/4).matrix, - U3(np.pi/2, np.pi/3, np.pi/4).matrix + U3(np.pi/2, np.pi/3, np.pi/4).data, + U3(np.pi/2, np.pi/3, np.pi/4).data ) ) ] diff --git a/tests/circuit/test_uniformly_controlled_gates.py b/tests/circuit/test_uniformly_controlled_gates.py index db45b72..7a82e56 100644 --- a/tests/circuit/test_uniformly_controlled_gates.py +++ b/tests/circuit/test_uniformly_controlled_gates.py @@ -272,54 +272,54 @@ def test_Diagonal( @pytest.mark.parametrize("multiplexor_simplification", [True, False]) @pytest.mark.parametrize("single_qubit_gates", [ [ - Hadamard().matrix, - PauliX().matrix, - Hadamard().matrix, - PauliX().matrix + Hadamard.data, + PauliX.data, + Hadamard.data, + PauliX.data ], [ - RX(np.pi/2).matrix, - RY(np.pi/3).matrix, - RX(np.pi/4).matrix, - RY(np.pi/5).matrix, - RX(np.pi/6).matrix, - RY(np.pi/7).matrix, - RX(np.pi/8).matrix, - RY(np.pi/9).matrix + RX(np.pi/2).data, + RY(np.pi/3).data, + RX(np.pi/4).data, + RY(np.pi/5).data, + RX(np.pi/6).data, + RY(np.pi/7).data, + RX(np.pi/8).data, + RY(np.pi/9).data ], [ - RY(np.pi).matrix, - RX(np.pi/2).matrix, - RY(np.pi/3).matrix, - RX(np.pi/4).matrix, - RY(np.pi/5).matrix, - RX(np.pi/6).matrix, - RY(np.pi/7).matrix, - RX(np.pi/8).matrix, - RY(np.pi/9).matrix, - RX(np.pi/10).matrix, - RY(np.pi/11).matrix, - RX(np.pi/12).matrix, - RY(np.pi/13).matrix, - RX(np.pi/14).matrix, - RY(np.pi/15).matrix, - RX(np.pi/16).matrix, - RY(np.pi/17).matrix, - RX(np.pi/18).matrix, - RY(np.pi/19).matrix, - RX(np.pi/20).matrix, - RY(np.pi/21).matrix, - RX(np.pi/22).matrix, - RY(np.pi/23).matrix, - RX(np.pi/24).matrix, - RY(np.pi/25).matrix, - RX(np.pi/26).matrix, - RY(np.pi/27).matrix, - RX(np.pi/28).matrix, - RY(np.pi/29).matrix, - RX(np.pi/30).matrix, - RY(np.pi/31).matrix, - RX(np.pi/32).matrix + RY(np.pi).data, + RX(np.pi/2).data, + RY(np.pi/3).data, + RX(np.pi/4).data, + RY(np.pi/5).data, + RX(np.pi/6).data, + RY(np.pi/7).data, + RX(np.pi/8).data, + RY(np.pi/9).data, + RX(np.pi/10).data, + RY(np.pi/11).data, + RX(np.pi/12).data, + RY(np.pi/13).data, + RX(np.pi/14).data, + RY(np.pi/15).data, + RX(np.pi/16).data, + RY(np.pi/17).data, + RX(np.pi/18).data, + RY(np.pi/19).data, + RX(np.pi/20).data, + RY(np.pi/21).data, + RX(np.pi/22).data, + RY(np.pi/23).data, + RX(np.pi/24).data, + RY(np.pi/25).data, + RX(np.pi/26).data, + RY(np.pi/27).data, + RX(np.pi/28).data, + RY(np.pi/29).data, + RX(np.pi/30).data, + RY(np.pi/31).data, + RX(np.pi/32).data ], ]) def test_Multiplexor( @@ -343,7 +343,7 @@ def test_Multiplexor( `multiplexor_simplification` : bool, optional, default=True Determines if the multiplexor is simplified. """ - from quick.circuit.circuit_utils import ( + from quick.circuit.utils import ( extract_single_qubits_and_diagonal, simplify ) diff --git a/tests/compiler/test_shende_compiler.py b/tests/compiler/test_shende_compiler.py index be52504..3a66e15 100644 --- a/tests/compiler/test_shende_compiler.py +++ b/tests/compiler/test_shende_compiler.py @@ -23,15 +23,13 @@ from quick.circuit import TKETCircuit from quick.compiler import ShendeCompiler -from quick.primitives import Bra, Ket, Operator +from quick.primitives import Statevector, Operator from quick.random import generate_random_state, generate_random_unitary # Define the test data generated_statevector = generate_random_state(7) -test_data_bra = Bra(generated_statevector) -test_data_ket = Ket(generated_statevector) -checker_data_ket = copy.deepcopy(test_data_ket) -checker_data_bra = copy.deepcopy(test_data_ket.to_bra()) +test_statevector = Statevector(generated_statevector) +checker_statevector = copy.deepcopy(test_statevector) unitary_matrix = generate_random_unitary(3) @@ -66,7 +64,7 @@ def test_state_preparation(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_unitary_preparation(self) -> None: # Initialize the Shende compiler @@ -86,8 +84,7 @@ def test_check_primitive(self) -> None: shende_compiler = ShendeCompiler(circuit_framework=TKETCircuit) # Ensure that the checker does not raise an error when a valid primitive is passed - shende_compiler._check_primitive(test_data_ket) - shende_compiler._check_primitive(test_data_bra) + shende_compiler._check_primitive(test_statevector) shende_compiler._check_primitive(Operator(unitary_matrix)) shende_compiler._check_primitive(generated_statevector) shende_compiler._check_primitive(unitary_matrix) @@ -117,8 +114,7 @@ def test_check_primitive_qubits(self) -> None: shende_compiler = ShendeCompiler(circuit_framework=TKETCircuit) # Ensure that the checker does not raise an error when valid qubits are passed - shende_compiler._check_primitive_qubits(test_data_ket, range(7)) - shende_compiler._check_primitive_qubits(test_data_bra, range(7)) + shende_compiler._check_primitive_qubits(test_statevector, range(7)) shende_compiler._check_primitive_qubits(Operator(unitary_matrix), range(3)) shende_compiler._check_primitive_qubits(generated_statevector, range(7)) shende_compiler._check_primitive_qubits(unitary_matrix, range(3)) @@ -129,7 +125,7 @@ def test_check_primitive_qubits_invalid_qubits(self) -> None: # Ensure that the checker raises a ValueError when invalid qubits are passed with pytest.raises(ValueError): - shende_compiler._check_primitive_qubits(test_data_ket, range(8)) + shende_compiler._check_primitive_qubits(test_statevector, range(8)) def test_check_primitives(self) -> None: # Initialize the Shende compiler @@ -137,39 +133,24 @@ def test_check_primitives(self) -> None: # Ensure that the checker does not raise an error when valid primitives are passed shende_compiler._check_primitives([ - (test_data_ket, range(7)), - (test_data_bra, range(7)), + (test_statevector, range(7)), (Operator(unitary_matrix), range(3)), (generated_statevector, range(7)), (unitary_matrix, range(3)) ]) - def test_compile_primitive_bra(self) -> None: - # Initialize the Shende compiler - shende_compiler = ShendeCompiler(circuit_framework=TKETCircuit) - - # Encode the data to a circuit - circuit = shende_compiler._compile_primitive(test_data_bra) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) - - def test_compile_primitive_ket(self) -> None: # Initialize the Shende compiler shende_compiler = ShendeCompiler(circuit_framework=TKETCircuit) # Encode the data to a circuit - circuit = shende_compiler._compile_primitive(test_data_ket) + circuit = shende_compiler._compile_primitive(test_statevector) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_compile_primitive_operator(self) -> None: # Initialize the Shende compiler @@ -195,7 +176,7 @@ def test_compile_primitive_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) # Encode the data to a circuit circuit = shende_compiler._compile_primitive(unitary_matrix) @@ -220,31 +201,18 @@ def test_compile_primitive_invalid_primitive(self) -> None: with pytest.raises(ValueError): shende_compiler._compile_primitive(np.array([0])) - def test_compile_bra(self) -> None: - # Initialize the Shende compiler - shende_compiler = ShendeCompiler(circuit_framework=TKETCircuit) - - # Encode the data to a circuit - circuit = shende_compiler.compile(test_data_bra) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) - def test_compile_ket(self) -> None: # Initialize the Shende compiler shende_compiler = ShendeCompiler(circuit_framework=TKETCircuit) # Encode the data to a circuit - circuit = shende_compiler.compile(test_data_ket) + circuit = shende_compiler.compile(test_statevector) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_compile_operator(self) -> None: # Initialize the Shende compiler @@ -270,7 +238,7 @@ def test_compile_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) # Encode the data to a circuit circuit = shende_compiler.compile(unitary_matrix) @@ -287,10 +255,8 @@ def test_compile_multiple_primitives(self) -> None: # Generate a random bra and ket over three qubits generated_statevector = generate_random_state(3) - test_data_ket = Ket(generated_statevector) - test_data_bra = Bra(generated_statevector) + test_data_ket = Statevector(generated_statevector) checker_data_ket = copy.deepcopy(test_data_ket) - checker_data_bra = copy.deepcopy(test_data_ket.to_bra()) # Generate two random unitary matrix over three qubits unitary_matrix_1 = generate_random_unitary(3) @@ -299,7 +265,7 @@ def test_compile_multiple_primitives(self) -> None: # Encode the data to a circuit circuit = shende_compiler.compile([ (test_data_ket, [0, 1, 2]), - (test_data_bra, [3, 4, 5]), + (test_data_ket, [3, 4, 5]), (unitary_matrix_1, [0, 1, 2]), (unitary_matrix_2, [3, 4, 5]) ]) @@ -308,8 +274,8 @@ def test_compile_multiple_primitives(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - checker_data_ket = np.dot(unitary_matrix_1, checker_data_ket.data.flatten()) - checker_data_bra = np.dot(unitary_matrix_2, checker_data_bra.data.flatten()) - checker_statevector = np.kron(checker_data_bra, checker_data_ket) + checker_data_ket_1 = np.dot(unitary_matrix_1, checker_data_ket.data.flatten()) + checker_data_ket_2 = np.dot(unitary_matrix_2, checker_data_ket.data.flatten()) + checker_statevector = np.kron(checker_data_ket_2, checker_data_ket_1) assert_almost_equal(np.array(statevector), checker_statevector, decimal=8) \ No newline at end of file diff --git a/tests/metrics/test_metrics.py b/tests/metrics/test_metrics.py index 79eee22..8ac1ec2 100644 --- a/tests/metrics/test_metrics.py +++ b/tests/metrics/test_metrics.py @@ -28,7 +28,8 @@ calculate_shannon_entropy, calculate_entanglement_entropy, calculate_entanglement_entropy_slope, - calculate_hilbert_schmidt_test + calculate_hilbert_schmidt_test, + calculate_frobenius_distance ) @@ -179,4 +180,10 @@ def test_calculate_hilbert_schmidt_fail(self) -> None: with pytest.raises(ValueError): calculate_hilbert_schmidt_test(unitary, np.zeros((4, 3))) # type: ignore with pytest.raises(ValueError): - calculate_hilbert_schmidt_test(np.zeros((4, 4)), unitary) # type: ignore \ No newline at end of file + calculate_hilbert_schmidt_test(np.zeros((4, 4)), unitary) # type: ignore + + def test_calculate_frobenius_distance(self) -> None: + """ Test the `calculate_frobenius_distance` method. + """ + unitary = unitary_group.rvs(4).astype(np.complex128) + assert_almost_equal(0.0, calculate_frobenius_distance(unitary, unitary)) \ No newline at end of file diff --git a/tests/predicates/__init__.py b/tests/predicates/__init__.py index ef97a38..0aaa5d2 100644 --- a/tests/predicates/__init__.py +++ b/tests/predicates/__init__.py @@ -12,24 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__all__ = [ - "test_is_square_matrix", - "test_is_diagonal_matrix", - "test_is_symmetric_matrix", - "test_is_identity_matrix", - "test_is_unitary_matrix", - "test_is_hermitian_matrix", - "test_is_positive_semidefinite_matrix", - "test_is_isometry" -] +__all__ = ["TestPredicates"] -from tests.predicates.test_predicates import ( - test_is_square_matrix, - test_is_diagonal_matrix, - test_is_symmetric_matrix, - test_is_identity_matrix, - test_is_unitary_matrix, - test_is_hermitian_matrix, - test_is_positive_semidefinite_matrix, - test_is_isometry -) +from tests.predicates.test_predicates import TestPredicates \ No newline at end of file diff --git a/tests/predicates/test_predicates.py b/tests/predicates/test_predicates.py index ddd19b5..927debe 100644 --- a/tests/predicates/test_predicates.py +++ b/tests/predicates/test_predicates.py @@ -14,14 +14,23 @@ from __future__ import annotations +__all__ = ["TestPredicates"] + import numpy as np from numpy.typing import NDArray import pytest from scipy.stats import unitary_group -from quick.predicates import ( +from quick.predicates.predicates import ( + is_power, + is_normalized, is_statevector, is_square_matrix, + is_orthogonal_matrix, + is_real_matrix, + is_special_matrix, + is_special_orthogonal_matrix, + is_special_unitary_matrix, is_diagonal_matrix, is_symmetric_matrix, is_identity_matrix, @@ -29,362 +38,633 @@ is_hermitian_matrix, is_positive_semidefinite_matrix, is_isometry, - is_density_matrix + is_density_matrix, + is_product_matrix, + is_locally_equivalent, + is_supercontrolled ) -@pytest.mark.parametrize("array, system_size, expected", [ - (np.array([1, 0]), 2, True), - (np.array([0, 1]), 2, True), - (np.array([1, 0, 0]), 3, True), - (np.array([1, 2]), 2, False), - (np.array([1, 2, 3]), 3, False), - (np.array([1, 0, 0, 0]), 2, True), - (np.array([[0.5], [0.5], [0.5], [0.5]]), 2, True) -]) -def test_is_statevector( - array: NDArray[np.complex128], - system_size: int, - expected: bool - ) -> None: - """ Test the `.is_statevector()` method. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a statevector. - `system_size` : int - The size of the system. If it's 2, then it's a qubit system. - `expected` : bool - The expected output of the function. +class TestPredicates: + """ `tests.predicates.TestPredicates` is the tester class for `quick.predicates` + module. """ - assert is_statevector(array, system_size) is expected + def test_is_power(self) -> None: + """ Test the `is_power()` function. + """ + assert is_power(2, 2) is True + assert is_power(3, 2) is False + assert is_power(2, 4) is True -def test_is_statevector_invalid_system_size() -> None: - """ Test the `.is_statevector()` method with invalid system size. + def test_is_normalized(self) -> None: + """ Test the `is_normalized()` function. + """ + state = np.arange(10).astype(np.complex128) + assert is_normalized(state) is False + state = state / np.linalg.norm(state) + assert is_normalized(state) is True - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a statevector. - """ - with pytest.raises(ValueError): - is_statevector(np.array([1, 0]), system_size=0) - -@pytest.mark.parametrize("array, expected", [ - (np.random.rand(2, 2), True), - (np.random.rand(3, 3), True), - (np.random.rand(4, 4), True), - (np.random.rand(5, 5), True), - (np.random.rand(2, 1), False), - (np.random.rand(1, 3), False), - (np.random.rand(4, 12), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_square_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_square_matrix()` method. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a square matrix. - `expected` : bool - The expected output of the function. - """ - assert is_square_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.diag([1, 2, 3]), True), - (np.diag([4, 5, 6, 7]), True), - (np.diag([8, 9]), True), - (np.array([ - [1, 2, 0], - [0, 3, 4], - [0, 0, 5] - ]), False), - (np.array([ - [1, 0, 0], - [2, 3, 0], - [0, 0, 4] - ]), False), - (np.array([ - [1, 0], - [1, 1] - ]), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_diagonal_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_diagonal_matrix()` method with diagonal matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a diagonal matrix. - `expected` : bool - The expected output of the function. - """ - assert is_diagonal_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.array([ - [1, 2, 3], - [2, 4, 5], - [3, 5, 6] - ]), True), - (np.array([ - [1, 2, 3, 4], - [2, 5, 6, 7], - [3, 6, 8, 9], - [4, 7, 9, 10] - ]), True), - (np.array([ - [1, 2], - [2, 3] - ]), True), - (np.array([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9] - ]), False), - (np.array([ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - [13, 14, 15, 16] - ]), False), - (np.array([ - [1, 2], - [3, 4] - ]), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_symmetric_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_symmetric_matrix()` method with symmetric matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a symmetric matrix. - `expected` : bool - The expected output of the function. - """ - assert is_symmetric_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.eye(2), True), - (np.eye(3), True), - (np.eye(4), True), - (np.eye(5), True), - (np.random.rand(2, 2), False), - (np.random.rand(3, 3), False), - (np.random.rand(4, 4), False), - (np.random.rand(3, 4), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_identity_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_identity_matrix()` method with identity matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is an identity matrix. - `expected` : bool - The expected output of the function. - """ - assert is_identity_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (unitary_group.rvs(2), True), - (unitary_group.rvs(3), True), - (unitary_group.rvs(4), True), - (unitary_group.rvs(5), True), - (np.random.rand(2, 2), False), - (np.random.rand(3, 3), False), - (np.random.rand(3, 4), False), - (np.random.rand(5, 2), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_unitary_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_unitary_matrix()` method. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a unitary matrix. - `expected` : bool - The expected output of the function. - """ - assert is_unitary_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.array([ - [1, 2 + 1j, 3], - [2 - 1j, 4, 5 + 2j], - [3, 5 - 2j, 6] - ]), True), - (np.array([ - [1, 2 + 1j], - [2 - 1j, 3] - ]), True), - (np.array([ - [1, 0], - [0, 1] - ]), True), - (np.array([ - [1, 2 + 1j, 3], - [2 + 1j, 4, 5 + 2j], - [3, 5 - 2j, 6] - ]), False), - (np.array([ - [1, 2 + 1j], - [2 + 1j, 3] - ]), False), - (np.array([ - [1, 2], - [3, 4] - ]), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_hermitian_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_hermitian_matrix()` method with Hermitian matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a Hermitian matrix. - `expected` : bool - The expected output of the function. - """ - assert is_hermitian_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.array([ - [1, 0], - [0, 1] - ]), True), - (np.array([ - [1, 0], - [0, 0] - ]), True), - (np.array([ - [1, 2], - [2, 3] - ]), False), - (np.array([ - [1, 2], - [3, 4] - ]), False), - (np.array([ - [1, 0], - [0, -1] - ]), False), - (np.array([ - [1, 2], - [2, 1] - ]), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_positive_semidefinite_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `.is_positive_semidefinite_matrix()` method with positive semidefinite matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is a positive semidefinite matrix. - `expected` : bool - The expected output of the function. - """ - assert is_positive_semidefinite_matrix(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.array([ - [0.56078693+0.13052803j, -0.31583062-0.08879493j], - [0.7123732 +0.2419316j, 0.39227097-0.22401521j], - [0.30203025+0.10607406j, -0.38000351+0.7374988j] - ], dtype=np.complex128), True), - (np.array([ - [0.08849653+0.24435482j], - [-0.72166734+0.64160373j] - ], dtype=np.complex128), True), - (np.array([ - [0.02572621+0.08711405j, -0.84637795+0.52477964j], - [0.86175428-0.4991281j, -0.06470557-0.06374861j] - ]), True), - (np.array([ - [1, 1], - [1, 1] - ], dtype=np.complex128), False), - (np.random.rand(3, 3), False), - (np.random.rand(1, 2), False), - (np.array([1, 0]), False), -]) -def test_is_isometry( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `is_isometry` function with various matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is an isometry. - `expected` : bool - The expected output of the function. - """ - assert is_isometry(array) is expected - -@pytest.mark.parametrize("array, expected", [ - (np.array([ - [0.55282636+0.j, 0.19339888+0.1369917j], - [0.19339888-0.1369917j, 0.44717364+0.j] - ], dtype=np.complex128), True), - (np.array([ - [0.19834677+0.j, 0.21077084+0.08851485j, 0.07369894-0.03780167j], - [0.21077084-0.08851485j, 0.5556912 +0.j, 0.09721694-0.13294078j], - [0.07369894+0.03780167j, 0.09721694+0.13294078j, 0.24596203+0.j] - ], dtype=np.complex128), True), - (np.array([ - [1, 2 + 1j], - [2 + 1j, 3] - ]), False), - (np.array([ - [1, 2], - [3, 4] - ]), False), - (np.random.rand(2, 3, 3), False) -]) -def test_is_density_matrix( - array: NDArray[np.complex128], - expected: bool - ) -> None: - """ Test the `is_density_matrix` function with various matrices. - - Parameters - ---------- - `array` : NDArray[np.complex128] - The input array to check if it is an isometry. - `expected` : bool - The expected out of the function. - """ - assert is_density_matrix(array) is expected \ No newline at end of file + @pytest.mark.parametrize("array, system_size, expected", [ + (np.array([1, 0]), 2, True), + (np.array([0, 1]), 2, True), + (np.array([1, 0, 0]), 3, True), + (np.array([1, 2]), 2, False), + (np.array([1, 2, 3]), 3, False), + (np.array([1, 0, 0, 0]), 2, True), + (np.array([[0.5], [0.5], [0.5], [0.5]]), 2, True) + ]) + def test_is_statevector( + self, + array: NDArray[np.complex128], + system_size: int, + expected: bool + ) -> None: + """ Test the `is_statevector()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a statevector. + `system_size` : int + The size of the system. If it's 2, then it's a qubit system. + `expected` : bool + The expected output of the function. + """ + assert is_statevector(array, system_size) is expected + + def test_is_statevector_invalid_system_size(self) -> None: + """ Test the `is_statevector()` function with invalid system size. + """ + with pytest.raises(ValueError): + is_statevector(np.array([1, 0]), system_size=0) + + @pytest.mark.parametrize("array, expected", [ + (np.random.rand(2, 2), True), + (np.random.rand(3, 3), True), + (np.random.rand(4, 4), True), + (np.random.rand(5, 5), True), + (np.random.rand(2, 1), False), + (np.random.rand(1, 3), False), + (np.random.rand(4, 12), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_square_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_square_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a square matrix. + `expected` : bool + The expected output of the function. + """ + assert is_square_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [1, 0], + [0, 1] + ]), True), + (np.array([ + [0, 1], + [1, 0] + ]), True), + (np.array([ + [1, 2], + [3, 4] + ]), False) + ]) + def test_is_orthogonal_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_orthogonal_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is an orthogonal matrix. + `expected` : bool + The expected output of the function. + """ + assert is_orthogonal_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([1, 2, 3]), True), + (np.array([1+1j, 2, 3]), False) + ]) + def test_is_real_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_real_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a real matrix. + `expected` : bool + The expected output of the function. + """ + assert is_real_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [1, 0], + [0, 1] + ]), True), + (np.array([ + [0, -1], + [1, 0] + ]), True), + (np.array([ + [1, 1], + [1, 1] + ]), False) + ]) + def test_is_special_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_special_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a special matrix. + `expected` : bool + The expected output of the function. + """ + assert is_special_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [-0.24665563, 0.52914357, -0.3101837, 0.7503027], + [-0.33006535, 0.74061563, 0.27071591, -0.51890099], + [-0.6150982, -0.31194876, 0.66273065, 0.2917709], + [-0.6722143, -0.27236654, -0.62552942, -0.28750192] + ]), True), + (np.array([ + [0.0181244, 0.8330495 , 0.55219219, -0.02799671], + [0.8772641, -0.06010757, 0.03781642, -0.47472592], + [0.1060249, 0.5481764 , -0.82739822, 0.0606098], + [0.46780116, -0.04379778, 0.09521496, 0.87759782] + ]), True), + (np.array([ + [1, 2, 3], + [3, 4, 5] + ]), False), + (np.array([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ]), False) + ]) + def test_is_special_orthogonal_matrix( + self, + array: NDArray[np.float64], + expected: bool + ) -> None: + """ Test the `is_special_orthogonal_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.float64] + The input array to check if it is a special orthogonal matrix. + `expected` : bool + The expected output of the function. + """ + assert is_special_orthogonal_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [0.38422631+2.83471478e-01j, 0.25943494+7.65221683e-02j, 0.56281274+3.91037228e-02j, 0.54010984-2.98070487e-01j], + [0.34951286-5.51982645e-01j, -0.1755857 +1.77050098e-01j, 0.38094516+5.26882445e-01j, -0.28000149+9.92657401e-02j], + [0.01016025+4.69180869e-01j, 0.05238197-4.75213843e-01j, 0.44501419+5.84109815e-03j, -0.54594179+2.34669613e-01j], + [0.17806077+3.05336581e-01j, 0.1054005 +7.90556427e-01j, -0.00897634-2.46649688e-01j, -0.42196931+6.80411917e-04j] + ]), True), + (np.array([ + [0.05270704-0.35904018j, -0.54492897-0.41275395j, -0.14347842-0.07950459j, -0.23602791-0.56425393j], + [0.19398055+0.62964354j, -0.18457864+0.15122235j, -0.61785577-0.32821084j, 0.04745637-0.13138828j], + [0.64639778+0.04023874j, -0.3498751 +0.00456368j, 0.43706868-0.33284833j, -0.14751464+0.36679658j], + [0.12091386-0.01277784j, 0.43253418+0.407713j, 0.26579003-0.33341187j, -0.35797256-0.56740522j] + ]), False) + ]) + def test_is_special_unitary_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_special_unitary_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a special unitary matrix. + `expected` : bool + The expected output of the function. + """ + assert is_special_unitary_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.diag([1, 2, 3]), True), + (np.diag([4, 5, 6, 7]), True), + (np.diag([8, 9]), True), + (np.array([ + [1, 2, 0], + [0, 3, 4], + [0, 0, 5] + ]), False), + (np.array([ + [1, 0, 0], + [2, 3, 0], + [0, 0, 4] + ]), False), + (np.array([ + [1, 0], + [1, 1] + ]), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_diagonal_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_diagonal_matrix()` function with diagonal matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a diagonal matrix. + `expected` : bool + The expected output of the function. + """ + assert is_diagonal_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [1, 2, 3], + [2, 4, 5], + [3, 5, 6] + ]), True), + (np.array([ + [1, 2, 3, 4], + [2, 5, 6, 7], + [3, 6, 8, 9], + [4, 7, 9, 10] + ]), True), + (np.array([ + [1, 2], + [2, 3] + ]), True), + (np.array([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ]), False), + (np.array([ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16] + ]), False), + (np.array([ + [1, 2], + [3, 4] + ]), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_symmetric_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_symmetric_matrix()` function with symmetric matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a symmetric matrix. + `expected` : bool + The expected output of the function. + """ + assert is_symmetric_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.eye(2), True), + (np.eye(3), True), + (np.eye(4), True), + (np.eye(5), True), + (np.random.rand(2, 2), False), + (np.random.rand(3, 3), False), + (np.random.rand(4, 4), False), + (np.random.rand(3, 4), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_identity_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_identity_matrix()` function with identity matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is an identity matrix. + `expected` : bool + The expected output of the function. + """ + assert is_identity_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (unitary_group.rvs(2), True), + (unitary_group.rvs(3), True), + (unitary_group.rvs(4), True), + (unitary_group.rvs(5), True), + (np.random.rand(2, 2), False), + (np.random.rand(3, 3), False), + (np.random.rand(3, 4), False), + (np.random.rand(5, 2), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_unitary_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_unitary_matrix()` function. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a unitary matrix. + `expected` : bool + The expected output of the function. + """ + assert is_unitary_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [1, 2 + 1j, 3], + [2 - 1j, 4, 5 + 2j], + [3, 5 - 2j, 6] + ]), True), + (np.array([ + [1, 2 + 1j], + [2 - 1j, 3] + ]), True), + (np.array([ + [1, 0], + [0, 1] + ]), True), + (np.array([ + [1, 2 + 1j, 3], + [2 + 1j, 4, 5 + 2j], + [3, 5 - 2j, 6] + ]), False), + (np.array([ + [1, 2 + 1j], + [2 + 1j, 3] + ]), False), + (np.array([ + [1, 2], + [3, 4] + ]), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_hermitian_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_hermitian_matrix()` function with Hermitian matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a Hermitian matrix. + `expected` : bool + The expected output of the function. + """ + assert is_hermitian_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [1, 0], + [0, 1] + ]), True), + (np.array([ + [1, 0], + [0, 0] + ]), True), + (np.array([ + [1, 2], + [2, 3] + ]), False), + (np.array([ + [1, 2], + [3, 4] + ]), False), + (np.array([ + [1, 0], + [0, -1] + ]), False), + (np.array([ + [1, 2], + [2, 1] + ]), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_positive_semidefinite_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_positive_semidefinite_matrix()` function with + positive semidefinite matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is a positive semidefinite matrix. + `expected` : bool + The expected output of the function. + """ + assert is_positive_semidefinite_matrix(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [0.56078693+0.13052803j, -0.31583062-0.08879493j], + [0.7123732 +0.2419316j, 0.39227097-0.22401521j], + [0.30203025+0.10607406j, -0.38000351+0.7374988j] + ], dtype=np.complex128), True), + (np.array([ + [0.08849653+0.24435482j], + [-0.72166734+0.64160373j] + ], dtype=np.complex128), True), + (np.array([ + [0.02572621+0.08711405j, -0.84637795+0.52477964j], + [0.86175428-0.4991281j, -0.06470557-0.06374861j] + ]), True), + (np.array([ + [1, 1], + [1, 1] + ], dtype=np.complex128), False), + (np.random.rand(3, 3), False), + (np.random.rand(1, 2), False), + (np.array([1, 0]), False), + ]) + def test_is_isometry( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_isometry` function with various matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is an isometry. + `expected` : bool + The expected output of the function. + """ + assert is_isometry(array) is expected + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [0.55282636+0.j, 0.19339888+0.1369917j], + [0.19339888-0.1369917j, 0.44717364+0.j] + ], dtype=np.complex128), True), + (np.array([ + [0.19834677+0.j, 0.21077084+0.08851485j, 0.07369894-0.03780167j], + [0.21077084-0.08851485j, 0.5556912 +0.j, 0.09721694-0.13294078j], + [0.07369894+0.03780167j, 0.09721694+0.13294078j, 0.24596203+0.j] + ], dtype=np.complex128), True), + (np.array([ + [1, 2 + 1j], + [2 + 1j, 3] + ]), False), + (np.array([ + [1, 2], + [3, 4] + ]), False), + (np.random.rand(2, 3, 3), False) + ]) + def test_is_density_matrix( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_density_matrix` function with various matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is an isometry. + `expected` : bool + The expected out of the function. + """ + assert is_density_matrix(array) is expected + + def test_is_product_matrix(self) -> None: + """ Test the `is_product_matrix` function. + """ + u1 = unitary_group.rvs(2) + u2 = unitary_group.rvs(2) + u3 = np.kron(u1, u2).astype(complex) + assert is_product_matrix(u3) is True + cx = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0] + ], dtype=complex) + assert is_product_matrix(cx) is False + + def test_is_locally_equivalent(self) -> None: + """ Test the `is_locally_equivalent` function. + """ + cx = np.array([ + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 0, 0] + ]) + cz = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, -1] + ], dtype=complex) + assert is_locally_equivalent(cx, cz) is True + + swap = np.array([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1] + ], dtype=complex) + assert is_locally_equivalent(cx, swap) is False + + @pytest.mark.parametrize("array, expected", [ + (np.array([ + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 0, 0] + ], dtype=complex), True), + (np.array([ + [1, 0, 0, 0], + [0, 0, 0, -1j], + [0, 0, 1, 0], + [0, 1j, 0, 0] + ], dtype=complex), True), + (np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, -1] + ], dtype=complex), True), + (np.array([ + [1, 0, 0, 0], + [0, 0, 1j, 0], + [0, 1j, 0, 0], + [0, 0, 0, 1] + ], dtype=complex), True), + (np.array([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1] + ], dtype=complex), False), + (np.array([ + [1, 0, 0, 0], + [0, 0.5+0.5j, 0, 0.5-0.5j], + [0, 0, 1, 0], + [0, 0.5-0.5j, 0, 0.5+0.5j] + ], dtype=complex), False) + ]) + def test_is_supercontrolled( + self, + array: NDArray[np.complex128], + expected: bool + ) -> None: + """ Test the `is_supercontrolled` function with various matrices. + + Parameters + ---------- + `array` : NDArray[np.complex128] + The input array to check if it is supercontrolled. + `expected` : bool + The expected output of the function. + """ + assert is_supercontrolled(array) is expected \ No newline at end of file diff --git a/tests/primitives/__init__.py b/tests/primitives/__init__.py index 78b2e5e..f500ef0 100644 --- a/tests/primitives/__init__.py +++ b/tests/primitives/__init__.py @@ -13,11 +13,9 @@ # limitations under the License. __all__ = [ - "TestBra", - "TestKet", + "TestStatevector", "TestOperator" ] -from tests.primitives.test_bra import TestBra -from tests.primitives.test_ket import TestKet -# from tests.primitives.test_operator import TestOperator \ No newline at end of file +from tests.primitives.test_statevector import TestStatevector +from tests.primitives.test_operator import TestOperator \ No newline at end of file diff --git a/tests/primitives/test_bra.py b/tests/primitives/test_bra.py deleted file mode 100644 index f3f0ef5..0000000 --- a/tests/primitives/test_bra.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright 2023-2025 Qualition Computing LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://github.com/Qualition/quick/blob/main/LICENSE -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -__all__ = ["TestBra"] - -import numpy as np -from numpy.testing import assert_allclose -import pytest - -from quick.primitives import Bra, Ket, Operator - - -class TestBra: - """ `tests.primitives.test_bra.TestBra` is the tester class for `quick.primitives.Bra`. - """ - def test_init(self) -> None: - """ Test the initialization of the `quick.primitives.Bra` class. - """ - bra = Bra(np.array([1, 0, 0, 0])) - assert_allclose(bra.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - def test_from_scalar_fail(self) -> None: - """ Test the failure of defining a `quick.primitives.Bra` object from a scalar. - """ - with pytest.raises(AttributeError): - Bra(1) # type: ignore - - def test_from_operator_fail(self) -> None: - """ Test the failure of defining a `quick.primitives.Bra` object from an operator. - """ - with pytest.raises(ValueError): - Bra(np.eye(4, dtype=complex)) - - def test_check_normalization(self) -> None: - """ Test the normalization of the `quick.primitives.Bra` object. - """ - data = np.array([1, 0, 0, 0]) - assert Bra.check_normalization(data) - - def test_check_normalization_fail(self) -> None: - """ Test the failure of the normalization of the `quick.primitives.Bra` object. - """ - data = np.array([1, 1, 1, 1]) - assert not Bra.check_normalization(data) - - def test_normalize(self) -> None: - """ Test the normalization of the `quick.primitives.Bra` object. - """ - data = np.array([1, 0, 0, 1]) - assert_allclose(Bra.normalize_data(data, np.linalg.norm(data)), np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) - - bra = Bra(data) - bra.normalize() - assert_allclose(bra.data, np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) - - # Re-normalize the already normalized to cover the case where if normalized we simply return - bra.normalize() - assert_allclose(bra.data, np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) - - def test_check_padding(self) -> None: - """ Test the padding of the `quick.primitives.Bra` object. - """ - data = np.array([1, 0, 0, 0]) - assert Bra.check_padding(data) - - def test_check_padding_fail(self) -> None: - """ Test the failure of the padding of the `quick.primitives.Bra` object. - """ - data = np.array([1, 0, 0]) - assert not Bra.check_padding(data) - - def test_pad(self) -> None: - """ Test the padding of the `quick.primitives.Bra` object. - """ - data = np.array([1, 0, 0]) - padded_data, _ = Bra.pad_data(data, 4) - assert_allclose(padded_data, np.array([1, 0, 0, 0])) - - bra = Bra(data) - bra.pad() - assert_allclose(bra.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - # Re-pad the already padded to cover the case where if padded we simply return - bra.pad() - assert_allclose(bra.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - def test_to_ket(self) -> None: - """ Test the conversion of the `quick.primitives.Bra` object to a `quick.primitives.Ket` object. - """ - bra = Bra(np.array([1+0j, 0+0j, 0+0j, 0+0j])) - ket = bra.to_ket() - assert_allclose(ket.data, np.array([ - [1-0j], - [0-0j], - [0-0j], - [0-0j] - ])) - - def test_change_indexing(self) -> None: - """ Test the change of indexing of the `quick.primitives.Bra` object. - """ - bra = Bra(np.array([1, 0, 0, 0])) - bra.change_indexing("snake") - assert_allclose(bra.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - bra = Bra(np.array([1, 0, 0, 0, - 1, 0, 0, 0])) - bra.change_indexing("snake") - assert_allclose(bra.data, np.array([ - (1+0j)/np.sqrt(2), 0+0j, 0+0j, 0+0j, - 0+0j, 0+0j, 0+0j, (1+0j)/np.sqrt(2) - ])) - - def test_change_indexing_fail(self) -> None: - """ Test the failure of the change of indexing of the `quick.primitives.Bra` object. - """ - bra = Bra(np.array([1, 0, 0, 0])) - with pytest.raises(ValueError): - bra.change_indexing("invalid") # type: ignore - - def test_check_mul(self) -> None: - """ Test the multiplication of the `quick.primitives.Bra` object. - """ - bra = Bra(np.array([1, 0, 0, 0])) - bra._check__mul__(1) - - ket = Ket(np.array([1, 0, 0, 0])) - bra._check__mul__(ket) - - operator = Operator(np.eye(4, dtype=complex)) - bra._check__mul__(operator) - - def test_check_mul_fail(self) -> None: - """ Test the failure of the multiplication of the `quick.primitives.Bra` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - bra = Bra(np.array([1, 0])) - with pytest.raises(ValueError): - bra._check__mul__(ket) - - operator = Operator(np.eye(4, dtype=complex)) - with pytest.raises(ValueError): - bra._check__mul__(operator) - - with pytest.raises(NotImplementedError): - bra._check__mul__("invalid") - - def test_eq(self) -> None: - """ Test the equality of the `quick.primitives.Bra` object. - """ - bra1 = Bra(np.array([1, 0, 0, 0])) - bra2 = Bra(np.array([1, 0, 0, 0])) - assert bra1 == bra2 - - def test_eq_fail(self) -> None: - """ Test the failure of the equality of the `quick.primitives.Bra` object. - """ - bra1 = Bra(np.array([1, 0, 0, 0])) - bra2 = Bra(np.array([0, 1, 0, 0])) - assert bra1 != bra2 - - with pytest.raises(NotImplementedError): - bra1 == "invalid" # type: ignore - - def test_len(self) -> None: - """ Test the length of the `quick.primitives.Bra` object. - """ - bra = Bra(np.array([1, 0, 0, 0])) - assert len(bra) == 4 - - def test_add(self) -> None: - """ Test the addition of the `quick.primitives.Bra` objects. - """ - bra1 = Bra(np.array([1, 0, 0, 0])) - bra2 = Bra(np.array([0, 1, 0, 0])) - assert_allclose((bra1 + bra2).data, np.array([(1+0j)/np.sqrt(2), (1+0j)/np.sqrt(2), 0+0j, 0+0j])) - - def test_add_fail(self) -> None: - """ Test the failure of the addition of the `quick.primitives.Bra` objects. - """ - bra1 = Bra(np.array([1, 0, 0, 0])) - bra2 = Bra(np.array([1, 0])) - - with pytest.raises(ValueError): - bra1 + bra2 # type: ignore - - with pytest.raises(NotImplementedError): - bra1 + "invalid" # type: ignore - - def test_mul_scalar(self) -> None: - """ Test the multiplication of the `quick.primitives.Bra` object with a scalar. - """ - bra = Bra(np.array([1, 0, 0, 0])) - assert_allclose((bra * 2).data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - def test_mul_bra(self) -> None: - """ Test the multiplication of the `quick.primitives.Bra` object with a `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - bra = Bra(np.array([1, 0, 0, 0])) - assert bra * ket == 1.0 + 0j - - def test_mul_fail(self) -> None: - """ Test the failure of the multiplication of the `quick.primitives.Bra` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - bra = Bra(np.array([1, 0])) - with pytest.raises(ValueError): - bra * ket # type: ignore - - with pytest.raises(NotImplementedError): - bra * "invalid" # type: ignore - - def test_rmul_scalar(self) -> None: - """ Test the multiplication of a `quick.primitives.Bra` object with a scalar. - """ - bra = Bra(np.array([1, 0, 0, 0])) - assert_allclose((2 * bra).data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - def test_str(self) -> None: - """ Test the string representation of the `quick.primitives.Bra` object. - """ - bra = Bra(np.array([1, 0, 0, 0])) - assert str(bra) == "⟨Ψ|" - - bra = Bra(np.array([1, 0, 0, 0]), label="psi") - assert str(bra) == "⟨psi|" - - def test_repr(self) -> None: - """ Test the string representation of the `quick.primitives.Bra` object. - """ - bra = Bra(np.array([1, 0, 0, 0])) - assert repr(bra) == "Bra(data=[1.+0.j 0.+0.j 0.+0.j 0.+0.j], label=Ψ)" \ No newline at end of file diff --git a/tests/primitives/test_ket.py b/tests/primitives/test_ket.py deleted file mode 100644 index 79c8668..0000000 --- a/tests/primitives/test_ket.py +++ /dev/null @@ -1,279 +0,0 @@ -# Copyright 2023-2025 Qualition Computing LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://github.com/Qualition/quick/blob/main/LICENSE -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -__all__ = ["TestKet"] - -import numpy as np -from numpy.testing import assert_allclose -import pytest - -from quick.primitives import Bra, Ket - - -class TestKet: - """ `tests.primitives.test_ket.TestKet` is the tester class for `quick.primitives.Ket`. - """ - def test_init(self) -> None: - """ Test the initialization of the `quick.primitives.Ket` class. - """ - ket = Ket(np.array([1, 0, 0, 0])) - assert_allclose(ket.data, np.array([ - [1+0j], - [0+0j], - [0+0j], - [0+0j] - ])) - - def test_from_scalar_fail(self) -> None: - """ Test the failure of defining a `quick.primitives.Ket` object from a scalar. - """ - with pytest.raises(AttributeError): - Ket(1) # type: ignore - - def test_from_operator_fail(self) -> None: - """ Test the failure of defining a `quick.primitives.Ket` object from an operator. - """ - with pytest.raises(ValueError): - Ket(np.eye(4, dtype=complex)) - - def test_check_normalization(self) -> None: - """ Test the normalization of the `quick.primitives.Ket` object. - """ - data = np.array([1, 0, 0, 0]) - assert Ket.check_normalization(data) - - def test_check_normalization_fail(self) -> None: - """ Test the failure of the normalization of the `quick.primitives.Ket` object. - """ - data = np.array([1, 1, 1, 1]) - assert not Ket.check_normalization(data) - - def test_normalize(self) -> None: - """ Test the normalization of the `quick.primitives.Ket` object. - """ - data = np.array([1, 0, 0, 1]) - assert_allclose(Ket.normalize_data(data, np.linalg.norm(data)), np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) - - ket = Ket(data) - ket.normalize() - assert_allclose(ket.data.flatten(), np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) - - # Re-normalize the already normalized to cover the case where if normalized we simply return - ket.normalize() - assert_allclose(ket.data.flatten(), np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) - - def test_check_padding(self) -> None: - """ Test the padding of the `quick.primitives.Ket` object. - """ - data = np.array([1, 0, 0, 0]) - assert Ket.check_padding(data) - - def test_check_padding_fail(self) -> None: - """ Test the failure of the padding of the `quick.primitives.Ket` object. - """ - data = np.array([1, 0, 0]) - assert not Ket.check_padding(data) - - def test_pad(self) -> None: - """ Test the padding of the `quick.primitives.Ket` object. - """ - data = np.array([1, 0, 0]) - padded_data, _ = Ket.pad_data(data, 4) - assert_allclose(padded_data, np.array([ - [1], - [0], - [0], - [0] - ])) - - ket = Ket(data) - ket.pad() - assert_allclose(ket.data.flatten(), np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - # Re-pad the already padded to cover the case where if padded we simply return - ket.pad() - assert_allclose(ket.data.flatten(), np.array([1+0j, 0+0j, 0+0j, 0+0j])) - - def test_to_bra(self) -> None: - """ Test the conversion of the `quick.primitives.Ket` object to a `quick.primitives.Bra` object. - """ - ket = Ket(np.array([1+0j, 0+0j, 0+0j, 0+0j])) - bra = ket.to_bra() - assert_allclose(bra.data, np.array([1-0j, 0-0j, 0-0j, 0-0j])) - - def test_change_indexing(self) -> None: - """ Test the change of indexing of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - ket.change_indexing("snake") - assert_allclose(ket.data, np.array([ - [1+0j], - [0+0j], - [0+0j], - [0+0j] - ])) - - ket = Ket(np.array([ - 1, 0, 0, 0, - 1, 0, 0, 0 - ])) - ket.change_indexing("snake") - assert_allclose(ket.data, np.array([ - [(1+0j)/np.sqrt(2)], - [0+0j], - [0+0j], - [0+0j], - [0+0j], - [0+0j], - [0+0j], - [(1+0j)/np.sqrt(2)] - ])) - - def test_change_indexing_fail(self) -> None: - """ Test the failure of the change of indexing of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - with pytest.raises(ValueError): - ket.change_indexing("invalid") # type: ignore - - def test_check_mul(self) -> None: - """ Test the multiplication of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - ket._check__mul__(1) - - bra = Bra(np.array([1, 0, 0, 0])) - ket._check__mul__(bra) - - def test_check_mul_fail(self) -> None: - """ Test the failure of the multiplication of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - bra = Bra(np.array([1, 0])) - with pytest.raises(ValueError): - ket._check__mul__(bra) - - with pytest.raises(NotImplementedError): - ket._check__mul__("invalid") - - def test_eq(self) -> None: - """ Test the equality of the `quick.primitives.Ket` object. - """ - ket1 = Ket(np.array([1, 0, 0, 0])) - ket2 = Ket(np.array([1, 0, 0, 0])) - assert ket1 == ket2 - - def test_eq_fail(self) -> None: - """ Test the failure of the equality of the `quick.primitives.Ket` object. - """ - ket1 = Ket(np.array([1, 0, 0, 0])) - ket2 = Ket(np.array([1, 0, 0, 1])) - assert ket1 != ket2 - - with pytest.raises(NotImplementedError): - ket1 == "invalid" # type: ignore - - def test_len(self) -> None: - """ Test the length of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - assert len(ket) == 4 - - def test_add(self) -> None: - """ Test the addition of the `quick.primitives.Ket` object. - """ - ket1 = Ket(np.array([1, 0, 0, 0])) - ket2 = Ket(np.array([0, 1, 0, 0])) - assert_allclose((ket1 + ket2).data, np.array([ - [(1+0j)/np.sqrt(2)], - [(1+0j)/np.sqrt(2)], - [0+0j], - [0+0j] - ])) - - def test_add_fail(self) -> None: - """ Test the failure of the addition of the `quick.primitives.Ket` objects. - """ - ket1 = Ket(np.array([1, 0, 0, 0])) - ket2 = Ket(np.array([1, 0])) - - with pytest.raises(ValueError): - ket1 + ket2 # type: ignore - - with pytest.raises(NotImplementedError): - ket1 + "invalid" # type: ignore - - def test_mul_scalar(self) -> None: - """ Test the multiplication of the `quick.primitives.Ket` object with a scalar. - """ - ket = Ket(np.array([1, 0, 0, 0])) - assert_allclose((ket * 2).data, np.array([ - [1+0j], - [0+0j], - [0+0j], - [0+0j] - ])) - - def test_mul_bra(self) -> None: - """ Test the multiplication of the `quick.primitives.Ket` object with a `quick.primitives.Bra` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - bra = Bra(np.array([1, 0, 0, 0])) - # NOTE: Turned off this test until the bra-ket interface is fixed. - # assert_allclose((ket * bra).data, np.array([[1+0j, 0+0j, 0+0j, 0+0j], - # [0+0j, 0+0j, 0+0j, 0+0j], - # [0+0j, 0+0j, 0+0j, 0+0j], - # [0+0j, 0+0j, 0+0j, 0+0j]])) - - def test_mul_fail(self) -> None: - """ Test the failure of the multiplication of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - bra = Bra(np.array([1, 0])) - with pytest.raises(ValueError): - ket * bra # type: ignore - - with pytest.raises(NotImplementedError): - ket * "invalid" # type: ignore - - def test_rmul_scalar(self) -> None: - """ Test the multiplication of a `quick.primitives.Ket` object with a scalar. - """ - ket = Ket(np.array([1, 0, 0, 0])) - assert_allclose((2 * ket).data, np.array([ - [1+0j], - [0+0j], - [0+0j], - [0+0j] - ])) - - def test_str(self) -> None: - """ Test the string representation of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - assert str(ket) == "|Ψ⟩" - - ket = Ket(np.array([1, 0, 0, 0]), label="psi") - assert str(ket) == "|psi⟩" - - def test_repr(self) -> None: - """ Test the string representation of the `quick.primitives.Ket` object. - """ - ket = Ket(np.array([1, 0, 0, 0])) - assert repr(ket) == ("Ket(data=[[1.+0.j]\n" - " [0.+0.j]\n" - " [0.+0.j]\n" - " [0.+0.j]], label=Ψ)") \ No newline at end of file diff --git a/tests/primitives/test_operator.py b/tests/primitives/test_operator.py index 6e2a945..b259a2e 100644 --- a/tests/primitives/test_operator.py +++ b/tests/primitives/test_operator.py @@ -10,4 +10,259 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. + +from __future__ import annotations + +__all__ = ["TestOperator"] + +import numpy as np +from numpy.testing import assert_almost_equal +import pytest +from scipy.stats import unitary_group + +from quick.primitives import Statevector, Operator + + +class TestOperator: + """ `tests.primitives.test_operator.TestOperator` is the tester class + for `quick.primitives.Operator`. + """ + def test_init(self) -> None: + """ Test the initialization of the `quick.primitives.Operator` class. + """ + operator = Operator( + np.array([ + [1, 0], + [0, 1] + ]), label="A" + ) + assert_almost_equal(operator.data, np.array([[1+0j, 0+0j], [0+0j, 1+0j]])) + assert operator.shape == (2, 2) + assert operator.num_qubits == 1 + assert operator.label == "A" + + def test_from_matrix(self) -> None: + """ Test the initialization of the `quick.primitives.Operator` class from a matrix. + """ + from quick.predicates import is_unitary_matrix + + matrix = np.arange(4).reshape(2, 2).astype(complex) + op = Operator.from_matrix(matrix) + assert is_unitary_matrix(op.data) + + def test_conj(self) -> None: + """ Test the conjugate of the `quick.primitives.Operator` class. + """ + unitary = np.array(unitary_group.rvs(8)).astype(complex) + operator = Operator(unitary) + conjugate_operator = operator.conj() + assert_almost_equal(conjugate_operator.data, unitary.conj()) + + def test_T(self) -> None: + """ Test the transpose of `quick.primitives.Operator` class. + """ + unitary = np.array(unitary_group.rvs(8)).astype(complex) + operator = Operator(unitary) + transpose_operator = operator.T() + assert_almost_equal(transpose_operator.data, unitary.T) + + def test_adjoint(self) -> None: + """ Test the adjoint of the `quick.primitives.Operator` class. + """ + unitary = np.array(unitary_group.rvs(8)).astype(complex) + operator = Operator(unitary) + adjoint_operator = operator.adjoint() + assert_almost_equal(adjoint_operator.data, unitary.conj().T) + + def test_from_scalar_fail(self) -> None: + """ Test the failure of defining a `quick.primitives.Operator` object from a scalar. + """ + with pytest.raises(ValueError): + Operator(1) # type: ignore + + def test_from_statevector_fail(self) -> None: + """ Test the failure of defining a `quick.primitives.Operator` object from a statevector. + """ + with pytest.raises(ValueError): + Operator(np.array([1, 0, 0, 0])) + + def test_reverse_bits(self) -> None: + """ Test the MSB to LSB (vice versa) conversion of the `quick.primitives.Operator` object. + """ + cx_msb = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0] + ]) + cx_lsb = np.array([ + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 0, 0] + ]) + operator = Operator(cx_msb) + operator.reverse_bits() + checker_operator = Operator(cx_lsb) + assert_almost_equal(operator.data, checker_operator.data) + + operator.reverse_bits() + checker_operator = Operator(cx_msb) + assert_almost_equal(operator.data, checker_operator.data) + + def test_contract(self) -> None: + """ Test the application of operators to `quick.primitives.Operator` objects. + """ + from quick.circuit import QiskitCircuit + + uni1 = np.array(unitary_group.rvs(2 ** 5)).astype(complex) + uni2 = np.array(unitary_group.rvs(2 ** 3)).astype(complex) + uni3 = np.array(unitary_group.rvs(2 ** 2)).astype(complex) + + op1 = Operator(uni1) + op2 = Operator(uni2) + op3 = Operator(uni3) + + op1.contract(op2, [3, 0, 1]) + op1.contract(op3, [2, 4]) + + checker_circuit = QiskitCircuit(5) + checker_circuit.unitary(uni1, [0, 1, 2, 3, 4]) + checker_circuit.unitary(uni2, [3, 0, 1]) + checker_circuit.unitary(uni3, [2, 4]) + + assert_almost_equal(checker_circuit.get_unitary(), op1.data) + + def test_control(self) -> None: + """ Test the control operation of the `quick.primitives.Operator` class. + """ + from quick.circuit import QiskitCircuit + + unitary = np.array(unitary_group.rvs(8)).astype(complex) + operator = Operator(unitary) + control_operator = operator.control(2) + + op_circuit = QiskitCircuit(3) + op_circuit.unitary(unitary, [0, 1, 2]) + checker_circuit = op_circuit.control(2) + assert_almost_equal(checker_circuit.get_unitary(), control_operator.data) + + def test_array(self) -> None: + """ Test the conversion of the `quick.primitives.Operator` to a NumPy array. + """ + operator = Operator(np.array([[1, 0], [0, 1]])) + assert_almost_equal(np.array(operator), np.array([[1, 0], [0, 1]])) + + def test_check_mul(self) -> None: + """ Test the multiplication of two `quick.primitives.Operator` objects. + """ + op1 = Operator(np.array([[1, 0], [0, 1]])) + op2 = Operator(np.array([[0, 1], [1, 0]])) + state = Statevector(np.array([1, 0])) + op1._check__mul__(op2) + op1._check__mul__(state) + + def test_check_mul_fail(self) -> None: + """ Test the failure of the multiplication of two `quick.primitives.Operator` objects. + """ + op1 = Operator(np.array([[1, 0], [0, 1]])) + with pytest.raises(ValueError): + # Mismatched number of qubits + op1._check__mul__(Statevector(np.array([1, 0, 0, 0]))) + + with pytest.raises(TypeError): + # Incompatible type + op1._check__mul__("not an operator or statevector or scalar") + + def test_eq(self) -> None: + """ Test the equality of two `quick.primitives.Operator` objects. + """ + op1 = Operator(np.array([[1, 0], [0, 1]])) + op2 = Operator(np.array([[1, 0], [0, 1]])) + + assert op1 == op2 + + def test_eq_fail(self) -> None: + """ Test the failure of the equality of two `quick.primitives.Operator` objects. + """ + op1 = Operator(np.array([[1, 0], [0, 1]])) + op2 = Operator(np.array([[0, 1], [1, 0]])) + assert op1 != op2 + + with pytest.raises(TypeError): + # Incompatible type + op1 == "not an operator" # type: ignore + + def test_mul_statevector(self) -> None: + """ Test the multiplication of a `quick.primitives.Operator` with a `quick.primitives.Statevector`. + """ + operator = Operator(np.array([[1, 0], [0, 1]])) + state = Statevector(np.array([1, 0])) + result = operator * state + assert_almost_equal(result.data, np.array([1, 0])) + + def test_mul_operator(self) -> None: + """ Test the multiplication of two `quick.primitives.Operator` objects. + """ + op1 = Operator(np.array([[1, 0], [0, 1]])) + op2 = Operator(np.array([[0, 1], [1, 0]])) + result = op1 * op2 + assert_almost_equal(result.data, np.array([[0, 1], [1, 0]])) + + def test_mul_fail(self) -> None: + """ Test the failure of the multiplication of a `quick.primitives.Operator` with an incompatible type. + """ + operator = Operator(np.array([[1, 0], [0, 1]])) + + with pytest.raises(ValueError): + # Incompatible dimensions + operator * Statevector(np.array([1, 0, 0, 0])) # type: ignore + + with pytest.raises(TypeError): + # Incompatible type + operator * "not an operator or statevector" # type: ignore + + def test_matmul(self) -> None: + """ Test the tensor product operation of the `quick.primitives.Operator` object. + """ + op1 = Operator(np.array([ + [1, 0], + [0, 1] + ])) + op2 = Operator(np.array([ + [0, 1], + [1, 0] + ])) + + op3 = op1 @ op2 + op3_checker = np.array([ + [0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0] + ]) + + assert_almost_equal(op3.data, op3_checker) + + op4 = op2 @ op1 + op4_checker = np.array([ + [0, 0, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, 0], + [0, 1, 0, 0] + ]) + + assert_almost_equal(op4.data, op4_checker) + + def test_str(self) -> None: + """ Test the string representation of the `quick.primitives.Operator` object. + """ + operator = Operator(np.array([[1, 0], [0, 1]]), label="Identity") + assert str(operator) == "Identity" + + def test_repr(self) -> None: + """ Test the string representation of the `quick.primitives.Operator` object. + """ + operator = Operator(np.array([[1, 0], [0, 1]]), label="Identity") + assert repr(operator) == "Operator(data=[[1 0]\n [0 1]], label=Identity)" \ No newline at end of file diff --git a/tests/primitives/test_statevector.py b/tests/primitives/test_statevector.py new file mode 100644 index 0000000..b13d2f0 --- /dev/null +++ b/tests/primitives/test_statevector.py @@ -0,0 +1,303 @@ +# Copyright 2023-2025 Qualition Computing LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://github.com/Qualition/quick/blob/main/LICENSE +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +__all__ = ["TestStatevector"] + +import numpy as np +from numpy.testing import assert_almost_equal +import pytest +from scipy.stats import unitary_group + +from quick.predicates import is_statevector +from quick.primitives import Statevector, Operator + + +class TestStatevector: + """ `tests.primitives.test_statevector.TestStatevector` is the tester class + for `quick.primitives.Statevector`. + """ + def test_init(self) -> None: + """ Test the initialization of the `quick.primitives.Statevector` class. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert_almost_equal(statevector.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) + assert statevector.label == "Ψ" + assert statevector.tensor_shape == (2, 2) + + def test_from_scalar_fail(self) -> None: + """ Test the failure of defining a `quick.primitives.Statevector` object from a scalar. + """ + with pytest.raises(ValueError): + Statevector(1) # type: ignore + + def test_from_operator_fail(self) -> None: + """ Test the failure of defining a `quick.primitives.Statevector` object from an operator. + """ + with pytest.raises(ValueError): + Statevector(np.eye(4, dtype=complex)) + + def test_normalize(self) -> None: + """ Test the normalization of the `quick.primitives.Statevector` object. + """ + data = np.array([1, 0, 0, 1]) + assert_almost_equal( + Statevector.normalize_data( + data, + np.linalg.norm(data)), np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)] + ) + ) + + statevector = Statevector(data) + statevector.normalize() + assert_almost_equal(statevector.data, np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) + + # Re-normalize the already normalized to cover the case where if normalized we simply return + statevector.normalize() + assert_almost_equal(statevector.data, np.array([(1+0j)/np.sqrt(2), 0+0j, 0+0j, (1+0j)/np.sqrt(2)])) + + def test_check_padding(self) -> None: + """ Test the padding of the `quick.primitives.Statevector` object. + """ + data = np.array([1, 0, 0, 0]) + assert Statevector.check_padding(data) + + def test_check_padding_fail(self) -> None: + """ Test the failure of the padding of the `quick.primitives.Statevector` object. + """ + data = np.array([1, 0, 0]) + assert not Statevector.check_padding(data) + + def test_pad(self) -> None: + """ Test the padding of the `quick.primitives.Statevector` object. + """ + data = np.array([1, 0, 0]) + padded_data = Statevector.pad_data(data, 4) + assert_almost_equal(padded_data, np.array([1, 0, 0, 0])) + + statevector = Statevector(data) + statevector.pad() + assert_almost_equal(statevector.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) + + # Re-pad the already padded to cover the case where if padded we simply return + statevector.pad() + assert_almost_equal(statevector.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) + + def test_to_quantumstate(self) -> None: + """ Test the conversion of the `quick.primitives.Statevector` object to a quantum state. + """ + statevector = Statevector(np.array([1, 2, 3, 4])) + assert is_statevector(statevector.data) + + def test_trace(self) -> None: + """ Test the trace of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 2, 3, 4, 5, 6])) + assert_almost_equal(statevector.trace(), 1) + + def test_partial_trace(self) -> None: + """ Test the partial trace of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 2, 3, 4, 5, 6, 7, 8])) + assert_almost_equal( + statevector.partial_trace([0, 2]), + np.array([ + [0.32352941+0.j, 0.46078431+0.j], + [0.46078431+0.j, 0.67647059+0.j] + ]) + ) + + def test_change_indexing(self) -> None: + """ Test the change of indexing of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + statevector.change_indexing("snake") + assert_almost_equal(statevector.data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) + + statevector = Statevector( + np.array([1, 0, 0, 0, 1, 0, 0, 0]) + ) + statevector.change_indexing("snake") + assert_almost_equal(statevector.data, np.array([ + (1+0j)/np.sqrt(2), 0+0j, 0+0j, 0+0j, + 0+0j, 0+0j, 0+0j, (1+0j)/np.sqrt(2) + ])) + + def test_change_indexing_fail(self) -> None: + """ Test the failure of the change of indexing of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + with pytest.raises(ValueError): + statevector.change_indexing("invalid") # type: ignore + + def test_reverse_bits(self) -> None: + """ Test the MSB to LSB (vice versa) conversion of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 2, 3, 4])) + statevector.reverse_bits() + checker_statevector = Statevector(np.array([1, 3, 2, 4])) + assert_almost_equal(statevector.data, checker_statevector.data) + + statevector.reverse_bits() + checker_statevector = Statevector(np.array([1, 2, 3, 4])) + assert_almost_equal(statevector.data, checker_statevector.data) + + def test_contract(self) -> None: + """ Test the application of operators to `quick.primitives.Operator` objects. + """ + from quick.circuit import QiskitCircuit + + state = np.zeros(2 ** 5) + state[0] = 1 + statevector = Statevector(state.astype(complex)) + uni1 = np.array(unitary_group.rvs(2 ** 5)).astype(complex) + uni2 = np.array(unitary_group.rvs(2 ** 3)).astype(complex) + uni3 = np.array(unitary_group.rvs(2 ** 2)).astype(complex) + + op1 = Operator(uni1) + op2 = Operator(uni2) + op3 = Operator(uni3) + + op1.contract(op2, [3, 0, 1]) + op1.contract(op3, [2, 4]) + statevector.contract(op1, [0, 1, 2, 3, 4]) + + checker_circuit = QiskitCircuit(5) + checker_circuit.unitary(uni1, [0, 1, 2, 3, 4]) + checker_circuit.unitary(uni2, [3, 0, 1]) + checker_circuit.unitary(uni3, [2, 4]) + + assert_almost_equal(checker_circuit.get_statevector(), statevector.data) + + def test_array(self) -> None: + """ Test the conversion of the `quick.primitives.Statevector` to a NumPy array. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert_almost_equal(np.array(statevector), np.array([1, 0, 0, 0])) + + def test_check_mul(self) -> None: + """ Test the multiplication of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + statevector._check__mul__(1) + + def test_check_mul_fail(self) -> None: + """ Test the failure of the multiplication of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + + with pytest.raises(TypeError): + statevector._check__mul__("invalid") + + def test_eq(self) -> None: + """ Test the equality of the `quick.primitives.Statevector` object. + """ + statevector1 = Statevector(np.array([1, 0, 0, 0])) + statevector2 = Statevector(np.array([1, 0, 0, 0])) + assert statevector1 == statevector2 + + def test_eq_fail(self) -> None: + """ Test the failure of the equality of the `quick.primitives.Statevector` object. + """ + statevector1 = Statevector(np.array([1, 0, 0, 0])) + statevector2 = Statevector(np.array([0, 1, 0, 0])) + assert statevector1 != statevector2 + + with pytest.raises(TypeError): + statevector1 == "invalid" # type: ignore + + def test_len(self) -> None: + """ Test the length of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert len(statevector) == 4 + + def test_add(self) -> None: + """ Test the addition of the `quick.primitives.Statevector` objects. + """ + statevector1 = Statevector(np.array([1, 0, 0, 0])) + statevector2 = Statevector(np.array([0, 1, 0, 0])) + assert_almost_equal( + (statevector1 + statevector2).data, + np.array([(1+0j)/np.sqrt(2), (1+0j)/np.sqrt(2), 0+0j, 0+0j]) + ) + + def test_add_fail(self) -> None: + """ Test the failure of the addition of the `quick.primitives.Statevector` objects. + """ + statevector1 = Statevector(np.array([1, 0, 0, 0])) + statevector2 = Statevector(np.array([1, 0])) + + with pytest.raises(ValueError): + statevector1 + statevector2 # type: ignore + + with pytest.raises(TypeError): + statevector1 + "invalid" # type: ignore + + def test_mul_scalar(self) -> None: + """ Test the multiplication of the `quick.primitives.Statevector` object with a scalar. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert_almost_equal((statevector * 2).data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) + + def test_mul_fail(self) -> None: + """ Test the failure of the multiplication of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0])) + + with pytest.raises(TypeError): + statevector * "invalid" # type: ignore + + def test_rmul_scalar(self) -> None: + """ Test the multiplication of a `quick.primitives.Statevector` object with a scalar. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert_almost_equal((2 * statevector).data, np.array([1+0j, 0+0j, 0+0j, 0+0j])) + + def test_matmul(self) -> None: + """ Test the matrix multiplication of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + tensor_state = statevector @ statevector + assert tensor_state.num_qubits == 4 + checker_state = np.zeros(16, dtype=complex) + checker_state[0] = 1 + assert_almost_equal( + tensor_state.data, + checker_state + ) + + def test_matmul_fail(self) -> None: + """ Test the failure of the matrix multiplication of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + + with pytest.raises(TypeError): + statevector @ "invalid" # type: ignore + + def test_str(self) -> None: + """ Test the string representation of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert str(statevector) == "|Ψ⟩" + + statevector = Statevector(np.array([1, 0, 0, 0]), label="psi") + assert str(statevector) == "|psi⟩" + + def test_repr(self) -> None: + """ Test the string representation of the `quick.primitives.Statevector` object. + """ + statevector = Statevector(np.array([1, 0, 0, 0])) + assert repr(statevector) == "Statevector(data=[1.+0.j 0.+0.j 0.+0.j 0.+0.j], label=Ψ)" \ No newline at end of file diff --git a/tests/random/test_random.py b/tests/random/test_random.py index 765cdca..93d70ee 100644 --- a/tests/random/test_random.py +++ b/tests/random/test_random.py @@ -18,11 +18,21 @@ import pytest -from quick.predicates import is_unitary_matrix, is_statevector, is_density_matrix +from quick.predicates import ( + is_unitary_matrix, + is_statevector, + is_density_matrix, + is_orthogonal_matrix, + is_special_orthogonal_matrix, + is_special_unitary_matrix +) from quick.random import ( generate_random_state, generate_random_unitary, - generate_random_density_matrix + generate_random_density_matrix, + generate_random_orthogonal_matrix, + generate_random_special_orthogonal_matrix, + generate_random_special_unitary_matrix ) @@ -35,7 +45,7 @@ def test_generate_random_state( self, num_qubits: int ) -> None: - """ Test the `generate_random_state` function. + """ Test the `generate_random_state()` function. Parameters ---------- @@ -51,7 +61,7 @@ def test_generate_random_unitary( self, num_qubits: int ) -> None: - """ Test the `generate_random_unitary` function. + """ Test the `generate_random_unitary()` function. Parameters ---------- @@ -72,7 +82,7 @@ def test_generate_random_density_matrix( generator: str, rank: int ) -> None: - """ Test the `generate_random_density_matrix` function. + """ Test the `generate_random_density_matrix()` function. Parameters ---------- @@ -94,7 +104,7 @@ def test_generate_random_density_matrix( def test_generate_random_density_matrix_invalid_generator( self ) -> None: - """ Test the `generate_random_density_matrix` function with an + """ Test the `generate_random_density_matrix()` function with an invalid generator. """ with pytest.raises(ValueError): @@ -102,4 +112,52 @@ def test_generate_random_density_matrix_invalid_generator( num_qubits=2, rank=1, generator="invalid-generator" # type: ignore - ) \ No newline at end of file + ) + + @pytest.mark.parametrize("num_qubits", [1, 2, 3, 4, 5]) + def test_generate_random_orthogonal_matrix( + self, + num_qubits: int + ) -> None: + """ Test the `generate_random_orthogonal_matrix()` function. + + Parameters + ---------- + `num_qubits` : int + The number of qubits in the SO matrix. + """ + o_matrix = generate_random_orthogonal_matrix(num_qubits) + + assert is_orthogonal_matrix(o_matrix) + + @pytest.mark.parametrize("num_qubits", [1, 2, 3, 4, 5]) + def test_generate_random_special_orthogonal_matrix( + self, + num_qubits: int + ) -> None: + """ Test the `generate_random_special_orthogonal_matrix()` function. + + Parameters + ---------- + `num_qubits` : int + The number of qubits in the SO matrix. + """ + so_matrix = generate_random_special_orthogonal_matrix(num_qubits) + + assert is_special_orthogonal_matrix(so_matrix) + + @pytest.mark.parametrize("num_qubits", [1, 2, 3, 4, 5]) + def test_generate_random_special_unitary_matrix( + self, + num_qubits: int + ) -> None: + """ Test the `generate_random_special_unitary_matrix()` function. + + Parameters + ---------- + `num_qubits` : int + The number of qubits in the SU matrix. + """ + su_matrix = generate_random_special_unitary_matrix(num_qubits) + + assert is_special_unitary_matrix(su_matrix) \ No newline at end of file diff --git a/tests/synthesis/gate_decompositions/two_qubit_decomposition/test_weyl.py b/tests/synthesis/gate_decompositions/two_qubit_decomposition/test_weyl.py index a7d9531..ccadaf8 100644 --- a/tests/synthesis/gate_decompositions/two_qubit_decomposition/test_weyl.py +++ b/tests/synthesis/gate_decompositions/two_qubit_decomposition/test_weyl.py @@ -21,18 +21,20 @@ from numpy.typing import NDArray from scipy.stats import unitary_group -from quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl import weyl_coordinates +from quick.synthesis.gate_decompositions.two_qubit_decomposition.weyl import ( + M, + M_DAGGER, + weyl_coordinates +) # Tolerance for floating point comparisons INVARIANT_TOL = 1e-12 -# Bell "Magic" basis -MAGIC = 1/np.sqrt(2) * np.array([ - [1, 0, 0, 1j], - [0, 1j, 1, 0], - [0, 1j, -1, 0], - [1, 0, 0, -1j] -], dtype=complex) +# Constants +PI = np.pi +PI_DOUBLE = 2 * PI +PI2 = PI / 2 +PI4 = PI / 4 def two_qubit_local_invariants(U: NDArray[np.complex128]) -> NDArray[np.float64]: @@ -60,17 +62,19 @@ def two_qubit_local_invariants(U: NDArray[np.complex128]) -> NDArray[np.float64] raise ValueError("Unitary must correspond to a two-qubit gate.") # Transform to bell basis - Um = MAGIC.conj().T.dot(U.dot(MAGIC)) - # Get determinate since +- one is allowed. - det_um = np.linalg.det(Um) - M = Um.T.dot(Um) + U_magic_basis = M_DAGGER @ U @ M + + # Get det since +- one is allowed. + det_um = np.linalg.det(U_magic_basis) + M_squared = U_magic_basis.T @ U_magic_basis + # trace(M)**2 - m_tr2 = M.trace() + m_tr2 = M_squared.trace() m_tr2 *= m_tr2 # Table II of Ref. 1 or Eq. 28 of Ref. 2. G1 = m_tr2 / (16 * det_um) - G2 = (m_tr2 - np.trace(M.dot(M))) / (4 * det_um) + G2 = (m_tr2 - np.trace(M_squared.dot(M_squared))) / (4 * det_um) # Here we split the real and imag pieces of G1 into two so as # to better equate to the Weyl chamber coordinates (c0,c1,c2) @@ -114,21 +118,127 @@ class TestWeyl: class. """ def test_weyl_coordinates_simple(self) -> None: - """ Check Weyl coordinates against known cases. + """ Check Weyl coordinates against known basis gates within the Weyl tetrahedron. + + .. math:: + A(a, b, c) = e^{(ia X \otimes X + ib Y \otimes Y + ic Z \otimes Z)} + + Reference for Weyl coordinates, however, we modify the coordinates slightly to match + the above representation instead: + https://threeplusone.com/pubs/on_gates.pdf Section 6 """ # Identity [0,0,0] U = np.identity(4).astype(complex) weyl = weyl_coordinates(U) assert_almost_equal(weyl, [0, 0, 0], decimal=8) - # CNOT [pi/4, 0, 0] - U = np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]], dtype=complex) + # CX [pi/4, 0, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0] + ], dtype=complex) weyl = weyl_coordinates(U) assert_almost_equal(weyl, [np.pi / 4, 0, 0], decimal=8) - # SWAP [pi/4, pi/4 ,pi/4] - U = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=complex) + # CY [pi/4, 0, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, -1j], + [0, 0, 1j, 0] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, 0, 0], decimal=8) + + # CZ [pi/4, 0, 0] + U = np.array([[ + 1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, -1] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, 0, 0], decimal=8) + + # CH [pi/4, 0, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1/np.sqrt(2), 1/np.sqrt(2)], + [0, 0, 1/np.sqrt(2), -1/np.sqrt(2)] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, 0, 0], decimal=8) + + # Mølmer–Sørensen [pi/4, 0, 0] + U = np.array([ + [1, 0, 0, 1j], + [0, 1, 1j, 0], + [0, 1j, 1, 0], + [1j, 0, 0, 1] + ], dtype=complex) * 1 / np.sqrt(2) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi/4, 0, 0], decimal=8) + + # Magic [pi/4, 0, 0] + U = np.array([ + [1, 1j, 0, 0], + [0, 0, 1j, 1], + [0, 0, 1j, -1], + [1, -1j, 0, 0] + ], dtype=complex) * 1 / np.sqrt(2) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, 0, 0], decimal=8) + + # ISWAP (imaginary SWAP) [pi/4, pi/4, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 0, 1j, 0], + [0, 1j, 0, 0], + [0, 0, 0, 1] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, np.pi / 4, 0], decimal=8) + + # Fermionic SWAP [pi/4, pi/4, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, -1] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, np.pi / 4, 0], decimal=8) + + # DCX [pi/4, pi/4, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 1, 0, 0], + [0, 0, 1, 0] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, np.pi / 4, 0], decimal=8) + + # Inverse DCX [pi/4, pi/4, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [0, 1, 0, 0] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi / 4, np.pi / 4, 0], decimal=8) + # SWAP [pi/4, pi/4, pi/4] + U = np.array([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1] + ], dtype=complex) weyl = weyl_coordinates(U) assert_almost_equal(weyl, [np.pi / 4, np.pi / 4, np.pi / 4], decimal=8) @@ -142,14 +252,101 @@ def test_weyl_coordinates_simple(self) -> None: ], dtype=complex, ) - weyl = weyl_coordinates(U) assert_almost_equal(weyl, [np.pi / 8, np.pi / 8, 0], decimal=8) + # Ising XX [t/2, 0, 0] + t = 0.1 + U = np.array([ + [np.cos(t/2), 0, 0, -1j * np.sin(t/2)], + [0, np.cos(t/2), -1j * np.sin(t/2), 0], + [0, -1j * np.sin(t/2), np.cos(t/2), 0], + [-1j * np.sin(t/2), 0, 0, np.cos(t/2)] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [t/2, 0, 0], decimal=8) + + # Ising YY [t/2, 0, 0] + t = 0.2 + U = np.array([ + [np.cos(t/2), 0, 0, 1j * np.sin(t/2)], + [0, np.cos(t/2), -1j * np.sin(t/2), 0], + [0, -1j * np.sin(t/2), np.cos(t/2), 0], + [1j * np.sin(t/2), 0, 0, np.cos(t/2)] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [t/2, 0, 0], decimal=8) + + # Ising ZZ [t/2, 0, 0] + t = 0.3 + U = np.array([ + [np.exp(-1j * t/2), 0, 0, 0], + [0, np.exp(1j * t/2), 0, 0], + [0, 0, np.exp(1j * t/2), 0], + [0, 0, 0, np.exp(-1j * t/2)] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [t/2, 0, 0], decimal=8) + + # CSX [pi/8, 0, 0] + U = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, (1+1j)/2, (1-1j)/2], + [0, 0, (1-1j)/2, (1+1j)/2] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi/8, 0, 0], decimal=8) + + # XY [t, t, 0] + t = 0.1 + U = np.array([ + [1, 0, 0, 0], + [0, np.cos(2*t), -1j * np.sin(2*t), 0], + [0, -1j * np.sin(2*t), np.cos(2*t), 0], + [0, 0, 0, 1] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [t, t, 0], decimal=8) + + # Givens [t/2, t/2, 0] + t = 0.1 + U = np.array([ + [1, 0, 0, 0], + [0, np.cos(t), -np.sin(t), 0], + [0, np.sin(t), np.cos(t), 0], + [0, 0, 0, 1] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [t/2, t/2, 0], decimal=8) + + # DB [3pi/16, 3pi/16, 0] + U = np.array([ + [1, 0, 0, 0], + [0, np.cos(3 * np.pi / 8), -np.sin(3 * np.pi / 8), 0], + [0, np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], + [0, 0, 0, 1] + ], dtype=complex) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [3 * np.pi / 16, 3 * np.pi / 16, 0], decimal=8) + + # SQRT SWAP [pi/8, pi/8, -pi/8] + U = np.array([ + [1, 0, 0, 0], + [0, (1 + 1j)/2, (1 - 1j)/2, 0], + [0, (1 - 1j)/2, (1 + 1j)/2, 0], + [0, 0, 0, 1] + ]) + weyl = weyl_coordinates(U) + assert_almost_equal(weyl, [np.pi/8, np.pi/8, -np.pi/8], decimal=8) + def test_weyl_coordinates_random(self) -> None: """ Randomly check Weyl coordinates with local invariants. + This test is useful for verifying the correctness of the + decomposition for arbitrary basis gates, which is useful + for transpilation if the decomposition supports it. """ - for _ in range(10): + for _ in range(30): U = unitary_group.rvs(4).astype(complex) weyl = weyl_coordinates(U) local_equiv = local_equivalence(weyl.astype(float)) diff --git a/tests/synthesis/statepreparation/test_isometry.py b/tests/synthesis/statepreparation/test_isometry.py index 7e13d72..cfdb609 100644 --- a/tests/synthesis/statepreparation/test_isometry.py +++ b/tests/synthesis/statepreparation/test_isometry.py @@ -22,17 +22,15 @@ import pytest from quick.circuit import QiskitCircuit -from quick.primitives import Bra, Ket +from quick.primitives import Statevector from quick.random import generate_random_state from quick.synthesis.statepreparation import Isometry from tests.synthesis.statepreparation import StatePreparationTemplate # Define the test data generated_data = generate_random_state(7) -test_data_bra = Bra(generated_data) -test_data_ket = Ket(generated_data) -checker_data_ket = copy.deepcopy(test_data_ket) -checker_data_bra = copy.deepcopy(test_data_ket.to_bra()) +test_statevector = Statevector(generated_data) +checker_statevector = copy.deepcopy(test_statevector) class TestIsometry(StatePreparationTemplate): @@ -46,31 +44,18 @@ def test_init_invalid_output_framework(self) -> None: with pytest.raises(TypeError): Isometry("invalid_framework") # type: ignore - def test_prepare_state_bra(self) -> None: - # Initialize the Isometry encoder - isometry_encoder = Isometry(QiskitCircuit) - - # Encode the data to a circuit - circuit = isometry_encoder.prepare_state(test_data_bra) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) - def test_prepare_state_ket(self) -> None: # Initialize the Isometry encoder isometry_encoder = Isometry(QiskitCircuit) # Encode the data to a circuit - circuit = isometry_encoder.prepare_state(test_data_ket) + circuit = isometry_encoder.prepare_state(test_statevector) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_prepare_state_ndarray(self) -> None: # Initialize the Isometry encoder @@ -83,7 +68,7 @@ def test_prepare_state_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_ket(self) -> None: # Initialize the Isometry encoder @@ -93,29 +78,13 @@ def test_apply_state_ket(self) -> None: circuit = QiskitCircuit(7) # Apply the state to a circuit - circuit = isometry_encoder.apply_state(circuit, test_data_ket, range(7)) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) - - def test_apply_state_bra(self) -> None: - # Initialize the Isometry encoder - isometry_encoder = Isometry(QiskitCircuit) - - # Initialize the circuit - circuit = QiskitCircuit(7) - - # Apply the state to a circuit - circuit = isometry_encoder.apply_state(circuit, test_data_bra, range(7)) + circuit = isometry_encoder.apply_state(circuit, test_statevector, range(7)) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_ndarray(self) -> None: # Initialize the Isometry encoder @@ -131,7 +100,7 @@ def test_apply_state_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_invalid_input(self) -> None: # Initialize the Isometry encoder @@ -151,10 +120,10 @@ def test_apply_state_invalid_qubit_indices(self) -> None: circuit = QiskitCircuit(7) with pytest.raises(TypeError): - isometry_encoder.apply_state(circuit, test_data_ket, "invalid_qubit_indices") # type: ignore + isometry_encoder.apply_state(circuit, test_statevector, "invalid_qubit_indices") # type: ignore with pytest.raises(TypeError): - isometry_encoder.apply_state(circuit, test_data_ket, [1+1j, 2+2j, 3+3j]) # type: ignore + isometry_encoder.apply_state(circuit, test_statevector, [1+1j, 2+2j, 3+3j]) # type: ignore def test_apply_state_qubit_indices_out_of_range(self) -> None: # Initialize the Isometry encoder @@ -164,4 +133,4 @@ def test_apply_state_qubit_indices_out_of_range(self) -> None: circuit = QiskitCircuit(7) with pytest.raises(IndexError): - isometry_encoder.apply_state(circuit, test_data_ket, [0, 1, 2, 3, 4, 5, 12]) \ No newline at end of file + isometry_encoder.apply_state(circuit, test_statevector, [0, 1, 2, 3, 4, 5, 12]) \ No newline at end of file diff --git a/tests/synthesis/statepreparation/test_mottonen.py b/tests/synthesis/statepreparation/test_mottonen.py index 4c2fee5..4ffeed4 100644 --- a/tests/synthesis/statepreparation/test_mottonen.py +++ b/tests/synthesis/statepreparation/test_mottonen.py @@ -22,17 +22,15 @@ import pytest from quick.circuit import QiskitCircuit -from quick.primitives import Bra, Ket +from quick.primitives import Statevector from quick.random import generate_random_state from quick.synthesis.statepreparation import Mottonen from tests.synthesis.statepreparation import StatePreparationTemplate # Define the test data generated_data = generate_random_state(7) -test_data_bra = Bra(generated_data) -test_data_ket = Ket(generated_data) -checker_data_ket = copy.deepcopy(test_data_ket) -checker_data_bra = copy.deepcopy(test_data_ket.to_bra()) +test_statevector = Statevector(generated_data) +checker_statevector = copy.deepcopy(test_statevector) class TestMottonen(StatePreparationTemplate): @@ -46,31 +44,18 @@ def test_init_invalid_output_framework(self) -> None: with pytest.raises(TypeError): Mottonen("invalid_framework") # type: ignore - def test_prepare_state_bra(self) -> None: - # Initialize the Mottonen encoder - shende_encoder = Mottonen(QiskitCircuit) - - # Encode the data to a circuit - circuit = shende_encoder.prepare_state(test_data_bra) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) - def test_prepare_state_ket(self) -> None: # Initialize the Mottonen encoder shende_encoder = Mottonen(QiskitCircuit) # Encode the data to a circuit - circuit = shende_encoder.prepare_state(test_data_ket) + circuit = shende_encoder.prepare_state(test_statevector) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_prepare_state_ndarray(self) -> None: # Initialize the Mottonen encoder @@ -83,7 +68,7 @@ def test_prepare_state_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_ket(self) -> None: # Initialize the Mottonen encoder @@ -93,29 +78,13 @@ def test_apply_state_ket(self) -> None: circuit = QiskitCircuit(7) # Apply the state to a circuit - circuit = shende_encoder.apply_state(circuit, test_data_ket, range(7)) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) - - def test_apply_state_bra(self) -> None: - # Initialize the Mottonen encoder - shende_encoder = Mottonen(QiskitCircuit) - - # Initialize the circuit - circuit = QiskitCircuit(7) - - # Apply the state to a circuit - circuit = shende_encoder.apply_state(circuit, test_data_bra, range(7)) + circuit = shende_encoder.apply_state(circuit, test_statevector, range(7)) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_ndarray(self) -> None: # Initialize the Mottonen encoder @@ -131,7 +100,7 @@ def test_apply_state_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_invalid_input(self) -> None: # Initialize the Mottonen encoder @@ -153,10 +122,10 @@ def test_apply_state_invalid_qubit_indices(self) -> None: # Apply the state to a circuit with pytest.raises(TypeError): - shende_encoder.apply_state(circuit, test_data_ket, "invalid_qubit_indices") # type: ignore + shende_encoder.apply_state(circuit, test_statevector, "invalid_qubit_indices") # type: ignore with pytest.raises(TypeError): - shende_encoder.apply_state(circuit, test_data_ket, [1+1j, 2+2j, 3+3j]) # type: ignore + shende_encoder.apply_state(circuit, test_statevector, [1+1j, 2+2j, 3+3j]) # type: ignore def test_apply_state_qubit_indices_out_of_range(self) -> None: # Initialize the Mottonen encoder @@ -167,4 +136,4 @@ def test_apply_state_qubit_indices_out_of_range(self) -> None: # Apply the state to a circuit with pytest.raises(IndexError): - shende_encoder.apply_state(circuit, test_data_ket, [0, 1, 2, 3, 4, 5, 12]) \ No newline at end of file + shende_encoder.apply_state(circuit, test_statevector, [0, 1, 2, 3, 4, 5, 12]) \ No newline at end of file diff --git a/tests/synthesis/statepreparation/test_shende.py b/tests/synthesis/statepreparation/test_shende.py index 3678079..cfdc6c0 100644 --- a/tests/synthesis/statepreparation/test_shende.py +++ b/tests/synthesis/statepreparation/test_shende.py @@ -22,17 +22,15 @@ import pytest from quick.circuit import QiskitCircuit -from quick.primitives import Bra, Ket +from quick.primitives import Statevector from quick.random import generate_random_state from quick.synthesis.statepreparation import Shende from tests.synthesis.statepreparation import StatePreparationTemplate # Define the test data generated_data = generate_random_state(7) -test_data_bra = Bra(generated_data) -test_data_ket = Ket(generated_data) -checker_data_ket = copy.deepcopy(test_data_ket) -checker_data_bra = copy.deepcopy(test_data_ket.to_bra()) +test_statevector = Statevector(generated_data) +checker_statevector = copy.deepcopy(test_statevector) class TestShende(StatePreparationTemplate): @@ -46,31 +44,18 @@ def test_init_invalid_output_framework(self) -> None: with pytest.raises(TypeError): Shende("invalid_framework") # type: ignore - def test_prepare_state_bra(self) -> None: - # Initialize the Shende encoder - shende_encoder = Shende(QiskitCircuit) - - # Encode the data to a circuit - circuit = shende_encoder.prepare_state(test_data_bra) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) - def test_prepare_state_ket(self) -> None: # Initialize the Shende encoder shende_encoder = Shende(QiskitCircuit) # Encode the data to a circuit - circuit = shende_encoder.prepare_state(test_data_ket) + circuit = shende_encoder.prepare_state(test_statevector) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_prepare_state_ndarray(self) -> None: # Initialize the Shende encoder @@ -83,7 +68,7 @@ def test_prepare_state_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_ket(self) -> None: # Initialize the Shende encoder @@ -93,29 +78,13 @@ def test_apply_state_ket(self) -> None: circuit = QiskitCircuit(7) # Apply the state to a circuit - circuit = shende_encoder.apply_state(circuit, test_data_ket, range(7)) - - # Get the state of the circuit - statevector = circuit.get_statevector() - - # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) - - def test_apply_state_bra(self) -> None: - # Initialize the Shende encoder - shende_encoder = Shende(QiskitCircuit) - - # Initialize the circuit - circuit = QiskitCircuit(7) - - # Apply the state to a circuit - circuit = shende_encoder.apply_state(circuit, test_data_bra, range(7)) + circuit = shende_encoder.apply_state(circuit, test_statevector, range(7)) # Get the state of the circuit statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_bra.data, decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_ndarray(self) -> None: # Initialize the Shende encoder @@ -131,7 +100,7 @@ def test_apply_state_ndarray(self) -> None: statevector = circuit.get_statevector() # Ensure that the state vector is close enough to the expected state vector - assert_almost_equal(np.array(statevector), checker_data_ket.data.flatten(), decimal=8) + assert_almost_equal(np.array(statevector), checker_statevector.data.flatten(), decimal=8) def test_apply_state_invalid_input(self) -> None: # Initialize the Shende encoder @@ -151,10 +120,10 @@ def test_apply_state_invalid_qubit_indices(self) -> None: circuit = QiskitCircuit(7) with pytest.raises(TypeError): - shende_encoder.apply_state(circuit, test_data_ket, "invalid_qubit_indices") # type: ignore + shende_encoder.apply_state(circuit, test_statevector, "invalid_qubit_indices") # type: ignore with pytest.raises(TypeError): - shende_encoder.apply_state(circuit, test_data_ket, [1+1j, 2+2j, 3+3j]) # type: ignore + shende_encoder.apply_state(circuit, test_statevector, [1+1j, 2+2j, 3+3j]) # type: ignore def test_apply_state_qubit_indices_out_of_range(self) -> None: # Initialize the Shende encoder @@ -164,4 +133,4 @@ def test_apply_state_qubit_indices_out_of_range(self) -> None: circuit = QiskitCircuit(7) with pytest.raises(IndexError): - shende_encoder.apply_state(circuit, test_data_ket, [0, 1, 2, 3, 4, 5, 12]) \ No newline at end of file + shende_encoder.apply_state(circuit, test_statevector, [0, 1, 2, 3, 4, 5, 12]) \ No newline at end of file diff --git a/tests/synthesis/statepreparation/test_statepreparation.py b/tests/synthesis/statepreparation/test_statepreparation.py index 64a61f2..0655e84 100644 --- a/tests/synthesis/statepreparation/test_statepreparation.py +++ b/tests/synthesis/statepreparation/test_statepreparation.py @@ -35,12 +35,7 @@ def test_init_invalid_output_framework(self) -> None: @abstractmethod def test_prepare_state_ket(self) -> None: - """ Test the preparation of the state from a `quick.primitives.Ket` instance. - """ - - @abstractmethod - def test_prepare_state_bra(self) -> None: - """ Test the preparation of the state from a `quick.primitives.Bra` instance. + """ Test the preparation of the state from a `quick.primitives.Statevector` instance. """ @abstractmethod @@ -50,12 +45,7 @@ def test_prepare_state_ndarray(self) -> None: @abstractmethod def test_apply_state_ket(self) -> None: - """ Test the application of the state from a `quick.primitives.Ket` instance. - """ - - @abstractmethod - def test_apply_state_bra(self) -> None: - """ Test the application of the state from a `quick.primitives.Bra` instance. + """ Test the application of the state from a `quick.primitives.Statevector` instance. """ @abstractmethod