diff --git a/doc/pulse-paper/qft.py b/doc/pulse-paper/qft.py index 7014a018a..bcd5b4a5e 100644 --- a/doc/pulse-paper/qft.py +++ b/doc/pulse-paper/qft.py @@ -42,9 +42,9 @@ def get_control_latex(model): num_qubits = model.num_qubits num_coupling = model._get_num_coupling() return [ - {f"sx{m}": r"$\sigma_x^{}$".format(m) for m in range(num_qubits)}, - {f"sz{m}": r"$\sigma_z^{}$".format(m) for m in range(num_qubits)}, - {f"g{m}": r"$g_{}$".format(m) for m in range(num_coupling)}, + {f"sx{m}": rf"$\sigma_x^{m}$" for m in range(num_qubits)}, + {f"sz{m}": rf"$\sigma_z^{m}$" for m in range(num_qubits)}, + {f"g{m}": rf"$g_{m}$" for m in range(num_coupling)}, ] diff --git a/doc/source/apidoc/qutip_qip.operations.rst b/doc/source/apidoc/qutip_qip.operations.rst index 3e9d900eb..6b0fc98db 100644 --- a/doc/source/apidoc/qutip_qip.operations.rst +++ b/doc/source/apidoc/qutip_qip.operations.rst @@ -50,6 +50,6 @@ qutip\_qip.operations .. autosummary:: - controlled_gate + controlled_gate_unitary expand_operator gate_sequence_product diff --git a/doc/source/qip-basics.rst b/doc/source/qip-basics.rst index f3a16fd2d..b2b509ca1 100644 --- a/doc/source/qip-basics.rst +++ b/doc/source/qip-basics.rst @@ -139,38 +139,51 @@ The pre-defined gates for the class :class:`~.operations.Gate` are shown in the ==================== ======================================== Gate name Description ==================== ======================================== -"RX" Rotation around x axis -"RY" Rotation around y axis -"RZ" Rotation around z axis -"R" Arbitrary single qubit rotation "X" Pauli-X gate "Y" Pauli-Y gate "Z" Pauli-Z gate +"H" Hadamard gate "S" Single-qubit rotation or Z90 +"Sdag" Inverse of S gate "T" Square root of S gate +"Tdag" Inverse of T gate "SQRTX" Square root of X gate -"H" Hadamard gate -"PHASEGATE" Add a phase one the state 1 -"CRX" Controlled rotation around x axis -"CRY" Controlled rotation around y axis -"CRZ" Controlled rotation around z axis -"CX" Controlled X gate (also called CNOT) +"SQRTXdag" Inverse of SQRTX gate +"RX" Rotation around x axis +"RY" Rotation around y axis +"RZ" Rotation around z axis +"PHASE" Adds a relative phase to ket 1 +"R" Arbitrary single qubit rotation +"QASMU" U rotation gate used as a primitive in the QASM standard +"CX" (CNOT) Controlled X gate "CY" Controlled Y gate "CZ" Controlled Z gate +"CH" Controlled H gate "CS" Controlled S gate +"CSdag" Controlled Sdag gate "CT" Controlled T gate -"CPHASE" Controlled phase gate -"QASMU" U rotation gate used as a primitive in the QASM standard -"BERKELEY" Berkeley gate -"SWAPalpha" SWAPalpha gate +"CTdag" Controlled Tdag gate +"CRX" Controlled rotation around x axis +"CRY" Controlled rotation around y axis +"CRZ" Controlled rotation around z axis +"CPHASE" Controlled Phase gate +"CQASMU" Controlled QASMU gate "SWAP" Swap the states of two qubits "ISWAP" Swap gate with additional phase for 01 and 10 states +"ISWAPdag" Inverse of ISWAP gate "SQRTSWAP" Square root of the SWAP gate +"SQRTSWAPdag" Inverse of SQRTSWAP gate "SQRTISWAP" Square root of the ISWAP gate +"SQRTISWAPdag" Inverse of SQRTISWAP gate +"BERKELEY" Berkeley gate +"BERKELEYdag" Inverse of BERKELEY gate +"SWAPALPHA" SWAPALPHA gate "MS" Mølmer-Sørensen gate +"RZX" RZX gate +"TOFFOLI" (CCX) Toffoli gate "FREDKIN" Fredkin gate -"TOFFOLI" Toffoli gate -"GLOBALPHASE" Global phase +"GLOBALPHASE" Global phase gate +"IDENTITY" Identity gate ==================== ======================================== For some of the gates listed above, :class:`.QubitCircuit` also has a primitive :func:`.QubitCircuit.resolve_gates()` method that decomposes them into elementary gate sets such as CX or SWAP with single-qubit gates (RX, RY and RZ). However, this method is not fully optimized. It is very likely that the depth of the circuit can be further reduced by merging quantum gates. It is required that the gate resolution be carried out before the measurements to the circuit are added. diff --git a/tests/pytest.ini b/pytest.ini similarity index 74% rename from tests/pytest.ini rename to pytest.ini index 7a448a001..d8be05f29 100644 --- a/tests/pytest.ini +++ b/pytest.ini @@ -1,22 +1,19 @@ [pytest] markers = slow: Mark a test as taking a long time to run, and so can be skipped with `pytest -m "not slow"`. - repeat(n): Repeat the given test 'n' times. requires_cython: Mark that the given test requires Cython to be installed. Such tests will be skipped if Cython is not available. + filterwarnings = error ; ImportWarning: PyxImporter.find_spec() not found ignore:PyxImporter:ImportWarning - ; DeprecationWarning: Please use `upcast` from the `scipy.sparse` namespace - ignore::DeprecationWarning:qutip.fastsparse*: + ignore::UserWarning: ignore:matplotlib not found:UserWarning ignore:the imp module is deprecated in favour of importlib:DeprecationWarning ignore:Dedicated options class are no longer needed, options should be passed as dict to solvers.:FutureWarning ignore::DeprecationWarning:qiskit.utils.algorithm_globals: - # Deprecation warning for python = 3.9 with matplotlib 3.9.4 - ignore:'mode' parameter is deprecated # Deprecation warning for scipy disp interface, will be removed in scipy 1.18 ignore:.*`disp` and `iprint` options.*L-BFGS-B.*deprecated.*:DeprecationWarning - - +pythonpath = src +testpaths = tests diff --git a/src/qutip_qip/algorithms/bit_flip.py b/src/qutip_qip/algorithms/bit_flip.py index d1a1665fc..34494a944 100644 --- a/src/qutip_qip/algorithms/bit_flip.py +++ b/src/qutip_qip/algorithms/bit_flip.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import CX, TOFFOLI, X +from qutip_qip.operations.gates import CX, TOFFOLI, X class BitFlipCode: diff --git a/src/qutip_qip/algorithms/phase_flip.py b/src/qutip_qip/algorithms/phase_flip.py index 2441b57cd..5992747f2 100644 --- a/src/qutip_qip/algorithms/phase_flip.py +++ b/src/qutip_qip/algorithms/phase_flip.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import CX, H +from qutip_qip.operations.gates import CX, H class PhaseFlipCode: diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py index 2dca6fd27..a84a1a645 100644 --- a/src/qutip_qip/algorithms/qft.py +++ b/src/qutip_qip/algorithms/qft.py @@ -3,10 +3,11 @@ """ import numpy as np -from qutip_qip.operations import H, RZ, CX, CPHASE, SWAP, expand_operator -from qutip_qip.circuit import QubitCircuit from qutip import Qobj +from qutip_qip.circuit import QubitCircuit from qutip_qip.decompose import decompose_one_qubit_gate +from qutip_qip.operations import expand_operator +from qutip_qip.operations.gates import H, RZ, CX, CPHASE, SWAP def qft(N=1): @@ -66,7 +67,7 @@ def qft_steps(N=1, swapping=True): for j in range(i): U_step_list.append( expand_operator( - CPHASE(np.pi / (2 ** (i - j))).get_qobj(), + CPHASE(theta=np.pi / (2 ** (i - j))).get_qobj(), dims=[2] * N, targets=[i, j], ) @@ -113,7 +114,10 @@ def qft_gate_sequence(N=1, swapping=True, to_cnot=False): for j in range(i): if not to_cnot: qc.add_gate( - CPHASE(np.pi / (2 ** (i - j)), arg_label=r"{\pi/2^{%d}}" % (i - j)), + CPHASE( + theta=np.pi / (2 ** (i - j)), + arg_label=r"{\pi/2^{%d}}" % (i - j), + ), targets=[j], controls=[i], ) @@ -136,6 +140,6 @@ def _cphase_to_cnot(targets, controls, arg_value, qc: QubitCircuit): qc.add_gate(decomposed_gates[4], targets=targets) qc.add_gate(CX, targets=targets, controls=controls) qc.add_gate(RZ(arg_value / 2), targets=controls) - gate = decomposed_gates[7] - gate.arg_value[0] += arg_value / 4 + gate = decomposed_gates[7] # This is a GLOBALPHASE Gate + gate.arg_value = gate.arg_value[0] + arg_value / 4 qc.add_gate(gate, targets=targets) diff --git a/src/qutip_qip/algorithms/qpe.py b/src/qutip_qip/algorithms/qpe.py index db6a828c0..079eff8ab 100644 --- a/src/qutip_qip/algorithms/qpe.py +++ b/src/qutip_qip/algorithms/qpe.py @@ -1,7 +1,8 @@ import numpy as np -from qutip_qip.circuit import QubitCircuit from qutip_qip.algorithms import qft_gate_sequence -from qutip_qip.operations import custom_gate_factory, controlled_gate_factory, H +from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations import get_unitary_gate, get_controlled_gate +from qutip_qip.operations.gates import H def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): @@ -41,7 +42,7 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): target_qubits = list( range(num_counting_qubits, num_counting_qubits + num_target_qubits) ) - elif isinstance(target_qubits, int): + elif type(target_qubits) is int: target_qubits = [target_qubits] num_target_qubits = 1 else: @@ -61,11 +62,11 @@ def qpe(U, num_counting_qubits, target_qubits=None, to_cnot=False): U_power = U if power == 1 else U**power # Add controlled-U^power gate - controlled_u = controlled_gate_factory( - gate=custom_gate_factory( - gate_name="U^power gate", + controlled_u = get_controlled_gate( + gate=get_unitary_gate( + gate_name=f"U^{power}", U=U_power, - )(), + ), ) qc.add_gate(controlled_u, targets=target_qubits, controls=[i]) diff --git a/src/qutip_qip/circuit/__init__.py b/src/qutip_qip/circuit/__init__.py index 01799f66d..de8d57221 100644 --- a/src/qutip_qip/circuit/__init__.py +++ b/src/qutip_qip/circuit/__init__.py @@ -1,7 +1,5 @@ """Circuit representation and simulation at the gate level.""" -import warnings - from .instruction import ( CircuitInstruction, GateInstruction, @@ -9,32 +7,6 @@ ) from .simulator import CircuitResult, CircuitSimulator from .circuit import QubitCircuit -from qutip_qip.operations import Gate, Measurement - - -def _add_deprecation(fun, msg): - def newfun(*args, **kwargs): - warnings.warn( - msg, - DeprecationWarning, - stacklevel=2, - ) - return fun(*args, **kwargs) - - return newfun - - -Gate = _add_deprecation( - Gate, - "The class Gate has been moved to qutip_qip.operations." - "Please use update the import statement.\n", -) -Measurement = _add_deprecation( - Measurement, - "The class Measurement has been moved to qutip_qip.operations." - "Please use update the import statement.\n", -) - __all__ = [ "CircuitSimulator", diff --git a/src/qutip_qip/circuit/_decompose.py b/src/qutip_qip/circuit/_decompose.py index a68922476..156728516 100644 --- a/src/qutip_qip/circuit/_decompose.py +++ b/src/qutip_qip/circuit/_decompose.py @@ -5,7 +5,18 @@ """ import numpy as np -from qutip_qip.operations import RX, RY, RZ, CX, ISWAP, SQRTSWAP, SQRTISWAP, CZ +from qutip_qip.operations.gates import ( + RX, + RY, + RZ, + CX, + ISWAP, + SQRTSWAP, + SQRTISWAP, + CZ, + CNOT, + SWAP, +) __all__ = ["_resolve_to_universal", "_resolve_2q_basis"] @@ -53,7 +64,8 @@ def _gate_SQRTNOT(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi / 4) temp_resolved.add_gate( - RX(arg_value=np.pi / 2, arg_label=r"\pi/2"), targets=targets, + RX(np.pi / 2, arg_label=r"\pi/2"), + targets=targets, ) @@ -63,10 +75,12 @@ def _gate_H(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=half_pi) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(np.pi, arg_label=r"\pi"), + targets=targets, ) @@ -79,7 +93,7 @@ def _gate_PHASEGATE(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=gate.arg_value / 2) temp_resolved.add_gate( - gate=RZ(arg_value=gate.arg_value, arg_label=gate.arg_label), + gate=RZ(gate.arg_value, arg_label=gate.arg_label), targets=targets, ) @@ -91,19 +105,24 @@ def _gate_CZ(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(np.pi, arg_label=r"\pi"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(np.pi, arg_label=r"\pi"), + targets=targets, ) + _gate_CSIGN = _gate_CZ @@ -124,23 +143,29 @@ def _gate_ISWAP(circ_instruction, temp_resolved): temp_resolved.add_gate(CX, targets=targets[1], controls=targets[0]) temp_resolved.add_gate(CX, targets=targets[0], controls=targets[1]) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[0], + RZ(half_pi, arg_label=r"\pi/2"), + targets=targets[0], ) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[1], + RZ(half_pi, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[0], + RY(half_pi, arg_label=r"\pi/2"), + targets=targets[0], ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets[0], + RX(np.pi, arg_label=r"\pi"), + targets=targets[0], ) temp_resolved.add_gate(CX, targets=targets[0], controls=targets[1]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets[0], + RY(half_pi, arg_label=r"\pi/2"), + targets=targets[0], ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets[0], + RX(np.pi, arg_label=r"\pi"), + targets=targets[0], ) @@ -151,59 +176,71 @@ def _gate_FREDKIN(circ_instruction, temp_resolved): temp_resolved.add_gate(CX, controls=targets[1], targets=targets[0]) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), targets=targets[1], + RZ(pi, arg_label=r"\pi"), + targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1], + RX(pi / 2, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], + RZ(-pi / 2, arg_label=r"-\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1], + RX(pi / 2, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), targets=targets[1], + RZ(pi, arg_label=r"\pi"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[0], targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), targets=targets[1], + RZ(-pi / 4, arg_label=r"-\pi/4"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), targets=targets[1], + RZ(pi / 4, arg_label=r"\pi/4"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[0], targets=targets[1]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), targets=targets[0], + RZ(pi / 4, arg_label=r"\pi/4"), + targets=targets[0], ) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), targets=targets[1], + RZ(-pi / 4, arg_label=r"-\pi/4"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[1]) temp_resolved.add_gate(CX, controls=controls, targets=targets[0]) temp_resolved.add_gate( - RZ(arg_value=pi / 4, arg_label=r"\pi/4"), targets=controls, + RZ(pi / 4, arg_label=r"\pi/4"), + targets=controls, ) temp_resolved.add_gate( - RZ(arg_value=-pi / 4, arg_label=r"-\pi/4"), targets=targets[0], + RZ(-pi / 4, arg_label=r"-\pi/4"), + targets=targets[0], ) temp_resolved.add_gate(CX, controls=controls, targets=targets[0]) temp_resolved.add_gate( - gate=RZ(arg_value=-3 * pi / 4, arg_label=r"-3\pi/4"), + gate=RZ(-3 * pi / 4, arg_label=r"-3\pi/4"), targets=targets[1], ) + temp_resolved.add_gate(RX(pi / 2, arg_label=r"\pi/2"), targets=targets[1]) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1] - ) - temp_resolved.add_gate( - RZ(arg_value=-pi / 2, arg_label=r"-\pi/2"), targets=targets[1], + RZ(-pi / 2, arg_label=r"-\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RX(arg_value=pi / 2, arg_label=r"\pi/2"), targets=targets[1], + RX(pi / 2, arg_label=r"\pi/2"), + targets=targets[1], ) temp_resolved.add_gate( - RZ(arg_value=pi, arg_label=r"\pi"), targets=targets[1], + RZ(pi, arg_label=r"\pi"), + targets=targets[1], ) temp_resolved.add_gate(CX, controls=targets[1], targets=targets[0]) temp_resolved.add_global_phase(phase=pi / 8) @@ -219,47 +256,58 @@ def _gate_TOFFOLI(circ_instruction, temp_resolved): temp_resolved.add_global_phase(phase=np.pi / 8) temp_resolved.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), targets=controls[1], + RZ(half_pi, arg_label=r"\pi/2"), + targets=controls[1], ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=controls[0], + RZ(quarter_pi, arg_label=r"\pi/4"), + targets=controls[0], ) temp_resolved.add_gate(CX, targets=controls[1], controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), + RZ(-quarter_pi, arg_label=r"-\pi/4"), targets=controls[1], ) temp_resolved.add_gate(CX, targets=controls[1], controls=controls[0]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(np.pi, arg_label=r"\pi"), + targets=targets, ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=controls[1], + RZ(quarter_pi, arg_label=r"\pi/4"), + targets=controls[1], ) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=targets, + RZ(quarter_pi, arg_label=r"\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), targets=targets, + RZ(-quarter_pi, arg_label=r"-\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[1]) temp_resolved.add_gate( - RZ(arg_value=quarter_pi, arg_label=r"\pi/4"), targets=targets, + RZ(quarter_pi, arg_label=r"\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[0]) temp_resolved.add_gate( - RZ(arg_value=-quarter_pi, arg_label=r"-\pi/4"), targets=targets, + RZ(-quarter_pi, arg_label=r"-\pi/4"), + targets=targets, ) temp_resolved.add_gate(CX, targets=targets, controls=controls[1]) temp_resolved.add_gate( - RY(arg_value=half_pi, arg_label=r"\pi/2"), targets=targets, + RY(half_pi, arg_label=r"\pi/2"), + targets=targets, ) temp_resolved.add_gate( - RX(arg_value=np.pi, arg_label=r"\pi"), targets=targets, + RX(np.pi, arg_label=r"\pi"), + targets=targets, ) temp_resolved.add_global_phase(phase=np.pi) @@ -271,14 +319,14 @@ def _basis_CZ(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CX: qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate(CZ, targets=targets, controls=controls) qc_temp.add_gate( - gate=RY(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) else: @@ -291,8 +339,10 @@ def _basis_CZ(qc_temp, temp_resolved): style=circ_instruction.style, ) + _basis_CSIGN = _basis_CZ + def _basis_ISWAP(qc_temp, temp_resolved): half_pi = np.pi / 2 quarter_pi = np.pi / 4 @@ -301,46 +351,46 @@ def _basis_ISWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CX: qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RZ(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RZ(half_pi, arg_label=r"\pi/2"), targets=controls, ) qc_temp.add_gate(ISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RZ(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RZ(half_pi, arg_label=r"\pi/2"), targets=targets, ) - elif gate.name == "SWAP": + elif gate == SWAP: qc_temp.add_global_phase(phase=quarter_pi) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets[0], ) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets[1], ) qc_temp.add_gate(ISWAP, targets=targets) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets[0], ) @@ -362,27 +412,27 @@ def _basis_SQRTSWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CX: qc_temp.add_gate( - gate=RY(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RY(half_pi, arg_label=r"\pi/2"), targets=targets, ) qc_temp.add_gate(SQRTSWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RZ(arg_value=np.pi, arg_label=r"\pi"), + gate=RZ(np.pi, arg_label=r"\pi"), targets=controls, ) qc_temp.add_gate(SQRTSWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - gate=RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RZ(-half_pi, arg_label=r"-\pi/2"), targets=controls, ) else: @@ -403,31 +453,31 @@ def _basis_SQRTISWAP(qc_temp, temp_resolved): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "CNOT" or gate.name == "CX": + if gate == CX: qc_temp.add_gate( - gate=RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RY(-half_pi, arg_label=r"-\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RX(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RX(half_pi, arg_label=r"\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + gate=RX(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate(SQRTISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RX(arg_value=np.pi, arg_label=r"\pi"), + gate=RX(np.pi, arg_label=r"\pi"), targets=controls, ) qc_temp.add_gate(SQRTISWAP, targets=[controls[0], targets[0]]) qc_temp.add_gate( - gate=RY(arg_value=half_pi, arg_label=r"\pi/2"), + gate=RY(half_pi, arg_label=r"\pi/2"), targets=controls, ) qc_temp.add_gate( - gate=RZ(arg_value=np.pi, arg_label=r"\pi"), + gate=RZ(np.pi, arg_label=r"\pi"), targets=controls, ) qc_temp.add_global_phase(phase=7 / 4 * np.pi) diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 6745cec8f..1c31f1c08 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -2,30 +2,26 @@ Quantum circuit representation and simulation. """ -import numpy as np import warnings -from typing import Iterable -from qutip_qip.typing import IntList - -from ._decompose import _resolve_to_universal, _resolve_2q_basis -from qutip_qip.operations import ( - Gate, - GLOBALPHASE, - RX, - RY, - RZ, - Measurement, - expand_operator, - GATE_CLASS_MAP, -) +import inspect +from typing import Iterable, Type +from qutip import qeye, Qobj +import numpy as np + from qutip_qip.circuit import ( CircuitSimulator, CircuitInstruction, GateInstruction, MeasurementInstruction, ) -from qutip_qip.circuit.utils import _check_iterable, _check_limit_ -from qutip import qeye, Qobj +from qutip_qip.circuit._decompose import ( + _resolve_to_universal, + _resolve_2q_basis, +) +from qutip_qip.operations import Gate, Measurement, expand_operator +from qutip_qip.operations import gates as std +from qutip_qip.typing import Int, IntSequence +from qutip_qip.utils import check_limit, convert_type_input_to_sequence try: from IPython.display import Image as DisplayImage, SVG as DisplaySVG @@ -67,7 +63,7 @@ def __init__( dims=None, num_cbits=0, user_gates=None, - N = None + N=None, ): # number of qubits in the register self._num_qubits = num_qubits @@ -89,12 +85,16 @@ def __init__( if input_states: self.input_states = input_states else: - self.input_states = [None for i in range(self.num_qubits + num_cbits)] + self.input_states = [ + None for i in range(self.num_qubits + num_cbits) + ] if output_states: self.output_states = output_states else: - self.output_states = [None for i in range(self.num_qubits + num_cbits)] + self.output_states = [ + None for i in range(self.num_qubits + num_cbits) + ] if user_gates is not None: raise ValueError( @@ -102,6 +102,29 @@ def __init__( "To define custom gates refer to this example in documentation " ) + @property + def num_qubits(self) -> int: + """ + Number of qubits in the circuit. + """ + return self._num_qubits + + @property + def N(self) -> int: + """ + Number of qubits in the circuit. + """ + warnings.warn( + "The 'N' parameter is deprecated. Please use " + "'num_qubits' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self._num_qubits + + def __repr__(self) -> str: + return "" + @property def global_phase(self): return self._global_phase @@ -119,6 +142,7 @@ def gates(self) -> list[CircuitInstruction]: ) return self._instructions + # fmt: off gates.setter def gates(self) -> None: warnings.warn( @@ -126,48 +150,17 @@ def gates(self) -> None: DeprecationWarning, stacklevel=2, ) + # fmt: on @property def instructions(self) -> list[CircuitInstruction]: return self._instructions - @property - def num_qubits(self) -> int: - """ - Number of qubits in the circuit. - """ - return self._num_qubits - - @property - def N(self) -> int: - """ - Number of qubits in the circuit. - """ - warnings.warn( - "The 'N' parameter is deprecated. Please use " - "'num_qubits' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._num_qubits - - def __repr__(self) -> str: - return "" - - def _repr_png_(self) -> None: - """ - Provide PNG representation for Jupyter Notebook. - """ - try: - self.draw(renderer="matplotlib") - except ImportError: - self.draw("text") - def add_state( self, state: str, - targets: IntList, - state_type: str = "input", # FIXME Add an enum type hinting? + targets: IntSequence, + state_type: str = "input", # FIXME Add an enum type hinting? ): """ Add an input or output state to the circuit. By default all the input @@ -200,7 +193,7 @@ def add_state( def add_measurement( self, measurement: str | Measurement, - targets: int | IntList, + targets: int | IntSequence, classical_store: int, index: None = None, ): @@ -250,15 +243,15 @@ def add_measurement( def add_gate( self, - gate: Gate | str, - targets: Iterable[int] = [], - controls: Iterable[int] = [], - arg_value: any = None, - arg_label: str | None = None, - control_value: int | None = None, - classical_controls: Iterable[int] = [], + gate: Gate | Type[Gate] | str, + targets: int | IntSequence = (), + controls: int | IntSequence = (), + classical_controls: int | IntSequence = (), classical_control_value: int | None = None, style: dict = None, + arg_value: None = None, + arg_label: None = None, + control_value: None = None, index: None = None, ): """ @@ -266,7 +259,7 @@ def add_gate( Parameters ---------- - gate: string or :class:`~.operations.Gate` + gate: :class:`~.operations.Gate` or :obj:`~.operations.Gate` or str Gate name. If gate is an instance of :class:`~.operations.Gate`, parameters are unpacked and added. targets: int or list, optional @@ -281,7 +274,7 @@ def add_gate( Label for gate representation. classical_controls : int or list of int, optional Indices of classical bits to control the gate. - control_value : int, optional + control_value : optional Value of classical bits to control on, the classical controls are interpreted as an integer with the lowest bit being the first one. If not specified, then the value is interpreted to be @@ -290,104 +283,133 @@ def add_gate( style: For circuit draw """ + # Deprecation warnings if index is not None: raise ValueError("argument index is no longer supported") if arg_value is not None or arg_label is not None: warnings.warn( - "Define 'arg_value', 'arg_label' in your Gate object e.g. RX(arg_value=np.pi)" \ + "Define 'arg_value', 'arg_label' in your Gate object e.g. RX(np.pi)" ", 'arg_value', 'arg_label' arguments will be removed from 'add_gate' method in the future version.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if control_value is not None: warnings.warn( - "Define 'control_value', in your Gate object e.g. CX(control_value=0)" \ - ", 'control_value' argument will be removed from 'add_gate' method in the future version.", + "'control_value' is no longer a valid argument and has been deprecated and will be removed in the future version. " + "from qutip_qip.operations import controlled", + "from qutip_qip.operations.gates import X", + "Example: gate = get_controlled_gate(X, num_ctrl_qubits=1, control_value=0) instead", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) - if isinstance(gate, GLOBALPHASE): + if type(gate) is std.GLOBALPHASE: self.add_global_phase(gate.arg_value[0]) return - # Handling case for int input (TODO use try except) - targets = [targets] if type(targets) is int else targets - controls = [controls] if type(controls) is int else controls - classical_controls = ( - [classical_controls] - if type(classical_controls) is int - else classical_controls - ) - - # This will raise an error if not an iterable type (e.g. list, tuple, etc.) - _check_iterable("targets", targets) - _check_iterable("controls", controls) - _check_iterable("classical_controls", classical_controls) - - # Checks each element is of given type (e.g. int) and within the limit - _check_limit_("targets", targets, self.num_qubits - 1, int) - _check_limit_("controls", controls, self.num_qubits - 1, int) - _check_limit_( - "classical_controls", classical_controls, self.num_cbits - 1, int - ) - - # Check len(controls) == gate.num_ctrl_qubits - - # Default value for classical control - if len(classical_controls) > 0 and classical_control_value is None: - classical_control_value = 2 ** (len(classical_controls)) - 1 - - # This can be remove if the gate input is only restricted to Gate or its object instead of strings + # This conditional block can be remove if the gate input is only + # restricted to Gate subclasses or object instead of strings in the future. if not isinstance(gate, Gate): - if type(gate) is str and gate in GATE_CLASS_MAP: - warnings.warn( - "Passing Gate as a string input has been deprecated and will be removed in future versions.", - DeprecationWarning, - stacklevel=2 - ) - gate_class = GATE_CLASS_MAP[gate] + if type(gate) is str: + if gate in std.GATE_CLASS_MAP: + warnings.warn( + "Passing Gate as a string input has been deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=2, + ) + gate_class = std.GATE_CLASS_MAP[gate] + + else: + raise KeyError( + "Can only pass standard gate name as strings" + "or Gate class or its object instantiation" + ) elif issubclass(gate, Gate): gate_class = gate + else: - raise ValueError( - "Can only pass standard gate name as strings" - "or Gate class or its object instantiation" + raise TypeError( + "gate must be of Gate type or oject or a string ", + f"got {type(gate)} instead.", ) - if gate_class.is_controlled_gate() and gate_class.is_parametric_gate(): - gate = gate_class( - control_value=control_value, - arg_value=arg_value, - arg_label=arg_label, - ) + if gate_class.is_parametric(): + gate = gate_class(arg_value, arg_label=arg_label) - elif gate_class.is_parametric_gate(): - gate = gate_class(arg_value=arg_value, arg_label=arg_label) + else: + gate = gate_class + + # Check for gates + if inspect.isabstract(gate): + raise TypeError("gate must not be an abstract class") + elif not (isinstance(gate, Gate) or issubclass(gate, Gate)): + raise TypeError(f"gate must be of type Gate, got {gate}") + elif gate.is_parametric() and (not isinstance(gate, Gate)): + raise TypeError( + "You must pass an instantiated object for a Parametrized Gate" + ) + elif (not gate.is_parametric()) and (not issubclass(gate, Gate)): + raise TypeError( + "You must pass a Gate type for a non-parametrized gate" + ) - elif gate_class.is_controlled_gate(): - gate = gate_class(control_value=control_value) + # Handling case for integer input + targets = convert_type_input_to_sequence(int, "targets", targets) + controls = convert_type_input_to_sequence(int, "controls", controls) + classical_controls = convert_type_input_to_sequence( + int, "classical_controls", classical_controls + ) - else: - gate = gate() + # Checks each element within the limit + check_limit("targets", targets, 0, self.num_qubits - 1) + check_limit("controls", controls, 0, self.num_qubits - 1) + check_limit( + "classical_controls", classical_controls, 0, self.num_cbits - 1 + ) + + # Check len(controls) == gate.num_ctrl_qubits + if gate.is_controlled() and len(controls) != gate.num_ctrl_qubits: + raise ValueError( + f"{gate.name} takes {gate.num_ctrl_qubits} qubits, but {len(controls)} were provided." + ) + + if len(controls) + len(targets) != gate.num_qubits: + raise ValueError( + f"{gate.name} takes {gate.num_qubits} qubits, but {len(controls) + len(targets)} were provided." + ) + + # Check for classical control + default_classical_ctrl_val = 2 ** (len(classical_controls)) - 1 + + if classical_control_value is None: + if len(classical_controls) > 0: + classical_control_value = default_classical_ctrl_val + + elif not isinstance(classical_control_value, Int): + raise TypeError( + f"classical_control_value must be an integer or None, got {classical_control_value}" + ) + + elif ( + classical_control_value < 0 + or classical_control_value > default_classical_ctrl_val + ): + raise ValueError( + f"{classical_control_value} must be with [0, {default_classical_ctrl_val}]" + ) qubits = [] - if controls is not None: - qubits.extend(controls) + qubits.extend(controls) qubits.extend(targets) - cbits = tuple() - if classical_controls is not None: - cbits = tuple(classical_controls) - self._instructions.append( GateInstruction( operation=gate, qubits=tuple(qubits), - cbits=cbits, + cbits=tuple(classical_controls), cbits_ctrl_value=classical_control_value, style=style, ) @@ -527,7 +549,6 @@ def run( state, cbits=None, measure_results=None, - precompute_unitary=False, ): """ Calculate the result of one instance of circuit run. @@ -555,14 +576,11 @@ def run( mode = "density_matrix_simulator" else: raise TypeError("State is not a ket or a density matrix.") - sim = CircuitSimulator( - self, - mode, - precompute_unitary, - ) + + sim = CircuitSimulator(self, mode) return sim.run(state, cbits, measure_results).get_final_states(0) - def run_statistics(self, state, cbits=None, precompute_unitary=False): + def run_statistics(self, state, cbits=None): """ Calculate all the possible outputs of a circuit (varied by measurement gates). @@ -586,10 +604,10 @@ def run_statistics(self, state, cbits=None, precompute_unitary=False): mode = "density_matrix_simulator" else: raise TypeError("State is not a ket or a density matrix.") - sim = CircuitSimulator(self, mode, precompute_unitary) + sim = CircuitSimulator(self, mode) return sim.run_statistics(state, cbits) - def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): + def resolve_gates(self, basis=["CX", "RX", "RY", "RZ"]): """ Unitary matrix calculator for N qubits returning the individual steps as unitary matrices operating from left to right in the specified @@ -622,12 +640,19 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): raise NotImplementedError("adjacent_gates must be called before \ measurements are added to the circuit") - basis_1q_valid = ["RX", "RY", "RZ", "IDLE"] - basis_2q_valid = ["CNOT", "CX", "CSIGN", "CZ", "ISWAP", "SQRTSWAP", "SQRTISWAP"] + basis_1q_valid = ["RX", "RY", "RZ", "IDENTITY"] + basis_2q_valid = [ + "CX", + "CSIGN", + "CZ", + "ISWAP", + "SQRTSWAP", + "SQRTISWAP", + ] basis_1q = [] basis_2q = [] - if isinstance(basis, list): + if isinstance(basis, Iterable): for gate in basis: if gate in basis_2q_valid: basis_2q.append(gate) @@ -661,15 +686,15 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name in ("X", "Y", "Z"): + if gate in (std.X, std.Y, std.Z): temp_resolved.add_global_phase(phase=np.pi / 2) - if gate.name == "X": - temp_resolved.add_gate(RX(np.pi), targets=targets) - elif gate.name == "Y": - temp_resolved.add_gate(RY(np.pi), targets=targets) + if gate == std.X: + temp_resolved.add_gate(std.RX(np.pi), targets=targets) + elif gate == std.Y: + temp_resolved.add_gate(std.RY(np.pi), targets=targets) else: - temp_resolved.add_gate(RZ(np.pi), targets=targets) + temp_resolved.add_gate(std.RZ(np.pi), targets=targets) else: try: @@ -712,45 +737,45 @@ def resolve_gates(self, basis=["CNOT", "CX", "RX", "RY", "RZ"]): targets = circ_instruction.targets controls = circ_instruction.controls - if gate.name == "RX" and "RX" not in basis_1q: + if type(gate) is std.RX and "RX" not in basis_1q: qc_temp.add_gate( - RY(arg_value=-half_pi, arg_label=r"-\pi/2"), + std.RY(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RZ(arg_value=gate.arg_value, arg_label=gate.arg_label), + std.RZ(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RY(arg_value=-half_pi, arg_label=r"\pi/2"), + std.RY(-half_pi, arg_label=r"\pi/2"), targets=targets, ) - elif gate.name == "RY" and "RY" not in basis_1q: + elif type(gate) is std.RY and "RY" not in basis_1q: qc_temp.add_gate( - RZ(arg_value=-half_pi, arg_label=r"-\pi/2"), + std.RZ(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RX(arg_value=gate.arg_value, arg_label=gate.arg_label), + std.RX(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RZ(arg_value=half_pi, arg_label=r"\pi/2"), + std.RZ(half_pi, arg_label=r"\pi/2"), targets=targets, ) - elif gate.name == "RZ" and "RZ" not in basis_1q: + elif type(gate) is std.RZ and "RZ" not in basis_1q: qc_temp.add_gate( - RX(arg_value=-half_pi, arg_label=r"-\pi/2"), + std.RX(-half_pi, arg_label=r"-\pi/2"), targets=targets, ) qc_temp.add_gate( - RY(arg_value=gate.arg_value, arg_label=gate.arg_label), + std.RY(gate.arg_value[0], arg_label=gate.arg_label), targets=targets, ) qc_temp.add_gate( - RX(arg_value=half_pi, arg_label=r"\pi/2"), + std.RX(half_pi, arg_label=r"\pi/2"), targets=targets, ) else: @@ -819,7 +844,9 @@ def propagators(self, expand=True, ignore_measurement=False): # For Circuit's Global Phase qobj = Qobj([self.global_phase]) if expand: - qobj = GLOBALPHASE(self.global_phase).get_qobj(num_qubits=self.num_qubits) + qobj = std.GLOBALPHASE(self.global_phase).get_qobj( + num_qubits=self.num_qubits + ) U_list.append(qobj) return U_list @@ -934,9 +961,9 @@ def _to_qasm(self, qasm_out): object to store QASM output. """ - qasm_out.output("qreg q[{}];".format(self.num_qubits)) + qasm_out.output(f"qreg q[{self.num_qubits}];") if self.num_cbits: - qasm_out.output("creg c[{}];".format(self.num_cbits)) + qasm_out.output(f"creg c[{self.num_cbits}];") qasm_out.output(n=1) for circ_instruction in self.instructions: diff --git a/src/qutip_qip/circuit/draw/base_renderer.py b/src/qutip_qip/circuit/draw/base_renderer.py index 06304d40d..deefb886c 100644 --- a/src/qutip_qip/circuit/draw/base_renderer.py +++ b/src/qutip_qip/circuit/draw/base_renderer.py @@ -6,7 +6,7 @@ from qutip_qip.circuit.draw.color_theme import qutip, light, dark, modern -@dataclass +@dataclass(slots=True) class StyleConfig: """ Dataclass to store the style configuration for circuit customization. @@ -83,6 +83,7 @@ class StyleConfig: label_pad: float = 0.1 bulge: str | bool = True align_layer: bool = False + measure_color = "#000000" theme: str | dict = "qutip" title: str | None = None bgcolor: str | None = None @@ -91,10 +92,9 @@ class StyleConfig: wire_color: str | None = None def __post_init__(self): - if isinstance(self.bulge, bool): + if type(self.bulge) is bool: self.bulge = "round4" if self.bulge else "square" - self.measure_color = "#000000" if self.theme == "qutip": self.theme = qutip elif self.theme == "light": diff --git a/src/qutip_qip/circuit/draw/color_theme.py b/src/qutip_qip/circuit/draw/color_theme.py index df7b3cbd9..320a8523f 100644 --- a/src/qutip_qip/circuit/draw/color_theme.py +++ b/src/qutip_qip/circuit/draw/color_theme.py @@ -7,29 +7,52 @@ "color": "#FFFFFF", # White "wire_color": "#000000", # Black "default_gate": "#000000", # Black - "H": "#6270CE", # Medium Slate Blue - "SNOT": "#6270CE", # Medium Slate Blue + "IDENTITY": "#FFFFFF", # White + "IDLE": "#FFFFFF", # White "X": "#CB4BF9", # Medium Orchid "Y": "#CB4BF9", # Medium Orchid "Z": "#CB4BF9", # Medium Orchid + "H": "#6270CE", # Medium Slate Blue + "SNOT": "#6270CE", # Medium Slate Blue "S": "#254065", # Dark Slate Blue "T": "#254065", # Dark Slate Blue + "Sdag": "#254065", # Dark Slate Blue + "Tdag": "#254065", # Dark Slate Blue + "SQRTX": "#2CAC70", # Green + "SQRTXdag": "#2CAC70", # Green + "SQRTNOT": "#2CAC70", # Green "RX": "#5EBDF8", # Light Sky Blue "RY": "#5EBDF8", # Light Sky Blue "RZ": "#5EBDF8", # Light Sky Blue - "CPHASE": "#456DB2", # Steel Blue - "TOFFOLI": "#3B3470", # Indigo + "PHASE": "#456DB2", # Steel Blue + "R": "#456DB2", # Steel Blue + "QASMU": "#456DB2", # Steel Blue "SWAP": "#3B3470", # Indigo - "CNOT": "#9598F5", # Light Slate Blue + "SQRTSWAP": "#3B3470", # Indigo + "SQRTSWAPdag": "#3B3470", # Indigo + "ISWAP": "#3B3470", # Indigo + "ISWAPdag": "#3B3470", # Indigo + "SQRTISWAP": "#3B3470", # Indigo + "SQRTISWAPdag": "#3B3470", # Indigo + "BERKELEY": "#3B3470", # Indigo + "BERKELEYdag": "#3B3470", # Indigo + "SWAPALPHA": "#7648CB", # Dark Orchid + "MS": "#7648CB", # Dark Orchid + "RZX": "#7648CB", # Dark Orchid "CX": "#9598F5", # Light Slate Blue "CY": "#9598F5", # Light Slate Blue "CZ": "#9598F5", # Light Slate Blue + "CH": "#9598F5", # Light Slate Blue "CS": "#9598F5", # Light Slate Blue + "CSdag": "#9598F5", # Light Slate Blue "CT": "#9598F5", # Light Slate Blue + "CTdag": "#9598F5", # Light Slate Blue "CRX": "#A66DDF", # Medium Purple "CRY": "#A66DDF", # Medium Purple "CRZ": "#A66DDF", # Medium Purple - "BERKELEY": "#7648CB", # Dark Orchid + "CPHASE": "#A66DDF", # Medium Purple + "CQASMU": "#A66DDF", # Medium Purple + "TOFFOLI": "#3B3470", # Indigo "FREDKIN": "#7648CB", # Dark Orchid } @@ -38,30 +61,53 @@ "color": "#000000", # Black "wire_color": "#000000", # Black "default_gate": "#D8CDAF", # Bit Dark Beige - "H": "#A3C1DA", # Light Blue - "SNOT": "#A3C1DA", # Light Blue + "IDENTITY": "#FFFFFF", # White + "IDLE": "#FFFFFF", # White "X": "#F4A7B9", # Light Pink "Y": "#F4A7B9", # Light Pink "Z": "#F4A7B9", # Light Pink + "H": "#A3C1DA", # Light Blue + "SNOT": "#A3C1DA", # Light Blue "S": "#D3E2EE", # Very Light Blue "T": "#D3E2EE", # Very Light Blue + "Sdag": "#D3E2EE", # Very Light Blue + "Tdag": "#D3E2EE", # Very Light Blue + "SQRTX": "#E1E0BA", # Light Yellow + "SQRTXdag": "#E1E0BA", # Light Yellow + "SQRTNOT": "#E1E0BA", # Light Yellow "RX": "#B3E6E4", # Light Teal "RY": "#B3E6E4", # Light Teal "RZ": "#B3E6E4", # Light Teal - "CPHASE": "#D5E0F2", # Light Slate Blue - "TOFFOLI": "#E6CCE6", # Soft Lavender + "PHASE": "#D5E0F2", # Light Slate Blue + "R": "#D5E0F2", # Light Slate Blue + "QASMU": "#D5E0F2", # Light Slate Blue "SWAP": "#FFB6B6", # Lighter Coral Pink - "CNOT": "#E0E2F7", # Very Light Indigo + "SQRTSWAP": "#FFB6B6", # Lighter Coral Pink + "SQRTSWAPdag": "#FFB6B6", # Lighter Coral Pink + "ISWAP": "#FFB6B6", # Lighter Coral Pink + "ISWAPdag": "#FFB6B6", # Lighter Coral Pink + "SQRTISWAP": "#FFB6B6", # Lighter Coral Pink + "SQRTISWAPdag": "#FFB6B6", # Lighter Coral Pink + "BERKELEY": "#FFB6B6", # Lighter Coral Pink + "BERKELEYdag": "#FFB6B6", # Lighter Coral Pink + "SWAPALPHA": "#CDC1E8", # Light Purple + "MS": "#CDC1E8", # Light Purple + "RZX": "#CDC1E8", # Light Purple "CX": "#E0E2F7", # Very Light Indigo "CY": "#E0E2F7", # Very Light Indigo "CZ": "#E0E2F7", # Very Light Indigo + "CH": "#E0E2F7", # Very Light Indigo "CS": "#E0E2F7", # Very Light Indigo + "CSdag": "#E0E2F7", # Very Light Indigo "CT": "#E0E2F7", # Very Light Indigo + "CTdag": "#E0E2F7", # Very Light Indigo "CRX": "#D6C9E8", # Light Muted Purple "CRY": "#D6C9E8", # Light Muted Purple "CRZ": "#D6C9E8", # Light Muted Purple - "BERKELEY": "#CDC1E8", # Light Purple - "FREDKIN": "#CDC1E8", # Light Purple + "CPHASE": "#D6C9E8", # Light Slate Blue + "CQASMU": "#D6C9E8", # Light Slate Blue + "TOFFOLI": "#E6CCE6", # Soft Lavender + "FREDKIN": "#E6CCE6", # Soft Lavender } dark: dict[str, str] = { @@ -69,30 +115,53 @@ "color": "#000000", # Black "wire_color": "#989898", # Dark Gray "default_gate": "#D8BFD8", # (Thistle) - "H": "#AFEEEE", # Pale Turquoise - "SNOT": "#AFEEEE", # Pale Turquoise + "IDENTITY": "#FFFFFF", # White + "IDLE": "#FFFFFF", # White "X": "#9370DB", # Medium Purple "Y": "#9370DB", # Medium Purple "Z": "#9370DB", # Medium Purple "S": "#B0E0E6", # Powder Blue + "H": "#AFEEEE", # Pale Turquoise + "SNOT": "#AFEEEE", # Pale Turquoise "T": "#B0E0E6", # Powder Blue + "Sdag": "#B0E0E6", # Powder Blue + "Tdag": "#B0E0E6", # Powder Blue + "SQRTX": "#718520", # Olive Yellow + "SQRTXdag": "#718520", # Olive Yellow + "SQRTNOT": "#718520", # Olive Yellow "RX": "#87CEEB", # Sky Blue "RY": "#87CEEB", # Sky Blue "RZ": "#87CEEB", # Sky Blue - "CPHASE": "#8A2BE2", # Blue Violet - "TOFFOLI": "#DA70D6", # Orchid + "PHASE": "#8A2BE2", # Blue Violet + "R": "#8A2BE2", # Blue Violet + "QASMU": "#8A2BE2", # Blue Violet "SWAP": "#BA55D3", # Medium Orchid - "CNOT": "#4682B4", # Steel Blue + "SQRTSWAP": "#BA55D3", # Medium Orchid + "SQRTSWAPdag": "#BA55D3", # Medium Orchid + "ISWAP": "#BA55D3", # Medium Orchid + "ISWAPdag": "#BA55D3", # Medium Orchid + "SQRTISWAP": "#BA55D3", # Medium Orchid + "SQRTISWAPdag": "#BA55D3", # Medium Orchid + "BERKELEY": "#BA55D3", # Medium Orchid + "BERKELEYdag": "#BA55D3", # Medium Orchid + "SWAPALPHA": "#6A5ACD", # Slate Blue + "MS": "#6A5ACD", # Slate Blue + "RZX": "#6A5ACD", # Slate Blue "CX": "#4682B4", # Steel Blue "CY": "#4682B4", # Steel Blue "CZ": "#4682B4", # Steel Blue + "CH": "#4682B4", # Steel Blue "CS": "#4682B4", # Steel Blue + "CSdag": "#4682B4", # Steel Blue "CT": "#4682B4", # Steel Blue + "CTdag": "#4682B4", # Steel Blue "CRX": "#7B68EE", # Medium Slate Blue "CRY": "#7B68EE", # Medium Slate Blue "CRZ": "#7B68EE", # Medium Slate Blue - "BERKELEY": "#6A5ACD", # Slate Blue - "FREDKIN": "#6A5ACD", # Slate Blue + "CPHASE": "#DA70D6", # Orchid + "CQASMU": "#DA70D6", # Orchid + "TOFFOLI": "#43414F", # Dark Gray + "FREDKIN": "#43414F", # Dark Gray } @@ -101,28 +170,51 @@ "color": "#FFFFFF", # White "wire_color": "#000000", # Black "default_gate": "#ED9455", # Slate Orange - "H": "#C25454", # Soft Red - "SNOT": "#C25454", # Soft Red + "IDENTITY": "#FFFFFF", # White + "IDLE": "#FFFFFF", # White "X": "#4A5D6D", # Dark Slate Blue "Y": "#4A5D6D", # Dark Slate Blue "Z": "#4A5D6D", # Dark Slate Blue + "H": "#C25454", # Soft Red + "SNOT": "#C25454", # Soft Red "S": "#2C3E50", # Very Dark Slate Blue "T": "#2C3E50", # Very Dark Slate Blue + "Sdag": "#2C3E50", # Very Dark Slate Blue + "Tdag": "#2C3E50", # Very Dark Slate Blue + "SQRTX": "#D2E587", # Yellow + "SQRTXdag": "#D2E587", # Yellow + "SQRTNOT": "#D2E587", # Yellow "RX": "#2F4F4F", # Dark Slate Teal "RY": "#2F4F4F", # Dark Slate Teal "RZ": "#2F4F4F", # Dark Slate Teal - "CPHASE": "#5E7D8B", # Dark Slate Blue - "TOFFOLI": "#4A4A4A", # Dark Gray + "PHASE": "#5E7D8B", # Dark Slate Blue + "R": "#5E7D8B", # Dark Slate Blue + "QASMU": "#5E7D8B", # Dark Slate Blue "SWAP": "#6A9ACD", # Slate Blue - "CNOT": "#5D8AA8", # Medium Slate Blue + "SQRTSWAP": "#6A9ACD", # Slate Blue + "SQRTSWAPdag": "#6A9ACD", # Slate Blue + "ISWAP": "#6A9ACD", # Slate Blue + "ISWAPdag": "#6A9ACD", # Slate Blue + "SQRTISWAP": "#6A9ACD", # Slate Blue + "SQRTISWAPdag": "#6A9ACD", # Slate Blue + "BERKELEY": "#6A9ACD", # Slate Blue + "BERKELEYdag": "#6A9ACD", # Slate Blue + "SWAPALPHA": "#4A5D6D", # Dark Slate Blue + "MS": "#4A5D6D", # Dark Slate Blue + "RZX": "#4A5D6D", # Dark Slate Blue "CX": "#5D8AA8", # Medium Slate Blue "CY": "#5D8AA8", # Medium Slate Blue "CZ": "#5D8AA8", # Medium Slate Blue + "CH": "#5D8AA8", # Medium Slate Blue "CS": "#5D8AA8", # Medium Slate Blue + "CSdag": "#5D8AA8", # Medium Slate Blue "CT": "#5D8AA8", # Medium Slate Blue + "CTdag": "#5D8AA8", # Medium Slate Blue "CRX": "#6C5B7B", # Dark Lavender "CRY": "#6C5B7B", # Dark Lavender "CRZ": "#6C5B7B", # Dark Lavender - "BERKELEY": "#4A5D6D", # Dark Slate Blue + "CPHASE": "#4A4A4A", # Dark Gray + "CQASMU": "#4A4A4A", # Dark Gray + "TOFFOLI": "#4A5D6D", # Dark Slate Blue "FREDKIN": "#4A5D6D", # Dark Slate Blue } diff --git a/src/qutip_qip/circuit/draw/mat_renderer.py b/src/qutip_qip/circuit/draw/mat_renderer.py index 8b62159b1..4c946c134 100644 --- a/src/qutip_qip/circuit/draw/mat_renderer.py +++ b/src/qutip_qip/circuit/draw/mat_renderer.py @@ -2,6 +2,7 @@ Module for rendering a quantum circuit using matplotlib library. """ +from typing import Type import numpy as np import matplotlib.pyplot as plt from matplotlib.axes import Axes @@ -15,6 +16,7 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import BaseRenderer, StyleConfig from qutip_qip.operations import Gate +from qutip_qip.operations import gates as std class MatRenderer(BaseRenderer): @@ -520,7 +522,7 @@ def to_pi_fraction(self, value: float, tolerance: float = 0.01) -> str: def _draw_multiq_gate( self, - gate: Gate, + gate: Gate | Type[Gate], targets: list[int], controls: list[int], cbits: list[int], @@ -543,7 +545,7 @@ def _draw_multiq_gate( ) com_xskip = self._get_xskip(wire_list, layer) - if gate.name == "CNOT" or gate.name == "CX": + if gate == std.CX: self._draw_control_node(controls[0], com_xskip, self.color) self._draw_target_node(targets[0], com_xskip, self.color) self._draw_qbridge(targets[0], controls[0], com_xskip, self.color) @@ -554,7 +556,7 @@ def _draw_multiq_gate( com_xskip, ) - elif gate.name == "SWAP": + elif gate == std.SWAP: self._draw_swap_mark(targets[0], com_xskip, self.color) self._draw_swap_mark(targets[1], com_xskip, self.color) self._draw_qbridge(targets[0], targets[1], com_xskip, self.color) @@ -565,7 +567,7 @@ def _draw_multiq_gate( com_xskip, ) - elif gate.name == "TOFFOLI": + elif gate == std.TOFFOLI: self._draw_control_node(controls[0], com_xskip, self.color) self._draw_control_node(controls[1], com_xskip, self.color) self._draw_target_node(targets[0], com_xskip, self.color) @@ -784,7 +786,7 @@ def canvas_plot(self) -> None: style = style if style is not None else {} self.text = gate.name - if gate.is_parametric_gate(): + if gate.is_parametric(): self.text = ( gate.arg_label if gate.arg_label is not None diff --git a/src/qutip_qip/circuit/draw/texrenderer.py b/src/qutip_qip/circuit/draw/texrenderer.py index 5060de479..81d99790d 100644 --- a/src/qutip_qip/circuit/draw/texrenderer.py +++ b/src/qutip_qip/circuit/draw/texrenderer.py @@ -8,6 +8,7 @@ from typing import Callable from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations import gates as std # As a general note wherever you see {{}} in a python rf string that represents a {} @@ -42,7 +43,7 @@ def __init__(self, qc: QubitCircuit): def _gate_label(self, gate) -> str: gate_label = gate.latex_str - if gate.is_parametric_gate() and gate.arg_label is not None: + if gate.is_parametric() and gate.arg_label is not None: return rf"{gate_label}({gate.arg_label})" return rf"{gate_label}" @@ -71,7 +72,7 @@ def latex_code(self) -> str: for n in range(self.num_qubits + self.num_cbits): if targets and n in targets: if len(targets) > 1: - if gate.name == "SWAP": + if gate == std.SWAP: if _swap_processing: col.append(r" \qswap \qw") continue @@ -98,17 +99,17 @@ def latex_code(self) -> str: rf" \ghost{{{self._gate_label(gate)}}} " ) - elif gate.name == "CNOT" or gate.name == "CX": + elif gate == std.CX: col.append(r" \targ ") - elif gate.name == "CY": + elif gate == std.CY: col.append(r" \targ ") - elif gate.name == "CZ": + elif gate == std.CZ: col.append(r" \targ ") - elif gate.name == "CS": + elif gate == std.CS: col.append(r" \targ ") - elif gate.name == "CT": + elif gate == std.CT: col.append(r" \targ ") - elif gate.name == "TOFFOLI": + elif gate == std.TOFFOLI: col.append(r" \targ ") else: col.append(rf" \gate{{{self._gate_label(gate)}}} ") @@ -262,7 +263,7 @@ def _convert_pdf(file_stem: str, dpi: int | None = None) -> bytes: @classmethod def _make_converter( self, configuration: dict - ) -> Callable[dict, str | bytes]: + ) -> Callable[[str, int], str | bytes]: """ Create the actual conversion function of signature file_stem: str -> 'T, diff --git a/src/qutip_qip/circuit/draw/text_renderer.py b/src/qutip_qip/circuit/draw/text_renderer.py index ce5b98838..f8387d215 100644 --- a/src/qutip_qip/circuit/draw/text_renderer.py +++ b/src/qutip_qip/circuit/draw/text_renderer.py @@ -3,10 +3,12 @@ """ from math import ceil +from typing import Type from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import BaseRenderer, StyleConfig from qutip_qip.operations import Gate +from qutip_qip.operations import gates as std class TextRenderer(BaseRenderer): @@ -116,7 +118,7 @@ def _draw_singleq_gate( def _draw_multiq_gate( self, - gate: Gate, + gate: Gate | Type[Gate], gate_text: str, targets: list[int], controls: list[int], @@ -151,7 +153,7 @@ def _draw_multiq_gate( sorted_targets = sorted(targets) # Adjust top_frame or bottom if there is a control wire - if gate.is_controlled_gate(): + if gate.is_controlled(): sorted_controls = sorted(controls) top_frame = ( (top_frame[:mid_index] + "┴" + top_frame[mid_index + 1 :]) @@ -333,7 +335,7 @@ def _update_target_multiq( def _update_qbridge( self, - gate: Gate, + gate: Gate | Type[Gate], targets: list[int], controls: list[int], wire_list_control: list[int], @@ -440,10 +442,10 @@ def layout(self) -> None: targets = list(circ_instruction.targets) controls = list(circ_instruction.controls) - if gate.is_parametric_gate() and gate.arg_label is not None: + if gate.is_parametric() and gate.arg_label is not None: gate_text = gate.arg_label - if gate.name == "SWAP": + if gate == std.SWAP: wire_list = list(range(min(targets), max(targets) + 1)) width = 4 * ceil(self.style.gate_pad) + 1 else: @@ -483,7 +485,7 @@ def layout(self) -> None: self._update_cbridge(qubits, cbits, wire_list, width) elif circ_instruction.is_gate_instruction(): - if gate.name == "SWAP": + if gate == std.SWAP: self._update_swap_gate(wire_list) else: self._update_target_multiq( @@ -492,7 +494,7 @@ def layout(self) -> None: parts, ) - if gate.is_controlled_gate(): + if gate.is_controlled(): sorted_controls = sorted(controls) # check if there is control wire above the gate top diff --git a/src/qutip_qip/circuit/instruction.py b/src/qutip_qip/circuit/instruction.py index 0c76bafc8..08df01528 100644 --- a/src/qutip_qip/circuit/instruction.py +++ b/src/qutip_qip/circuit/instruction.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Type from dataclasses import dataclass, field from qutip_qip.operations import Gate, Measurement @@ -8,18 +9,18 @@ def _validate_non_negative_int_tuple(T: any, txt: str = ""): raise TypeError(f"Must pass a tuple for {txt}, got {type(T)}") for q in T: - if not isinstance(q, int): + if type(q) is not int: raise ValueError(f"All {txt} indices must be an int, found {q}") if q < 0: raise ValueError(f"{txt} indices must be non-negative, found {q}") -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class CircuitInstruction(ABC): - operation: Gate | Measurement - qubits: tuple[int] = tuple() - cbits: tuple[int] = tuple() + operation: Gate | Type[Gate] | Measurement + qubits: tuple[int, ...] = tuple() + cbits: tuple[int, ...] = tuple() style: dict = field(default_factory=dict) def __post_init__(self): @@ -56,14 +57,21 @@ def __repr__(self) -> str: return str(self) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class GateInstruction(CircuitInstruction): - operation: Gate + operation: Gate | Type[Gate] cbits_ctrl_value: int | None = None def __post_init__(self) -> None: - super().__post_init__() - if not (isinstance(self.operation, Gate) or issubclass(self.operation, Gate)): + super(GateInstruction, self).__post_init__() + # Don't make it super(), it will throw an error because slots=True + # destroys __class__ reference to the original class until Python 3.13 + # Check CPython Issue #90562, this has been resolved in Python 3.14 + + if not ( + isinstance(self.operation, Gate) + or issubclass(self.operation, Gate) + ): raise TypeError(f"Operation must be a Gate, got {self.operation}") if len(self.qubits) != self.operation.num_qubits: @@ -89,14 +97,14 @@ def __post_init__(self) -> None: ) @property - def controls(self) -> tuple[int]: - if self.operation.is_controlled_gate(): + def controls(self) -> tuple[int, ...]: + if self.operation.is_controlled(): return self.qubits[: self.operation.num_ctrl_qubits] return () @property - def targets(self) -> tuple[int]: - if self.operation.is_controlled_gate(): + def targets(self) -> tuple[int, ...]: + if self.operation.is_controlled(): return self.qubits[self.operation.num_ctrl_qubits :] return self.qubits @@ -106,7 +114,7 @@ def is_gate_instruction(self) -> bool: def to_qasm(self, qasm_out) -> None: gate = self.operation args = None - if gate.is_parametric_gate(): + if gate.is_parametric(): args = gate.arg_value qasm_gate = qasm_out.qasm_name(gate.name) @@ -132,12 +140,12 @@ def __str__(self) -> str: cbits({self.cbits}), style({self.style})" -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class MeasurementInstruction(CircuitInstruction): operation: Measurement def __post_init__(self) -> None: - super().__post_init__() + super(MeasurementInstruction, self).__post_init__() if not isinstance(self.operation, Measurement): raise TypeError( f"Operation must be a measurement, got {self.operation}" diff --git a/src/qutip_qip/circuit/simulator/__init__.py b/src/qutip_qip/circuit/simulator/__init__.py index 46297004c..d9544da2c 100644 --- a/src/qutip_qip/circuit/simulator/__init__.py +++ b/src/qutip_qip/circuit/simulator/__init__.py @@ -1,13 +1,7 @@ from .result import CircuitResult from .matrix_mul_simulator import CircuitSimulator -from .utils import ( - gate_sequence_product, - gate_sequence_product_with_expansion, -) __all__ = [ "CircuitResult", "CircuitSimulator", - "gate_sequence_product", - "gate_sequence_product_with_expansion", ] diff --git a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py index 39f17b9f3..3620fe4f4 100644 --- a/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py +++ b/src/qutip_qip/circuit/simulator/matrix_mul_simulator.py @@ -6,7 +6,6 @@ from qutip import ket2dm, Qobj from qutip_qip.circuit.simulator import CircuitResult from qutip_qip.operations import expand_operator -import warnings def _decimal_to_binary(decimal, length): @@ -35,12 +34,7 @@ class CircuitSimulator: Operator based circuit simulator. """ - def __init__( - self, - qc, - mode: str = "state_vector_simulator", - precompute_unitary: bool = False, - ): + def __init__(self, qc, mode: str = "state_vector_simulator") -> None: """ Simulate state evolution for Quantum Circuits. @@ -66,10 +60,6 @@ def __init__( self._qc = qc self.dims = qc.dims self.mode = mode - if precompute_unitary: - warnings.warn( - "Precomputing the full unitary is no longer supported. Switching to normal simulation mode." - ) @property def qc(self): @@ -265,6 +255,11 @@ def step(self): else: state = self._evolve_state(gate, qubits, current_state) + else: + raise ValueError( + f"Invalid operation {self.qc.instructions[self._op_index]}" + ) + self._state = state self._op_index += 1 diff --git a/src/qutip_qip/circuit/simulator/utils.py b/src/qutip_qip/circuit/simulator/utils.py deleted file mode 100644 index 4f52eb8a2..000000000 --- a/src/qutip_qip/circuit/simulator/utils.py +++ /dev/null @@ -1,240 +0,0 @@ -from itertools import chain -from qutip import tensor -from qutip_qip.operations import expand_operator - - -def _flatten(lst): - """ - Helper to flatten lists. - """ - - return [item for sublist in lst for item in sublist] - - -def _mult_sublists(tensor_list, overall_inds, U, inds): - """ - Calculate the revised indices and tensor list by multiplying a new unitary - U applied to inds. - - Parameters - ---------- - tensor_list : list of Qobj - List of gates (unitaries) acting on disjoint qubits. - - overall_inds : list of list of int - List of qubit indices corresponding to each gate in tensor_list. - - U: Qobj - Unitary to be multiplied with the the unitary specified by tensor_list. - - inds: list of int - List of qubit indices corresponding to U. - - Returns - ------- - tensor_list_revised: list of Qobj - List of gates (unitaries) acting on disjoint qubits incorporating U. - - overall_inds_revised: list of list of int - List of qubit indices corresponding to each gate in tensor_list_revised. - - Examples - -------- - - First, we get some imports out of the way, - - >>> from qutip_qip.operations.gates import _mult_sublists - >>> from qutip_qip.operations.gates import X, Y, Z - - Suppose we have a unitary list of already processed gates, - X, Y, Z applied on qubit indices 0, 1, 2 respectively and - encounter a new TOFFOLI gate on qubit indices (0, 1, 3). - - >>> tensor_list = [X.get_qobj(), Y.get_qobj(), Z.get_qobj()] - >>> overall_inds = [[0], [1], [2]] - >>> U = toffoli() - >>> U_inds = [0, 1, 3] - - Then, we can use _mult_sublists to produce a new list of unitaries by - multiplying TOFFOLI (and expanding) only on the qubit indices involving - TOFFOLI gate (and any multiplied gates). - - >>> U_list, overall_inds = _mult_sublists(tensor_list, overall_inds, U, U_inds) - >>> np.testing.assert_allclose(U_list[0]) == Z.get_qobj()) - >>> toffoli_xy = toffoli() * tensor(X.get_qobj(), Y.get_qobj(), identity(2)) - >>> np.testing.assert_allclose(U_list[1]), toffoli_xy) - >>> overall_inds = [[2], [0, 1, 3]] - """ - - tensor_sublist = [] - inds_sublist = [] - - tensor_list_revised = [] - overall_inds_revised = [] - - for sub_inds, sub_U in zip(overall_inds, tensor_list): - if len(set(sub_inds).intersection(inds)) > 0: - tensor_sublist.append(sub_U) - inds_sublist.append(sub_inds) - else: - overall_inds_revised.append(sub_inds) - tensor_list_revised.append(sub_U) - - inds_sublist = _flatten(inds_sublist) - U_sublist = tensor(tensor_sublist) - - revised_inds = list(set(inds_sublist).union(set(inds))) - N = len(revised_inds) - - sorted_positions = sorted(range(N), key=lambda key: revised_inds[key]) - ind_map = {ind: pos for ind, pos in zip(revised_inds, sorted_positions)} - - U_sublist = expand_operator( - U_sublist, dims=[2] * N, targets=[ind_map[ind] for ind in inds_sublist] - ) - U = expand_operator( - U, dims=[2] * N, targets=[ind_map[ind] for ind in inds] - ) - - U_sublist = U * U_sublist - inds_sublist = revised_inds - - overall_inds_revised.append(inds_sublist) - tensor_list_revised.append(U_sublist) - - return tensor_list_revised, overall_inds_revised - - -def _expand_overall(tensor_list, overall_inds): - """ - Tensor unitaries in tensor list and then use expand_operator to rearrange - them appropriately according to the indices in overall_inds. - """ - - U_overall = tensor(tensor_list) - overall_inds = _flatten(overall_inds) - U_overall = expand_operator( - U_overall, dims=[2] * len(overall_inds), targets=overall_inds - ) - overall_inds = sorted(overall_inds) - return U_overall, overall_inds - - -def gate_sequence_product(U_list, ind_list): - """ - Calculate the overall unitary matrix for a given list of unitary operations - that are still of original dimension. - - Parameters - ---------- - U_list : list of Qobj - List of gates(unitaries) implementing the quantum circuit. - - ind_list : list of list of int - List of qubit indices corresponding to each gate in tensor_list. - - Returns - ------- - U_overall : qobj - Unitary matrix corresponding to U_list. - - overall_inds : list of int - List of qubit indices on which U_overall applies. - - Examples - -------- - - First, we get some imports out of the way, - - >>> from qutip_qip.operations.gates import gate_sequence_product - >>> from qutip_qip.operations.gates import X, Y, Z, TOFFOLI - - Suppose we have a circuit with gates X, Y, Z, TOFFOLI - applied on qubit indices 0, 1, 2 and [0, 1, 3] respectively. - - >>> tensor_lst = [X.get_qobj(), Y.get_qobj(), Z.get_qobj(), TOFFOLI.get_qobj()] - >>> overall_inds = [[0], [1], [2], [0, 1, 3]] - - Then, we can use gate_sequence_product to produce a single unitary - obtained by multiplying unitaries in the list using heuristic methods - to reduce the size of matrices being multiplied. - - >>> U_list, overall_inds = gate_sequence_product(tensor_lst, overall_inds) - """ - num_qubits = len(set(chain(*ind_list))) - sorted_inds = sorted(set(_flatten(ind_list))) - ind_list = [[sorted_inds.index(ind) for ind in inds] for inds in ind_list] - - U_overall = 1 - overall_inds = [] - - for i, (U, inds) in enumerate(zip(U_list, ind_list)): - # when the tensor_list covers the full dimension of the circuit, we - # expand the tensor_list to a unitary and call gate_sequence_product - # recursively on the rest of the U_list. - if len(overall_inds) == 1 and len(overall_inds[0]) == num_qubits: - # FIXME undefined variable tensor_list - U_overall, overall_inds = _expand_overall( - tensor_list, overall_inds - ) - U_left, rem_inds = gate_sequence_product(U_list[i:], ind_list[i:]) - U_left = expand_operator( - U_left, dims=[2] * num_qubits, targets=rem_inds - ) - return U_left * U_overall, [ - sorted_inds[ind] for ind in overall_inds - ] - - # special case for first unitary in the list - if U_overall == 1: - U_overall = U_overall * U - overall_inds = [ind_list[0]] - tensor_list = [U_overall] - continue - - # case where the next unitary interacts on some subset of qubits - # with the unitaries already in tensor_list. - elif len(set(_flatten(overall_inds)).intersection(set(inds))) > 0: - tensor_list, overall_inds = _mult_sublists( - tensor_list, overall_inds, U, inds - ) - - # case where the next unitary does not interact with any unitary in - # tensor_list - else: - overall_inds.append(inds) - tensor_list.append(U) - - U_overall, overall_inds = _expand_overall(tensor_list, overall_inds) - - return U_overall, [sorted_inds[ind] for ind in overall_inds] - - -def gate_sequence_product_with_expansion(U_list, left_to_right=True): - """ - Calculate the overall unitary matrix for a given list of unitary - operations, assuming that all operations have the same dimension. - This is only for backward compatibility. - - Parameters - ---------- - U_list : list - List of gates(unitaries) implementing the quantum circuit. - - left_to_right : Boolean - Check if multiplication is to be done from left to right. - - Returns - ------- - U_overall : qobj - Unitary matrix corresponding to U_list. - """ - - U_overall = 1 - for U in U_list: - if left_to_right: - U_overall = U * U_overall - else: - U_overall = U_overall * U - - return U_overall diff --git a/src/qutip_qip/circuit/utils.py b/src/qutip_qip/circuit/utils.py deleted file mode 100644 index 2d4809d0e..000000000 --- a/src/qutip_qip/circuit/utils.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Iterable - - -def _check_iterable(input_name: str, input_value: any): - try: - iter(input_value) - except TypeError: - raise TypeError( - f"{input_name} must be an iterable input, got {input_value}." - ) - - -def _check_limit_( - input_name: str, input_value: Iterable, limit, element_type: type = int -): - for e in input_value: - if type(e) is not element_type: - raise TypeError( - f"Each entry of {input_name} must be less than {limit}, got {input_value}." - ) - - if e > limit: - raise ValueError( - f"Each entry of {input_name} must be less than {limit}, got {input_value}." - ) diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 7056464cf..4cd55e9c9 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -1,8 +1,8 @@ import numpy as np from qutip_qip.circuit import GateInstruction -from qutip_qip.operations import RZ from qutip_qip.compiler import GateCompiler, PulseInstruction +from qutip_qip.operations.gates import RX, RZ, ISWAP, SQRTISWAP, GLOBALPHASE class CavityQEDCompiler(GateCompiler): @@ -75,10 +75,10 @@ def __init__( super().__init__(num_qubits, params=params, pulse_dict=pulse_dict, N=N) self.gate_compiler.update( { - "ISWAP": self.iswap_compiler, - "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": self.rz_compiler, - "RX": self.rx_compiler, + ISWAP: self.iswap_compiler, + SQRTISWAP: self.sqrtiswap_compiler, + RZ: self.rz_compiler, + RX: self.rx_compiler, } ) self.wq = np.sqrt(self.params["eps"] ** 2 + self.params["delta"] ** 2) @@ -116,7 +116,7 @@ def _rotation_compiler( args["num_samples"], maximum=self.params[param_label][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] / 2.0 / np.pi * 0.5, + area=circuit_instruction.operation.arg_value[0] / (4.0 * np.pi), ) pulse_info = [(op_label + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] @@ -193,22 +193,18 @@ def _swap_compiler( ] # corrections - compiled_gate1 = self.gate_compiler["RZ"]( - GateInstruction( - operation=RZ(arg_value=correction_angle), qubits=(q1,) - ), + compiled_gate1 = self.gate_compiler[RZ]( + GateInstruction(operation=RZ(correction_angle), qubits=(q1,)), args, ) instruction_list += compiled_gate1 - compiled_gate2 = self.gate_compiler["RZ"]( - GateInstruction( - operation=RZ(arg_value=correction_angle), qubits=(q2,) - ), + compiled_gate2 = self.gate_compiler[RZ]( + GateInstruction(operation=RZ(correction_angle), qubits=(q2,)), args, ) instruction_list += compiled_gate2 - self.gate_compiler["GLOBALPHASE"](correction_angle) + self.gate_compiler[GLOBALPHASE](correction_angle) return instruction_list def sqrtiswap_compiler(self, circuit_instruction, args): diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 2780af9a4..6afcd3825 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -1,8 +1,8 @@ import numpy as np from qutip_qip.circuit import GateInstruction -from qutip_qip.operations import RX, RY, RZX from qutip_qip.compiler import GateCompiler, PulseInstruction +from qutip_qip.operations.gates import CX, RX, RY, RZX class SCQubitsCompiler(GateCompiler): @@ -69,7 +69,7 @@ class SCQubitsCompiler(GateCompiler): >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import ModelProcessor, SCQubitsModel >>> from qutip_qip.compiler import SCQubitsCompiler - >>> from qutip_qip.operations import CX + >>> from qutip_qip.operations.gates import CX >>> >>> qc = QubitCircuit(2) >>> qc.add_gate(CX, targets=0, controls=1) @@ -87,11 +87,10 @@ def __init__(self, num_qubits, params): super(SCQubitsCompiler, self).__init__(num_qubits, params=params) self.gate_compiler.update( { - "RY": self.ry_compiler, - "RX": self.rx_compiler, - "CX": self.cnot_compiler, - "CNOT": self.cnot_compiler, - "RZX": self.rzx_compiler, + RY: self.ry_compiler, + RX: self.rx_compiler, + CX: self.cnot_compiler, + RZX: self.rzx_compiler, } ) self.args = { # Default configuration @@ -282,28 +281,28 @@ def cnot_compiler(self, circuit_instruction, args): q2 = circuit_instruction.targets[0] # += extends a list in Python - result += self.gate_compiler["RX"]( - GateInstruction(operation=RX(arg_value=-PI / 2), qubits=(q2,)), + result += self.gate_compiler[RX]( + GateInstruction(operation=RX(-PI / 2), qubits=(q2,)), args, ) - result += self.gate_compiler["RZX"]( - GateInstruction(operation=RZX(arg_value=PI / 2), qubits=(q1, q2)), + result += self.gate_compiler[RZX]( + GateInstruction(operation=RZX(PI / 2), qubits=(q1, q2)), args, ) - result += self.gate_compiler["RX"]( - GateInstruction(operation=RX(arg_value=-PI / 2), qubits=(q1,)), + result += self.gate_compiler[RX]( + GateInstruction(operation=RX(-PI / 2), qubits=(q1,)), args, ) - result += self.gate_compiler["RY"]( - GateInstruction(operation=RY(arg_value=-PI / 2), qubits=(q1,)), + result += self.gate_compiler[RY]( + GateInstruction(operation=RY(-PI / 2), qubits=(q1,)), args, ) - result += self.gate_compiler["RX"]( - GateInstruction(operation=RX(arg_value=PI / 2), qubits=(q1,)), + result += self.gate_compiler[RX]( + GateInstruction(operation=RX(PI / 2), qubits=(q1,)), args, ) diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index bfe2451ed..e41814036 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -4,6 +4,7 @@ from qutip_qip.compiler import PulseInstruction, Scheduler from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations.gates import GLOBALPHASE, IDLE class GateCompiler: @@ -61,8 +62,8 @@ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): self._num_qubits = num_qubits # backward compatibility self.params = params if params is not None else {} self.gate_compiler = { - "GLOBALPHASE": self.globalphase_compiler, - "IDLE": self.idle_compiler, + GLOBALPHASE: self.globalphase_compiler, + IDLE: self.idle_compiler, } self.args = { # Default configuration "shape": "rectangular", @@ -81,7 +82,7 @@ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): you can simply remove it. """, UserWarning, - stacklevel=2 + stacklevel=2, ) @property @@ -117,7 +118,7 @@ def idle_compiler(self, circuit_instruction, args): """ idle_time = None gate = circuit_instruction.operation - if gate.is_parametric_gate(): + if gate.is_parametric(): idle_time = gate.arg_value return [PulseInstruction(circuit_instruction, idle_time, [])] @@ -168,10 +169,13 @@ def compile(self, circuit, schedule_mode=None, args=None): # compile gates for circuit_instruction in instructions: gate = circuit_instruction.operation - if gate.name not in self.gate_compiler: + if gate.is_parametric(): + gate = type(gate) + + if gate not in self.gate_compiler: raise ValueError(f"Unsupported gate {gate.name}") - instruction = self.gate_compiler[gate.name]( + instruction = self.gate_compiler[gate]( circuit_instruction, self.args ) if instruction is None: diff --git a/src/qutip_qip/compiler/instruction.py b/src/qutip_qip/compiler/instruction.py index b01a0b414..84ed8726b 100644 --- a/src/qutip_qip/compiler/instruction.py +++ b/src/qutip_qip/compiler/instruction.py @@ -1,6 +1,5 @@ from copy import deepcopy import numpy as np -from qutip_qip.operations import ControlledGate class PulseInstruction: @@ -86,6 +85,6 @@ def controls(self): :type: list """ - if self.gate.is_controlled_gate(): + if self.gate.is_controlled(): return self._controls return None diff --git a/src/qutip_qip/compiler/scheduler.py b/src/qutip_qip/compiler/scheduler.py index 2398303bc..7882285ba 100644 --- a/src/qutip_qip/compiler/scheduler.py +++ b/src/qutip_qip/compiler/scheduler.py @@ -426,7 +426,7 @@ def schedule( -------- >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.compiler import Scheduler - >>> from qutip_qip.operations import H, CZ, SWAP + >>> from qutip_qip.operations.gates import H, CZ, SWAP >>> circuit = QubitCircuit(7) >>> circuit.add_gate(H, targets=3) # gate0 >>> circuit.add_gate(CZ, targets=5, controls=3) # gate1 @@ -548,7 +548,7 @@ def commutation_rules(self, ind1, ind2, instructions): [instruction1, instruction2], key=lambda instruction: instruction.name, ) - if instruction1.name in ["CNOT", "CX"] and instruction2.name in ( + if instruction1.name == "CX" and instruction2.name in ( "X", "RX", ): @@ -556,7 +556,7 @@ def commutation_rules(self, ind1, ind2, instructions): commute = True else: commute = False - elif instruction1.name in ["CNOT", "CX"] and instruction2.name in ( + elif instruction1.name == "CX" and instruction2.name in ( "Z", "RZ", ): diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index e041727e0..31ec02c90 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -1,6 +1,13 @@ import numpy as np from qutip_qip.compiler import GateCompiler, PulseInstruction +from qutip_qip.operations.gates import ( + GLOBALPHASE, + ISWAP, + RX, + RZ, + SQRTISWAP, +) class SpinChainCompiler(GateCompiler): @@ -71,10 +78,11 @@ class SpinChainCompiler(GateCompiler): >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import ModelProcessor, SpinChainModel >>> from qutip_qip.compiler import SpinChainCompiler + >>> from qutip_qip.operations.gates import RX, RZ >>> >>> qc = QubitCircuit(2) - >>> qc.add_gate("RX", targets=0, arg_value=np.pi) - >>> qc.add_gate("RZ", targets=1, arg_value=np.pi) + >>> qc.add_gate(RX(np.pi), targets=0) + >>> qc.add_gate(RZ(np.pi), targets=1) >>> >>> model = SpinChainModel(2, "linear", g=0.1) >>> processor = ModelProcessor(model=model) @@ -100,11 +108,11 @@ def __init__( super().__init__(num_qubits, params=params, pulse_dict=pulse_dict, N=N) self.gate_compiler.update( { - "ISWAP": self.iswap_compiler, - "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": self.rz_compiler, - "RX": self.rx_compiler, - "GLOBALPHASE": self.globalphase_compiler, + ISWAP: self.iswap_compiler, + SQRTISWAP: self.sqrtiswap_compiler, + RZ: self.rz_compiler, + RX: self.rx_compiler, + GLOBALPHASE: self.globalphase_compiler, } ) self.global_phase = global_phase @@ -140,7 +148,10 @@ def _rotation_compiler( args["num_samples"], maximum=self.params[param_label][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] / 2.0 / np.pi * 0.5, + area=circuit_instruction.operation.arg_value[0] + / 2.0 + / np.pi + * 0.5, ) pulse_info = [(op_label + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] diff --git a/src/qutip_qip/decompose/_utility.py b/src/qutip_qip/decompose/_utility.py deleted file mode 100644 index 48553b207..000000000 --- a/src/qutip_qip/decompose/_utility.py +++ /dev/null @@ -1,25 +0,0 @@ -from qutip import Qobj - - -def check_gate(gate, num_qubits): - """Verifies input is a valid quantum gate. - - Parameters - ---------- - gate : :class:`qutip.Qobj` - The matrix that's supposed to be decomposed should be a Qobj. - num_qubits: - Total number of qubits in the circuit. - Raises - ------ - TypeError - If the gate is not a Qobj. - ValueError - If the gate is not a unitary operator on qubits. - """ - if not isinstance(gate, Qobj): - raise TypeError("The input matrix is not a Qobj.") - if not gate.isunitary: - raise ValueError("Input is not unitary.") - if gate.dims != [[2] * num_qubits] * 2: - raise ValueError(f"Input is not a unitary on {num_qubits} qubits.") diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/decompose/decompose_single_qubit_gate.py index 726add861..b2c599e0e 100644 --- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py +++ b/src/qutip_qip/decompose/decompose_single_qubit_gate.py @@ -2,9 +2,8 @@ import numpy as np import cmath -from qutip_qip.decompose._utility import check_gate - -from qutip_qip.operations import GLOBALPHASE, X, RX, RY, RZ +from qutip_qip.utils import valid_unitary +from qutip_qip.operations.gates import GLOBALPHASE, X, RX, RY, RZ class MethodError(Exception): @@ -56,20 +55,20 @@ def _ZYZ_rotation(input_gate): input_gate : :class:`qutip.Qobj` The matrix that's supposed to be decomposed should be a Qobj. """ - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) - Phase_gate = GLOBALPHASE(arg_value=global_phase_angle) + Phase_gate = GLOBALPHASE(global_phase_angle) Rz_beta = RZ( - arg_value=beta, + beta, arg_label=rf"{(beta / np.pi):0.2f} \times \pi", ) Ry_theta = RY( - arg_value=theta, + theta, arg_label=rf"{(theta / np.pi):0.2f} \times \pi", ) Rz_alpha = RZ( - arg_value=alpha, + alpha, arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) @@ -85,23 +84,23 @@ def _ZXZ_rotation(input_gate): input_gate : :class:`qutip.Qobj` The matrix that's supposed to be decomposed should be a Qobj. """ - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) alpha = alpha - np.pi / 2 beta = beta + np.pi / 2 # theta and global phase are same as ZYZ values - Phase_gate = GLOBALPHASE(arg_value=global_phase_angle) + Phase_gate = GLOBALPHASE(global_phase_angle) Rz_alpha = RZ( - arg_value=alpha, - arg_label=rf"{(alpha / np.pi):0.2f} \times \pi".format, + alpha, + arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) Rx_theta = RX( - arg_value=theta, + theta, arg_label=rf"{(theta / np.pi):0.2f} \times \pi", ) Rz_beta = RZ( - arg_value=beta, + beta, arg_label=rf"{(beta / np.pi):0.2f} \times \pi", ) @@ -114,29 +113,29 @@ def _ZXZ_rotation(input_gate): def _ZYZ_pauli_X(input_gate): """Returns a 1 qubit unitary as a product of ZYZ rotation matrices and Pauli X.""" - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) alpha, theta, beta, global_phase_angle = _angles_for_ZYZ(input_gate) - Phase_gate = GLOBALPHASE(arg_value=global_phase_angle) + Phase_gate = GLOBALPHASE(global_phase_angle) Rz_A = RZ( - arg_value=alpha, + alpha, arg_label=rf"{(alpha / np.pi):0.2f} \times \pi", ) Ry_A = RY( - arg_value=theta / 2, + theta / 2, arg_label=rf"{(theta / np.pi):0.2f} \times \pi", ) Pauli_X = X Ry_B = RY( - arg_value=-theta / 2, + -theta / 2, arg_label=rf"{(-theta / np.pi):0.2f} \times \pi", ) Rz_B = RZ( - arg_value=-(alpha + beta) / 2, + -(alpha + beta) / 2, arg_label=rf"{(-(alpha + beta) / (2 * np.pi)):0.2f} \times \pi", ) Rz_C = RZ( - arg_value=(-alpha + beta) / 2, + (-alpha + beta) / 2, arg_label=rf"{((-alpha + beta) / (2 * np.pi)):0.2f} \times \pi", ) @@ -222,7 +221,7 @@ def decompose_one_qubit_gate(input_gate, method): :math:`\textrm{B}`, 1 gates forming :math:`\textrm{C}`, and some global phase gate. """ - check_gate(input_gate, num_qubits=1) + valid_unitary(input_gate, num_qubits=1) f = _single_decompositions_dictionary.get(method, None) if f is None: raise MethodError(f"Invalid decomposition method: {method!r}") diff --git a/src/qutip_qip/device/cavityqed.py b/src/qutip_qip/device/cavityqed.py index 3427116e0..4a6b30a26 100644 --- a/src/qutip_qip/device/cavityqed.py +++ b/src/qutip_qip/device/cavityqed.py @@ -55,8 +55,8 @@ class DispersiveCavityQED(ModelProcessor): from qutip_qip.device import DispersiveCavityQED qc = QubitCircuit(2) - qc.add_gate(RX(arg_value=np.pi), targets=0) - qc.add_gate(RY(arg_value=np.pi), targets=1) + qc.add_gate(RX(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) qc.add_gate(ISWAP, targets=[1, 0]) processor = DispersiveCavityQED(2, g=0.1) diff --git a/src/qutip_qip/device/circuitqed.py b/src/qutip_qip/device/circuitqed.py index d6dafdffa..ba8871548 100644 --- a/src/qutip_qip/device/circuitqed.py +++ b/src/qutip_qip/device/circuitqed.py @@ -46,11 +46,11 @@ class SCQubits(ModelProcessor): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import SCQubits - from qutip_qip.operations import RY, RZ, CX + from qutip_qip.operations.gates import RY, RZ, CX qc = QubitCircuit(2) - qc.add_gate(RZ, targets=0, arg_value=np.pi) - qc.add_gate(RY, targets=1, arg_value=np.pi) + qc.add_gate(RZ(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) qc.add_gate(CX, targets=0, controls=1) processor = SCQubits(2) @@ -69,7 +69,7 @@ def __init__(self, num_qubits, dims=None, zz_crosstalk=False, **params): **params, ) super().__init__(model=model) - self.native_gates = ["RX", "RY", "CNOT", "CX", "RZX"] + self.native_gates = ["RX", "RY", "CX", "RZX"] self._default_compiler = SCQubitsCompiler self.pulse_mode = "continuous" diff --git a/src/qutip_qip/device/model.py b/src/qutip_qip/device/model.py index a8e35dcf2..c56c7093c 100644 --- a/src/qutip_qip/device/model.py +++ b/src/qutip_qip/device/model.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import List, Tuple, Hashable +from typing import Hashable from qutip import Qobj from qutip_qip.noise import Noise @@ -40,7 +40,7 @@ def __init__(self, num_qubits, dims=None, **params): self._noise = [] # TODO make this a property - def get_all_drift(self) -> List[Tuple[Qobj, List[int]]]: + def get_all_drift(self) -> list[tuple[Qobj, list[int]]]: """ Get all the drift Hamiltonians. @@ -52,7 +52,7 @@ def get_all_drift(self) -> List[Tuple[Qobj, List[int]]]: """ return self._drift - def get_control(self, label: Hashable) -> Tuple[Qobj, List[int]]: + def get_control(self, label: Hashable) -> tuple[Qobj, list[int]]: """ Get the control Hamiltonian corresponding to the label. @@ -67,11 +67,11 @@ def get_control(self, label: Hashable) -> Tuple[Qobj, List[int]]: The control Hamiltonian in the form of ``(qobj, targets)``. """ if hasattr(self, "_old_index_label_map"): - if isinstance(label, int): + if type(label) is int: label = self._old_index_label_map[label] return self._controls[label] - def get_control_labels(self) -> List[Hashable]: + def get_control_labels(self) -> list[Hashable]: """ Get a list of all available control Hamiltonians. Optional, required only when plotting the pulses or @@ -85,7 +85,7 @@ def get_control_labels(self) -> List[Hashable]: """ return list(self._controls.keys()) - def get_noise(self) -> List[Noise]: + def get_noise(self) -> list[Noise]: """ Get a list of :obj:`.Noise` objects. Single qubit relaxation (T1, T2) are not included here. diff --git a/src/qutip_qip/device/optpulseprocessor.py b/src/qutip_qip/device/optpulseprocessor.py index e2bfe1385..d24fa5970 100644 --- a/src/qutip_qip/device/optpulseprocessor.py +++ b/src/qutip_qip/device/optpulseprocessor.py @@ -70,7 +70,7 @@ def load_circuit( >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import OptPulseProcessor - >>> from qutip_qip.operations import H + >>> from qutip_qip.operations.gates import H >>> qc = QubitCircuit(1) >>> qc.add_gate(H, targets=0) >>> num_tslots = 10 @@ -85,7 +85,7 @@ def load_circuit( >>> from qutip_qip.circuit import QubitCircuit >>> from qutip_qip.device import OptPulseProcessor - >>> from qutip_qip.operations import H, SWAP, CX + >>> from qutip_qip.operations.gates import H, SWAP, CX >>> qc = QubitCircuit(2) >>> qc.add_gate(H, targets=0) >>> qc.add_gate(SWAP, targets=[0, 1]) @@ -201,9 +201,9 @@ def load_circuit( if result.fid_err > min_fid_err: warnings.warn( - "The fidelity error of gate {} is higher " + f"The fidelity error of gate {prop_ind} is higher " "than required limit. Use verbose=True to see" - "the more detailed information.".format(prop_ind) + "the more detailed information." ) time_record.append(result.time[1:] + last_time) @@ -211,13 +211,11 @@ def load_circuit( coeff_record.append(result.final_amps.T) if verbose: - print("********** Gate {} **********".format(prop_ind)) - print("Final fidelity error {}".format(result.fid_err)) - print( - "Final gradient normal {}".format(result.grad_norm_final) - ) - print("Terminated due to {}".format(result.termination_reason)) - print("Number of iterations {}".format(result.num_iter)) + print(f"********** Gate {prop_ind} **********") + print(f"Final fidelity error {result.fid_err}") + print(f"Final gradient normal {result.grad_norm_final}") + print(f"Terminated due to {result.termination_reason}") + print(f"Number of iterations {result.num_iter}") tlist = np.hstack([[0.0]] + time_record) for i in range(len(self.pulses)): diff --git a/src/qutip_qip/device/processor.py b/src/qutip_qip/device/processor.py index 9ddaf248b..fde81541d 100644 --- a/src/qutip_qip/device/processor.py +++ b/src/qutip_qip/device/processor.py @@ -1,10 +1,10 @@ -from collections.abc import Iterable +from collections.abc import Sequence import warnings from copy import deepcopy import numpy as np from qutip import Qobj, QobjEvo, mesolve, mcsolve -from qutip_qip.operations import GLOBALPHASE +from qutip_qip.operations.gates import GLOBALPHASE from qutip_qip.noise import Noise, process_noise from qutip_qip.device import Model from qutip_qip.device.utils import _pulse_interpolate @@ -176,7 +176,7 @@ def _get_drift_obj(self): def _unify_targets(self, qobj, targets): if targets is None: targets = list(range(len(qobj.dims[0]))) - if not isinstance(targets, Iterable): + if not isinstance(targets, Sequence): targets = [targets] return targets @@ -380,7 +380,7 @@ def coeffs(self, coeffs): self.set_coeffs(coeffs) def _generate_iterator_from_dict_or_list(self, value): - if isinstance(value, dict): + if type(value) is dict: iterator = value.items() elif isinstance(value, (list, np.ndarray)): iterator = enumerate(value) @@ -442,6 +442,7 @@ def set_tlist(self, tlist): for pulse in self.pulses: pulse.tlist = tlist return + iterator = self._generate_iterator_from_dict_or_list(tlist) pulse_dict = self.get_pulse_dict() for pulse_label, value in iterator: @@ -490,24 +491,29 @@ def get_full_coeffs(self, full_tlist=None): self._is_pulses_valid() if not self.pulses: return np.array((0, 0), dtype=float) + if full_tlist is None: full_tlist = self.get_full_tlist() + coeffs_list = [] for pulse in self.pulses: if pulse.tlist is None and pulse.coeff is None: coeffs_list.append(np.zeros(len(full_tlist))) continue + if not isinstance(pulse.coeff, (bool, np.ndarray)): raise ValueError( "get_full_coeffs only works for " "NumPy array or bool coeff." ) - if isinstance(pulse.coeff, bool): + + if type(pulse.coeff) is bool: if pulse.coeff: coeffs_list.append(np.ones(len(full_tlist))) else: coeffs_list.append(np.zeros(len(full_tlist))) continue + if self.spline_kind == "step_func": arg = {"_step_func_coeff": True} coeffs_list.append( @@ -623,7 +629,7 @@ def remove_pulse(self, indices=None, label=None): The label of the pulse """ if indices is not None: - if not isinstance(indices, Iterable): + if not isinstance(indices, Sequence): indices = [indices] indices.sort(reverse=True) for ind in indices: @@ -649,13 +655,13 @@ def _is_pulses_valid(self): continue if pulse.tlist is None: raise ValueError( - "Pulse id={} is invalid. " - "Please define a tlist for the pulse.".format(i) + f"Pulse id={i} is invalid. " + "Please define a tlist for the pulse." ) if pulse.tlist is not None and pulse.coeff is None: raise ValueError( - "Pulse id={} is invalid. " - "Please define a coeff for the pulse.".format(i) + f"Pulse id={i} is invalid. " + "Please define a coeff for the pulse." ) coeff_len = len(pulse.coeff) tlist_len = len(pulse.tlist) @@ -665,10 +671,10 @@ def _is_pulses_valid(self): else: raise ValueError( "The length of tlist and coeff of the pulse " - "labelled {} is invalid. " + f"labelled {i} is invalid. " "It's either len(tlist)=len(coeff) or " "len(tlist)-1=len(coeff) for coefficients " - "as step function".format(i) + "as step function" ) else: if coeff_len == tlist_len: @@ -676,8 +682,8 @@ def _is_pulses_valid(self): else: raise ValueError( "The length of tlist and coeff of the pulse " - "labelled {} is invalid. " - "It should be either len(tlist)=len(coeff)".format(i) + f"labelled {i} is invalid. " + "It should be either len(tlist)=len(coeff)" ) return True @@ -690,16 +696,16 @@ def get_pulse_dict(self): def find_pulse(self, pulse_name): pulse_dict = self.get_pulse_dict() - if isinstance(pulse_name, int): + if type(pulse_name) is int: return self.pulses[pulse_name] else: try: return self.pulses[pulse_dict[pulse_name]] except KeyError: raise KeyError( - "Pulse name {} undefined. " + f"Pulse name {pulse_name} undefined. " "Please define it in the attribute " - "`pulse_dict`.".format(pulse_name) + "`pulse_dict`." ) @property @@ -1110,7 +1116,7 @@ def run_state( warnings.warn( "states will be deprecated and replaced by init_state", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if init_state is None and states is None: raise ValueError("Qubit state not defined.") @@ -1120,7 +1126,7 @@ def run_state( init_state = states if analytical: if kwargs or self.noise: - raise warnings.warn( + raise warnings.warn( # FIXME this should raise an Error Type "Analytical matrices exponentiation" "does not process noise or" "any keyword arguments." diff --git a/src/qutip_qip/device/spinchain.py b/src/qutip_qip/device/spinchain.py index 1c15b6534..a6a470d36 100644 --- a/src/qutip_qip/device/spinchain.py +++ b/src/qutip_qip/device/spinchain.py @@ -124,11 +124,12 @@ class LinearSpinChain(SpinChain): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import LinearSpinChain + from qutip_qip.operations.gates import RX, RY, ISWAP qc = QubitCircuit(2) - qc.add_gate("RX", targets=0, arg_value=np.pi) - qc.add_gate("RY", targets=1, arg_value=np.pi) - qc.add_gate("ISWAP", targets=[1, 0]) + qc.add_gate(RX(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) + qc.add_gate(ISWAP, targets=[1, 0]) processor = LinearSpinChain(2, g=0.1, t1=300) processor.load_circuit(qc) @@ -208,11 +209,11 @@ class CircularSpinChain(SpinChain): import qutip from qutip_qip.circuit import QubitCircuit from qutip_qip.device import CircularSpinChain - from qutip_qip.operations import RX, RY, ISWAP + from qutip_qip.operations.gates import RX, RY, ISWAP qc = QubitCircuit(2) - qc.add_gate(RX(arg_value=np.pi), targets=0) - qc.add_gate(RY(arg_value=np.pi), targets=1) + qc.add_gate(RX(np.pi), targets=0) + qc.add_gate(RY(np.pi), targets=1) qc.add_gate(ISWAP, targets=[1, 0]) processor = CircularSpinChain(2, g=0.1, t1=300) @@ -406,13 +407,11 @@ def get_control_latex(self): num_qubits = self.num_qubits num_coupling = self._get_num_coupling() return [ - {f"sx{m}": r"$\sigma_x^{}$".format(m) for m in range(num_qubits)}, - {f"sz{m}": r"$\sigma_z^{}$".format(m) for m in range(num_qubits)}, + {f"sx{m}": rf"$\sigma_x^{m}$" for m in range(num_qubits)}, + {f"sz{m}": rf"$\sigma_z^{m}$" for m in range(num_qubits)}, { - f"g{m}": r"$\sigma_x^{}\sigma_x^{} +" - r" \sigma_y^{}\sigma_y^{}$".format( - m, (m + 1) % num_qubits, m, (m + 1) % num_qubits - ) + f"g{m}": rf"$\sigma_x^{m}\sigma_x^{(m + 1) % num_qubits} +" + rf" \sigma_y^{m}\sigma_y^{(m + 1) % num_qubits}$" for m in range(num_coupling) }, ] diff --git a/src/qutip_qip/device/utils.py b/src/qutip_qip/device/utils.py index afea75469..34a6eb222 100644 --- a/src/qutip_qip/device/utils.py +++ b/src/qutip_qip/device/utils.py @@ -20,22 +20,24 @@ def _pulse_interpolate(pulse, tlist): if pulse.tlist is None and pulse.coeff is None: coeff = np.zeros(len(tlist)) return coeff - if isinstance(pulse.coeff, bool): + + if type(pulse.coeff) is bool: if pulse.coeff: coeff = np.ones(len(tlist)) else: coeff = np.zeros(len(tlist)) return coeff + coeff = pulse.coeff if len(coeff) == len(pulse.tlist) - 1: # for discrete pulse coeff = np.concatenate([coeff, [0]]) - from scipy import interpolate - + kind = "cubic" if pulse.spline_kind == "step_func": kind = "previous" - else: - kind = "cubic" + + from scipy import interpolate + inter = interpolate.interp1d( pulse.tlist, coeff, kind=kind, bounds_error=False, fill_value=0.0 ) diff --git a/src/qutip_qip/noise/relaxation.py b/src/qutip_qip/noise/relaxation.py index 72726bd68..e0ec200e2 100644 --- a/src/qutip_qip/noise/relaxation.py +++ b/src/qutip_qip/noise/relaxation.py @@ -1,10 +1,10 @@ -import numbers -import numpy as np from collections.abc import Iterable +import numpy as np from qutip import destroy, num from qutip_qip.noise import Noise from qutip_qip.pulse import Pulse +from qutip_qip.typing import Int, IntSequence, Real, RealSequence class RelaxationNoise(Noise): @@ -37,15 +37,16 @@ class RelaxationNoise(Noise): def __init__( self, - t1: float | list[float] | None = None, - t2: float | list[float] | None = None, - targets: int | list[int] | None = None, + t1: Real | RealSequence | None = None, + t2: Real | RealSequence | None = None, + targets: Int | IntSequence | None = None, ): self.t1 = t1 self.t2 = t2 self.targets = targets - def _T_to_list(self, T: float | list[float], N: int) -> list[float]: + @staticmethod + def _T_to_list(T: Real | RealSequence, N: int) -> RealSequence: """ Check if the relaxation time is valid @@ -61,21 +62,21 @@ def _T_to_list(self, T: float | list[float], N: int) -> list[float]: T: list of float The relaxation time in Python list form """ - if (isinstance(T, numbers.Real) and T > 0) or T is None: + if (isinstance(T, Real) and T > 0) or T is None: return [T] * N elif isinstance(T, Iterable) and len(T) == N: return T else: raise ValueError( - "Invalid relaxation time T={}," + f"Invalid relaxation time T={T}," "either the length is not equal to the number of qubits, " - "or T is not a positive number.".format(T) + "or T is not a positive number." ) def get_noisy_pulses( self, - dims: list[int] | None = None, - pulses: list[Pulse] | None = None, + dims: IntSequence | None = None, + pulses: RealSequence | None = None, systematic_noise: Pulse | None = None, ) -> tuple[list[Pulse], Pulse]: """ @@ -108,7 +109,7 @@ def get_noisy_pulses( if len(self.t1) != N or len(self.t2) != N: raise ValueError( "Length of t1 or t2 does not match N, " - "len(t1)={}, len(t2)={}".format(len(self.t1), len(self.t2)) + f"len(t1)={len(self.t1)}, len(t2)={len(self.t2)}" ) if self.targets is None: @@ -129,8 +130,7 @@ def get_noisy_pulses( if t1 is not None: if 2 * t1 < t2: raise ValueError( - "t1={}, t2={} does not fulfill " - "2*t1>t2".format(t1, t2) + f"t1={t1}, t2={t2} does not fulfill " "2*t1>t2" ) T2_eff = 1.0 / (1.0 / t2 - 1.0 / 2.0 / t1) else: diff --git a/src/qutip_qip/operations/__init__.py b/src/qutip_qip/operations/__init__.py index e9c930079..3a15ffa82 100644 --- a/src/qutip_qip/operations/__init__.py +++ b/src/qutip_qip/operations/__init__.py @@ -2,7 +2,18 @@ Operations on quantum circuits. """ -from .gates import ( +from .namespace import NameSpace +from .utils import ( + hadamard_transform, + expand_operator, + gate_sequence_product, + controlled_gate_unitary, +) +from .gateclass import Gate, get_unitary_gate +from .parametric import ParametricGate, AngleParametricGate +from .controlled import ControlledGate, get_controlled_gate +from .measurement import Measurement +from .old_gates import ( rx, ry, rz, @@ -33,120 +44,19 @@ molmer_sorensen, toffoli, rotation, - controlled_gate, globalphase, - hadamard_transform, qubit_clifford_group, - expand_operator, - gate_sequence_product, -) - -from .gateclass import ( - Gate, - ControlledGate, - ParametricGate, - custom_gate_factory, - controlled_gate_factory, - AngleParametricGate, ) -from .std import ( - X, - Y, - Z, - RX, - RY, - RZ, - PHASE, - H, - SNOT, - SQRTNOT, - SQRTX, - S, - T, - R, - QASMU, - SWAP, - ISWAP, - SQRTSWAP, - SQRTISWAP, - SWAPALPHA, - BERKELEY, - MS, - RZX, - CX, - CY, - CZ, - CRX, - CRY, - CRZ, - CS, - CT, - CH, - CNOT, - CPHASE, - CSIGN, - CQASMU, - TOFFOLI, - FREDKIN, - GLOBALPHASE, - IDLE, -) -from .measurement import Measurement - -GATE_CLASS_MAP: dict[str, Gate] = { - "GLOBALPHASE": GLOBALPHASE, - "IDLE": IDLE, - "X": X, - "Y": Y, - "Z": Z, - "RX": RX, - "RY": RY, - "RZ": RZ, - "H": H, - "SNOT": SNOT, - "SQRTNOT": SQRTX, - "SQRTX": SQRTX, - "S": S, - "T": T, - "R": R, - "QASMU": QASMU, - "SWAP": SWAP, - "ISWAP": ISWAP, - "iSWAP": ISWAP, - "CNOT": CX, - "SQRTSWAP": SQRTSWAP, - "SQRTISWAP": SQRTISWAP, - "SWAPALPHA": SWAPALPHA, - "SWAPalpha": SWAPALPHA, - "BERKELEY": BERKELEY, - "MS": MS, - "TOFFOLI": TOFFOLI, - "FREDKIN": FREDKIN, - "CSIGN": CZ, - "CRX": CRX, - "CRY": CRY, - "CRZ": CRZ, - "CX": CX, - "CY": CY, - "CZ": CZ, - "CS": CS, - "CT": CT, - "CH": CH, - "CPHASE": CPHASE, - "RZX": RZX, - "CQASMU": CQASMU, -} __all__ = [ + "NameSpace", "Gate", "ParametricGate", "ControlledGate", - "custom_gate_factory", - "controlled_gate_factory", + "get_unitary_gate", + "get_controlled_gate", "AngleParametricGate", "Measurement", - "GATE_CLASS_MAP", - "GLOBALPHASE", "rx", "ry", "rz", @@ -177,49 +87,10 @@ "molmer_sorensen", "toffoli", "rotation", - "controlled_gate", + "controlled_gate_unitary", "globalphase", "hadamard_transform", "qubit_clifford_group", "expand_operator", "gate_sequence_product", - "X", - "Y", - "Z", - "RX", - "RY", - "RZ", - "PHASE", - "H", - "SNOT", - "SQRTNOT", - "SQRTX", - "S", - "T", - "R", - "QASMU", - "SWAP", - "ISWAP", - "CNOT", - "SQRTSWAP", - "SQRTISWAP", - "SWAPALPHA", - "MS", - "TOFFOLI", - "FREDKIN", - "BERKELEY", - "CNOT", - "CSIGN", - "CRX", - "CRY", - "CRZ", - "CY", - "CX", - "CZ", - "CH", - "CS", - "CT", - "CPHASE", - "RZX", - "CQASMU", ] diff --git a/src/qutip_qip/operations/controlled.py b/src/qutip_qip/operations/controlled.py new file mode 100644 index 000000000..ea7dd7f3e --- /dev/null +++ b/src/qutip_qip/operations/controlled.py @@ -0,0 +1,293 @@ +import inspect +import warnings +from abc import abstractmethod +from functools import partial +from typing import Type + +from qutip import Qobj +from qutip_qip.operations import ( + Gate, + NameSpace, + controlled_gate_unitary, +) + + +class class_or_instance_method: + """ + Binds a method to the instance if called on an instance, + or to the class if called on the class. + """ + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + # Called on the class (e.g., CX.get_qobj()) + if instance is None: + return partial(self.func, owner) + + # Called on the instance (e.g., CRX(0.5).get_qobj()) + return partial(self.func, instance) + + +class ControlledGate(Gate): + r""" + Abstract base class for controlled quantum gates. + + A controlled gate applies a target unitary operation only when the control + qubits are in a specific state. + + Attributes + ---------- + target_gate : :class:`.Gate` + The gate to be applied to the target qubits. + + num_ctrl_qubits : int + The number of qubits acting as controls. + + ctrl_value : int + The decimal value of the control state required to execute the + unitary operator on the target qubits. + + Examples: + * If the gate should execute when the 0-th qubit is $|1\rangle$, + set ``ctrl_value=1``. + * If the gate should execute when two control qubits are $|10\rangle$ + (binary 10), set ``ctrl_value=0b10``. + """ + + __slots__ = ("_target_inst",) + + num_ctrl_qubits: int + ctrl_value: int + target_gate: Type[Gate] + + def __init_subclass__(cls, **kwargs) -> None: + """ + Validates the subclass definition. + """ + + super().__init_subclass__(**kwargs) + if inspect.isabstract(cls): + return + + # Must have a target_gate + target_gate = getattr(cls, "target_gate", None) + if target_gate is None or not issubclass(target_gate, Gate): + raise TypeError( + f"Class '{cls.__name__}' attribute 'target_gate' must be a Gate class, " + f"got {type(target_gate)} with value {target_gate}." + ) + + # Check num_ctrl_qubits is a positive integer + num_ctrl_qubits = getattr(cls, "num_ctrl_qubits", None) + if (type(num_ctrl_qubits) is not int) or (num_ctrl_qubits < 1): + raise TypeError( + f"Class '{cls.__name__}' attribute 'num_ctrl_qubits' must be a postive integer, " + f"got {type(num_ctrl_qubits)} with value {num_ctrl_qubits}." + ) + + # Check num_ctrl_qubits < num_qubits + if not cls.num_ctrl_qubits < cls.num_qubits: + raise ValueError( + f"{cls.__name__}: 'num_ctrl_qubits' must be less than the 'num_qubits'" + ) + + # Check num_ctrl_qubits + target_gate.num_qubits = num_qubits + if cls.num_ctrl_qubits + cls.target_gate.num_qubits != cls.num_qubits: + raise AttributeError( + f"'num_ctrls_qubits' {cls.num_ctrl_qubits} + 'target_gate qubits' {cls.target_gate.num_qubits} must be equal to 'num_qubits' {cls.num_qubits}" + ) + cls._validate_control_value() + + # Default self_inverse + # Don't replace cls.__dict__ with hasattr() that does a MRO search + if "self_inverse" not in cls.__dict__: + cls.self_inverse = cls.target_gate.self_inverse + + # In the circuit plot, only the target gate is shown. + # The control has its own symbol. + if "latex_str" not in cls.__dict__: + cls.latex_str = cls.target_gate.latex_str + + if not cls.is_controlled(): + raise ValueError( + f"Class '{cls.name}' method 'is_controlled()' must always return True." + ) + + if cls.is_parametric() != cls.target_gate.is_parametric(): + raise ValueError( + f"Class '{cls.name}' method 'is_parametric()' must return {cls.target_gate.is_parametric()}." + ) + + def __init__(self, *args, **kwargs) -> None: + self._target_inst = self.target_gate(*args, **kwargs) + + def __getattr__(self, name: str) -> any: + """ + If an attribute (like 'arg_value') or method (like 'validate_params') + isn't found on the ControlledGate, Python falls back to this method. + We forward the request to the underlying target gate instance. + """ + return getattr(self._target_inst, name) + + def __setattr__(self, name, value) -> None: + """ + Intercept attribute assignment. If it's our internal storage variable, + set it normally on this instance. Otherwise, forward the assignment + to the underlying target gate. + """ + if name == "_target_inst": + super().__setattr__(name, value) + else: + setattr(self._target_inst, name, value) + + # Although target_gate is specified as a class attribute, It has been + # been made an abstract method to make ControlledGate abstract (required in Metaclass) + # This is because Python currently doesn't support abstract class attributes. + @property + @abstractmethod + def target_gate() -> Type[Gate]: + pass + + @classmethod + def _validate_control_value(cls) -> None: + """ + Internal validation for the control value. + + Raises + ------ + TypeError + If ctrl_value is not an integer. + ValueError + If ctrl_value is negative or exceeds the maximum value + possible for the number of control qubits ($2^N - 1$). + """ + + if type(cls.ctrl_value) is not int: + raise TypeError( + f"Control value must be an int, got {cls.ctrl_value}" + ) + + if cls.ctrl_value < 0 or cls.ctrl_value > 2**cls.num_ctrl_qubits - 1: + raise ValueError( + f"Control value can't be negative and can't be greater than " + f"2^num_ctrl_qubits - 1, got {cls.ctrl_value}" + ) + + @class_or_instance_method + def get_qobj(cls_or_self, dtype: str = "dense") -> Qobj: + """ + Construct the full Qobj representation of the controlled gate. + + Returns + ------- + qobj : qutip.Qobj + The unitary matrix representing the controlled operation. + """ + if isinstance(cls_or_self, type): + target_gate = cls_or_self.target_gate + else: + target_gate = cls_or_self._target_inst + + return controlled_gate_unitary( + U=target_gate.get_qobj(dtype), + num_controls=cls_or_self.num_ctrl_qubits, + control_value=cls_or_self.ctrl_value, + ) + + @class_or_instance_method + def inverse(cls_or_self) -> Gate | Type[Gate]: + if cls_or_self.self_inverse: + inverse_gate = cls_or_self + + # Non-parametrized Gates e.g. S + elif isinstance(cls_or_self, type): + inverse_gate = get_controlled_gate( + cls_or_self.target_gate.inverse(), + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + ) + + else: + target_inv_inst = cls_or_self._target_inst.inverse() + inverse_gate_class = type(target_inv_inst) + params = target_inv_inst.arg_value + + inverse_gate = get_controlled_gate( + inverse_gate_class, + cls_or_self.num_ctrl_qubits, + cls_or_self.ctrl_value, + )(*params) + + return inverse_gate + + @staticmethod + def is_controlled() -> bool: + return True + + @classmethod + def is_parametric(cls) -> bool: + return cls.target_gate.is_parametric() + + @classmethod + def __str__(cls) -> str: + return f"Gate({cls.name}, target_gate={cls.target_gate}, num_ctrl_qubits={cls.num_ctrl_qubits}, control_value={cls.ctrl_value})" + + def __eq__(self, other) -> bool: + # Returns false for CRX(0.5), CRY(0.5) + if type(self) is not type(other): + return False + + # Returns false for CRX(0.5), CRX(0.6) + if self.is_parametric() and self._target_inst != other._target_inst: + return False + + return True + + def __hash__(self) -> int: + if self.is_parametric(): + return hash((type(self), self._target_inst)) + return hash(type(self)) + + +def get_controlled_gate( + gate: Type[Gate], + n_ctrl_qubits: int = 1, + control_value: int | None = None, + gate_name: str | None = None, + gate_namespace: NameSpace | None = None, +) -> ControlledGate: + """ + Gate Factory for Controlled Gate that takes a gate and num_ctrl_qubits. + """ + + if control_value is None: + control_value = 2**n_ctrl_qubits - 1 + + if gate_name is None: + gate_name = f"{'C' * n_ctrl_qubits}{gate.name}" + + if gate_namespace is not None: + found_gate = gate_namespace.get( + (gate.name, n_ctrl_qubits, control_value) + ) + if found_gate is not None: + warnings.warn( + f"Found the same existing Controlled Gate {found_gate.name}", + UserWarning, + ) + return found_gate + + class _CustomControlledGate(ControlledGate): + __slots__ = () + namespace = gate_namespace + name = gate_name + latex_str = rf"{gate_name}" + + num_ctrl_qubits = n_ctrl_qubits + num_qubits = n_ctrl_qubits + gate.num_qubits + ctrl_value = control_value + target_gate = gate + + return _CustomControlledGate diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 121ea4aa2..3a1ad83de 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -1,61 +1,133 @@ -from abc import ABC, ABCMeta, abstractmethod +# annotations import won't be needed after minimum version becomes 3.14 (PEP 749) +from __future__ import annotations import inspect +from abc import ABC, ABCMeta, abstractmethod +from typing import Type import numpy as np from qutip import Qobj -from qutip_qip.operations import controlled_gate +from qutip_qip.operations.namespace import NameSpace + +_read_only_set: set[str] = set( + ( + "namespace", + "num_qubits", + "num_ctrl_qubits", + "num_params", + "ctrl_value", + "self_inverse", + "is_clifford", + "target_gate", + "latex_str", + ) +) class _GateMetaClass(ABCMeta): - """ - The purpose of this meta class is to enforce read-only constraints on specific class attributes. - - This meta class prevents critical attributes from being overwritten - after definition, while still allowing them to be set during inheritance. + def __init__(cls, name, bases, attrs): + """ + This method is automatically invoked during class creation. It validates that + the new gate class has a unique name within its specific namespace (defaulting + to "std"). If the same gate already exists in that namespace, it raises a strict + TypeError to prevent ambiguous gate definitions. + + This is required since in the codebase at several places like decomposition we + check for e.g. gate.name == 'X', which is corrupted if user defines a gate with + the same name. + """ + super().__init__(name, bases, attrs) - For example: - class X(Gate): - num_qubits = 1 # Allowed (during class creation) + # Don't register the Abstract Gate Classes or private helpers + if inspect.isabstract(cls): + cls._is_frozen = True + return - But: - X.num_qubits = 2 # Raises AttributeError (prevention of overwrite) + # _is_frozen class attribute (flag) signals class (or subclass) is built, + # don't overwrite any defaults like num_qubits etc in __setattr__. + cls._is_frozen = True + + # Namespace being None corresponds to Temporary Gates + # Only if Namespace is not None register the gate + if (namespace := getattr(cls, "namespace", None)) is not None: + + # We are checking beforehand because in case of Controlled Gate + # two key's refer to the same controlled gate: + # gate_name, (target_gate.name, num_ctrl_qubits, ctrl_value) + + # If suppose (target_gate=X, num_ctrl_qubits=1, ctrl_value=0) existed + # but we were redefining it with a different name, the cls.name insert + # step would go through, but wrt. second key won't and will throw an error. + # This will lead to leakage in the namespace i.e. classes which don't exist but are in the namespace. + if namespace.get(cls.name) is not None: + raise ValueError( + f"Existing {cls.name} in namespace {namespace}" + ) + + # The basic principle is don't define a gate class if it already exists + if cls.is_controlled(): + cls.namespace.register( + ( + cls.target_gate.name, + cls.num_ctrl_qubits, + cls.ctrl_value, + ), + cls, + ) + cls.namespace.register(cls.name, cls) - This is required since num_qubits etc. are class attributes (shared by all object instances). - """ + def __setattr__(cls, name: str, value: any) -> None: + """ + One of the main purpose of this meta class is to enforce read-only constraints + on specific class attributes. This prevents critical attributes from being + overwritten after definition, while still allowing them to be set during inheritance. - _read_only = ["num_qubits", "num_ctrl_qubits", "num_params", "target_gate", "self_inverse", "is_clifford"] - _read_only_set = set(_read_only) + For example: + class X(Gate): + num_qubits = 1 # Allowed (during class creation) - def __setattr__(cls, name: str, value: any) -> None: - for attribute in cls._read_only_set: - if name == attribute and hasattr(cls, attribute): - raise AttributeError(f"{attribute} is read-only!") - super().__setattr__(name, value) + But: + X.num_qubits = 2 # Raises AttributeError (prevention of overwrite) + + This is required since num_qubits etc. are class attributes (shared by all object instances). + """ + # cls.__dict__.get() instead of getattr() ensures we don't + # accidentally inherit the True flag from a parent class for _is_frozen. + + if cls.__dict__.get("_is_frozen", False) and name in _read_only_set: + raise AttributeError(f"{name} is read-only!") + super().__setattr__(name, value) + + def __str__(cls) -> str: + return f"Gate({cls.name})" + + def __repr__(cls) -> str: + return f"Gate({cls.name}, num_qubits={cls.num_qubits})" class Gate(ABC, metaclass=_GateMetaClass): r""" Abstract base class for a quantum gate. - Concrete gate classes or gate implementations should be defined as subclasses + Concrete gate classes or gate implementations should be defined as subclasses of this class. Attributes ---------- name : str - The name of the gate. If not manually set, this defaults to the - class name. This is a class attribute; modifying it affects all + The name of the gate. If not manually set, this defaults to the + class name. This is a class attribute; modifying it affects all instances. num_qubits : int - The number of qubits the gate acts upon. This is a mandatory + The number of qubits the gate acts upon. This is a mandatory class attribute for subclasses. self_inverse: bool Indicates if the gate is its own inverse (e.g., $U = U^{-1}$). + Default value is False. is_clifford: bool - Indicates if the gate belongs to the Clifford group, which maps + Indicates if the gate belongs to the Clifford group, which maps Pauli operators to Pauli operators. Default value is False latex_str : str @@ -63,54 +135,126 @@ class attribute for subclasses. Defaults to the class name if not provided. """ - def __init_subclass__(cls, **kwargs): + # __slots__ in Python are meant to fixed-size array of attribute values + # instead of a default dynamic sized __dict__ created in object instances. + # This helps save memory, faster lookup time & restrict adding new attributes to class. + __slots__ = () + namespace: NameSpace | None = None + + name: str + num_qubits: int + self_inverse: bool = False + is_clifford: bool = False + latex_str: str + + def __init_subclass__(cls, **kwargs) -> None: """ Automatically runs when a new subclass is defined via inheritance. - This method sets the ``name`` and ``latex_str`` attributes - if they are not defined in the subclass. It also validates that - ``num_qubits`` is a non-negative integer. + This method sets the ``name`` and ``latex_str`` attributes if + they are not defined in the subclass. It also validates that ``num_qubits`` + is a non-negative integer, ``is_clifford``, ``self_inverse`` are + bool and ``inverse`` method is not defined if ``self_inverse`` is set True. """ - super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): # Skip the below check for an abstract class - return + # Skip the below check for an abstract class + if inspect.isabstract(cls): + return super().__init_subclass__(**kwargs) # If name attribute in subclass is not defined, set it to the name of the subclass # e.g. class H(Gate): # pass - + # print(H.name) -> 'H' - + # e.g. class H(Gate): # name = "Hadamard" # pass - + # print(H.name) -> 'Hadamard' - if "name" not in cls.__dict__: + if "name" not in vars(cls): cls.name = cls.__name__ # Same as above for attribute latex_str (used in circuit draw) - if "latex_str" not in cls.__dict__: + if "latex_str" not in vars(cls): cls.latex_str = cls.__name__ # Assert num_qubits is a non-negative integer num_qubits = getattr(cls, "num_qubits", None) if (type(num_qubits) is not int) or (num_qubits < 0): raise TypeError( - f"Class '{cls.__name__}' attribute 'num_qubits' must be a non-negative integer, " + f"Class '{cls.name}' attribute 'num_qubits' must be a non-negative integer, " f"got {type(num_qubits)} with value {num_qubits}." ) - @property - @abstractmethod - def num_qubits(self) -> Qobj: - pass + # Check is_clifford is a bool + if type(cls.is_clifford) is not bool: + raise TypeError( + f"Class '{cls.name}' attribute 'is_clifford' must be a bool, " + f"got {type(cls.is_clifford)} with value {cls.is_clifford}." + ) + + # Check self_inverse is a bool + if type(cls.self_inverse) is not bool: + raise TypeError( + f"Class '{cls.name}' attribute 'self_inverse' must be a bool, " + f"got {type(cls.self_inverse)} with value {cls.self_inverse}." + ) + + # Can't define inverse() method if self_inverse is set True + if cls.self_inverse and "inverse" in cls.__dict__: + raise TypeError( + f"Gate '{cls.name}' is marked as self_inverse=True. " + f"You are not allowed to override the 'inverse()' method. " + f"Remove the method; the base class handles it automatically." + ) + + try: + param_flag = cls.is_parametric() + except TypeError as e: + raise TypeError( + f"Class '{cls.name}' must define 'is_parametric()' as a callable " + f"@staticmethod or @classmethod taking no instance arguments. " + f"Error: {e}" + ) + + if type(param_flag) is not bool: + raise TypeError( + f"Class '{cls.name}' method 'is_controlled()' must return a strict bool, " + f"got {type(param_flag)} with value {param_flag}." + ) + + try: + control_flag = cls.is_controlled() + except TypeError as e: + raise TypeError( + f"Class '{cls.name}' must define 'is_parametric()' as a callable " + f"@staticmethod or @classmethod taking no instance arguments. " + f"Error: {e}" + ) + + if type(control_flag) is not bool: + raise TypeError( + f"Class '{cls.name}' method 'is_controlled()' must return a strict bool, " + f"got {type(control_flag)} with value {control_flag}." + ) + + return super().__init_subclass__(**kwargs) + + def __init__(self) -> None: + """ + This method is overwritten in case of Parametrized and Controlled Gates. + """ + raise TypeError( + f"Gate '{type(self).name}' can't be initialised. " + f"If your gate requires parameters, it must inherit from 'ParametricGate'. " + f"Or if it must be controlled, it must inherit from 'ControlledGate'." + ) @staticmethod @abstractmethod - def get_qobj() -> Qobj: + def get_qobj(dtype: str = "dense") -> Qobj: """ Get the :class:`qutip.Qobj` representation of the gate operator. @@ -119,35 +263,27 @@ def get_qobj() -> Qobj: qobj : :obj:`qutip.Qobj` The compact gate operator as a unitary matrix. """ - pass + raise NotImplementedError - @property - def is_clifford(self) -> bool: - return False - - @property - @abstractmethod - def self_inverse(self) -> bool: - pass - - def inverse(self): + @classmethod + def inverse(cls) -> Type[Gate]: """ Return the inverse of the gate. - If ``self_inverse`` is True, returns ``self``. Otherwise, + If ``self_inverse`` is True, returns ``self``. Otherwise, returns the specific inverse gate class. Returns ------- - Gate + Type[Gate] A Gate instance representing $G^{-1}$. """ - if self.self_inverse: - return self - # Implement this via gate factory? + if cls.self_inverse: + return cls + return get_unitary_gate(f"{cls.name}_inv", cls.get_qobj().dag()) @staticmethod - def is_controlled_gate() -> bool: + def is_controlled() -> bool: """ Check if the gate is a controlled gate. @@ -158,7 +294,7 @@ def is_controlled_gate() -> bool: return False @staticmethod - def is_parametric_gate() -> bool: + def is_parametric() -> bool: """ Check if the gate accepts variable parameters (e.g., rotation angles). @@ -169,327 +305,34 @@ def is_parametric_gate() -> bool: """ return False - def __str__(self) -> str: - return f"Gate({self.name})" - - def __repr__(self) -> str: - return f"Gate({self.name}, num_qubits={self.num_qubits}, qobj={self.get_qobj()})" - - -class ParametricGate(Gate): - r""" - Abstract base class for parametric quantum gates. - - Parameters - ---------- - arg_value : float or Sequence - The argument value(s) for the gate. If a single float is provided, - it is converted to a list. These values are saved as attributes - and can be accessed or modified later. - - arg_label : str, optional - Label for the argument to be shown in the circuit plot. - - Example: - If ``arg_label="\phi"``, the LaTeX name for the gate in the circuit - plot will be rendered as ``$U(\phi)$``. - - Attributes - ---------- - num_params : int - The number of parameters required by the gate. This is a mandatory - class attribute for subclasses. - - arg_value : Sequence - The numerical values of the parameters provided to the gate. - arg_label : str, optional - The LaTeX string representing the parameter variable in circuit plots. - - Raises - ------ - ValueError - If the number of provided arguments does not match `num_params`. +def get_unitary_gate( + gate_name: str, U: Qobj, gate_namespace: NameSpace | None = None +) -> Type[Gate]: """ - - def __init_subclass__(cls, **kwargs) -> None: - """ - Validates the subclass definition. - - Ensures that `num_params` is defined as a positive integer. - """ - super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): - return - - # Assert num_params is a positive integer - num_params = getattr(cls, "num_params", None) - if (type(num_params) is not int) or (num_params < 1): - raise TypeError( - f"Class '{cls.__name__}' attribute 'num_params' must be a postive integer, " - f"got {type(num_params)} with value {num_params}." - ) - - def __init__(self, arg_value: float, arg_label: str | None = None): - if type(arg_value) is float or type(arg_value) is np.float64: - arg_value = [arg_value] - - if len(arg_value) != self.num_params: - raise ValueError(f"Requires {self.num_params} parameters, got {len(arg_value)} parameters") - - self.validate_params(arg_value) - self.arg_value = arg_value - self.arg_label = arg_label - - @property - @abstractmethod - def num_params(self) -> Qobj: - pass - - @abstractmethod - def validate_params(self, arg_value): - r""" - Validate the provided parameters. - - This method should be implemented by subclasses to check if the - parameters are valid type and within valid range (e.g., $0 \le \theta < 2\pi$). - - Parameters - ---------- - arg_value : list of float - The parameters to validate. - """ - pass - - @abstractmethod - def get_qobj(self) -> Qobj: - """ - Get the QuTiP quantum object representation using the current parameters. - - Returns - ------- - qobj : qutip.Qobj - The unitary matrix representing the gate with the specific `arg_value`. - """ - pass - - @staticmethod - def is_parametric_gate(): - return True - - def __str__(self): - return f""" - Gate({self.name}, arg_value={self.arg_value}, - arg_label={self.arg_label}), - """ - - -class ControlledGate(Gate): - r""" - Abstract base class for controlled quantum gates. - - A controlled gate applies a target unitary operation only when the control - qubits are in a specific state. - - Parameters - ---------- - control_value : int, optional - The decimal value of the control state required to execute the - unitary operator on the target qubits. - - Examples: - * If the gate should execute when the 0-th qubit is $|1\rangle$, - set ``control_value=1``. - * If the gate should execute when two control qubits are $|10\rangle$ - (binary 10), set ``control_value=2``. - - Defaults to all-ones (e.g., $2^N - 1$) if not provided. - - Attributes - ---------- - num_ctrl_qubits : int - The number of qubits acting as controls. - - target_gate : Gate - The gate to be applied to the target qubits. + Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. """ - def __init_subclass__(cls, **kwargs): - """ - Validates the subclass definition. + # Check whether U is unitary + n = np.log2(U.shape[0]) + if n != np.log2(U.shape[1]): + raise ValueError("The U must be square matrix.") - Ensures that: - 1. `num_ctrl_qubits` is a positive integer. - 2. `num_ctrl_qubits` is less than the total `num_qubits`. - 3. The sum of `num_ctrl_qubits` and `target.num_qubits` equals the total `num_qubits`. - """ + if n % 1 != 0: + raise ValueError("The unitary U must have dim NxN, where N=2^n") - super().__init_subclass__(**kwargs) - if inspect.isabstract(cls): - return - - # Assert num_ctrl_qubits is a positive integer - num_ctrl_qubits = getattr(cls, "num_ctrl_qubits", None) - if (type(num_ctrl_qubits) is not int) or (num_ctrl_qubits < 1): - raise TypeError( - f"Class '{cls.__name__}' attribute 'num_ctrl_qubits' must be a postive integer, " - f"got {type(num_ctrl_qubits)} with value {num_ctrl_qubits}." - ) - - if cls.num_ctrl_qubits >= cls.num_qubits: - raise ValueError(f"{cls.__name__}: 'num_ctrl_qubits' must be less than the 'num_qubits'") - - # Assert num_ctrl_qubits + target_gate.num_qubits = num_qubits - if cls.num_ctrl_qubits + cls.target_gate.num_qubits != cls.num_qubits: - raise AttributeError(f"'num_ctrls_qubits' {cls.num_ctrl_qubits} + 'target_gate qubits' {cls.target_gate.num_qubits} must be equal to 'num_qubits' {cls.num_qubits}") - - # Default value for control_value - cls._control_value = 2**cls.num_ctrl_qubits - 1 - - def __init__( - self, - arg_value: any = None, - control_value: int | None = None, - arg_label: str | None = None, - ) -> None: - if control_value is not None: - self._validate_control_value(control_value) - self._control_value = control_value - - if self.is_parametric_gate(): - ParametricGate.__init__(self, arg_value=arg_value, arg_label=arg_label) - - # In the circuit plot, only the target gate is shown. - # The control has its own symbol. - self.latex_str = self.target_gate.latex_str - - @property - @abstractmethod - def num_ctrl_qubits() -> int: - pass - - @property - @abstractmethod - def target_gate() -> int: - pass + if not np.allclose((U * U.dag()).full(), np.eye(U.shape[0])): + raise ValueError("U must be a unitary matrix") - @property - def self_inverse(self) -> int: - return self.target_gate.self_inverse - - @property - def control_value(self) -> int: - return self._control_value - - def _validate_control_value(self, control_value: int) -> None: - """ - Internal validation for the control value. - - Raises - ------ - TypeError - If control_value is not an integer. - ValueError - If control_value is negative or exceeds the maximum value - possible for the number of control qubits ($2^N - 1$). - """ - - if type(control_value) is not int: - raise TypeError(f"Control value must be an int, got {control_value}") - - if control_value < 0: - raise ValueError(f"Control value can't be negative, got {control_value}") - - if control_value > 2**self.num_ctrl_qubits - 1: - raise ValueError(f"Control value can't be greater than 2^num_ctrl_qubits - 1, got {control_value}") - - def get_qobj(self) -> Qobj: - """ - Construct the full Qobj representation of the controlled gate. - - Returns - ------- - qobj : qutip.Qobj - The unitary matrix representing the controlled operation. - """ - if self.is_parametric_gate(): - return controlled_gate( - U=self.target_gate(self.arg_value).get_qobj(), - control_value=self.control_value, - ) - - return controlled_gate( - U=self.target_gate.get_qobj(), - control_value=self.control_value, - ) - - @staticmethod - def is_controlled_gate() -> bool: - return True - - @classmethod - def is_parametric_gate(cls) -> bool: - return cls.target_gate.is_parametric_gate() - - def __str__(self) -> str: - return f"Gate({self.name}, target_gate={self.target_gate}, num_ctrl_qubits={self.num_ctrl_qubits}, control_value={self.control_value})" - - -def custom_gate_factory(gate_name: str, U: Qobj) -> Gate: - """ - Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. - """ - - inverse = (U == U.dag()) - - class CustomGate(Gate): - latex_str = r"U" + class _CustomGate(Gate): + __slots__ = () + namespace = gate_namespace name = gate_name - num_qubits = int(np.log2(U.shape[0])) - self_inverse = inverse - - def __init__(self): - self._U = U + num_qubits = int(n) + self_inverse = U == U.dag() @staticmethod - def get_qobj(): - return U - - return CustomGate - - -def controlled_gate_factory( - gate: Gate, - n_ctrl_qubits: int = 1, - control_value: int = -1, -) -> ControlledGate: - """ - Gate Factory for Custom Gate that wraps an arbitrary unitary matrix U. - """ - - class _CustomGate(ControlledGate): - latex_str = rf"C{gate.name}" - target_gate = gate - num_qubits = n_ctrl_qubits + target_gate.num_qubits - num_ctrl_qubits = n_ctrl_qubits - - @property - def control_value(self) -> int: - if control_value == -1: - return 2**n_ctrl_qubits - 1 - return control_value + def get_qobj(dtype=U.dtype): + return U.to(dtype) return _CustomGate - - -class AngleParametricGate(ParametricGate): - def validate_params(self, arg_value): - for arg in arg_value: - try: - float(arg) - except TypeError: - raise ValueError(f"Invalid arg {arg} in arg_value") - - @property - def self_inverse(self) -> int: - return False diff --git a/src/qutip_qip/operations/gates/__init__.py b/src/qutip_qip/operations/gates/__init__.py new file mode 100644 index 000000000..2a47dc0e2 --- /dev/null +++ b/src/qutip_qip/operations/gates/__init__.py @@ -0,0 +1,177 @@ +from .single_qubit_gate import ( + X, + Y, + Z, + RX, + RY, + RZ, + PHASE, + H, + SNOT, + SQRTNOT, + SQRTX, + SQRTXdag, + S, + Sdag, + T, + Tdag, + R, + QASMU, + IDENTITY, + IDLE, +) +from .two_qubit_gate import ( + SWAP, + ISWAP, + ISWAPdag, + SQRTSWAP, + SQRTSWAPdag, + SQRTISWAP, + SQRTISWAPdag, + BERKELEY, + BERKELEYdag, + SWAPALPHA, + MS, + RZX, + CNOT, + CX, + CY, + CZ, + CS, + CSdag, + CT, + CTdag, + CH, + CRX, + CRY, + CRZ, + CPHASE, + CSIGN, + CQASMU, +) +from .other_gates import ( + GLOBALPHASE, + TOFFOLI, + FREDKIN, +) + +GATE_CLASS_MAP = { + "GLOBALPHASE": GLOBALPHASE, + "IDENTITY": IDENTITY, + "IDLE": IDLE, + "X": X, + "Y": Y, + "Z": Z, + "RX": RX, + "RY": RY, + "RZ": RZ, + "H": H, + "SNOT": SNOT, + "SQRTNOT": SQRTNOT, + "SQRTX": SQRTX, + "SQRTXdag": SQRTXdag, + "S": S, + "Sdag": Sdag, + "T": T, + "Tdag": Tdag, + "R": R, + "QASMU": QASMU, + "SWAP": SWAP, + "ISWAP": ISWAP, + "ISWAPdag": ISWAPdag, + "SQRTSWAP": SQRTSWAP, + "SQRTSWAPdag": SQRTSWAPdag, + "SQRTISWAP": SQRTISWAP, + "SQRTISWAPdag": SQRTISWAPdag, + "BERKELEY": BERKELEY, + "BERKELEYdag": BERKELEYdag, + "SWAPALPHA": SWAPALPHA, + "MS": MS, + "TOFFOLI": TOFFOLI, + "FREDKIN": FREDKIN, + "CSIGN": CZ, + "CRX": CRX, + "CRY": CRY, + "CRZ": CRZ, + "CNOT": CX, + "CX": CX, + "CY": CY, + "CZ": CZ, + "CS": CS, + "CSdag": CSdag, + "CT": CT, + "CTdag": CTdag, + "CH": CH, + "CPHASE": CPHASE, + "RZX": RZX, + "CQASMU": CQASMU, +} + +CONTROLLED_GATE_MAP = { + X: CX, + Y: CY, + Z: CZ, + H: CH, + S: CS, + T: CT, + RX: CRX, + RY: CRY, + RZ: CRZ, + PHASE: CPHASE, + QASMU: CQASMU, +} + + +__all__ = [ + "GATE_CLASS_MAP", + "IDENTITY", + "IDLE", + "X", + "Y", + "Z", + "RX", + "RY", + "RZ", + "PHASE", + "H", + "SNOT", + "SQRTNOT", + "SQRTX", + "SQRTXdag", + "S", + "Sdag", + "T", + "Tdag", + "R", + "QASMU", + "SWAP", + "ISWAP", + "ISWAPdag", + "CNOT", + "SQRTSWAP", + "SQRTSWAPdag", + "SQRTISWAP", + "SQRTISWAPdag", + "BERKELEY", + "BERKELEYdag", + "SWAPALPHA", + "MS", + "CSIGN", + "CRX", + "CRY", + "CRZ", + "CY", + "CX", + "CZ", + "CH", + "CS", + "CSdag", + "CT", + "CTdag", + "CPHASE", + "RZX", + "CQASMU", + "GLOBALPHASE", + "TOFFOLI", + "FREDKIN", +] diff --git a/src/qutip_qip/operations/std/other_gates.py b/src/qutip_qip/operations/gates/other_gates.py similarity index 53% rename from src/qutip_qip/operations/std/other_gates.py rename to src/qutip_qip/operations/gates/other_gates.py index 25a5ee258..ab8ca692e 100644 --- a/src/qutip_qip/operations/std/other_gates.py +++ b/src/qutip_qip/operations/gates/other_gates.py @@ -1,9 +1,13 @@ +from functools import cache +from typing import Final, Type + import scipy.sparse as sp import numpy as np - from qutip import Qobj -from qutip_qip.operations import ControlledGate, AngleParametricGate -from qutip_qip.operations.std import X, SWAP + +from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate +from qutip_qip.operations.gates import X, SWAP +from qutip_qip.operations.namespace import NS_GATE class GLOBALPHASE(AngleParametricGate): @@ -12,38 +16,47 @@ class GLOBALPHASE(AngleParametricGate): Examples -------- - >>> from qutip_qip.operations import GLOBALPHASE + >>> from qutip_qip.operations.gates import GLOBALPHASE """ - num_qubits: int = 0 - num_params: int = 1 - self_inverse = False - latex_str = r"{\rm GLOBALPHASE}" + __slots__ = () + namespace = NS_GATE + + num_qubits: Final[int] = 0 + num_params: Final[int] = 1 + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm GLOBALPHASE}" - def __init__(self, arg_value: float = 0.0): - super().__init__(arg_value=arg_value) + def __init__(self, phase: float = 0.0): + super().__init__(phase) def __repr__(self): - return f"Gate({self.name}, phase {self.arg_value})" + return f"Gate({self.name}, phase {self.arg_value[0]}) -> Qobj:" - def get_qobj(self, num_qubits=None): + def compute_qobj(arg_value, dtype): + raise NotImplementedError + + def get_qobj(self, num_qubits=None, dtype: str = "dense") -> Qobj: + phase = self.arg_value[0] if num_qubits is None: - return Qobj(self.arg_value) + return Qobj(phase, dtype=dtype) N = 2**num_qubits return Qobj( - np.exp(1.0j * self.arg_value[0]) * sp.eye(N, N, dtype=complex, format="csr"), + np.exp(1.0j * phase) * sp.eye(N, N, dtype=complex, format="csr"), dims=[[2] * num_qubits, [2] * num_qubits], + dtype=dtype, ) + class TOFFOLI(ControlledGate): """ TOFFOLI gate. Examples -------- - >>> from qutip_qip.operations import TOFFOLI - >>> TOFFOLI([0, 1, 2]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> from qutip_qip.operations.gates import TOFFOLI + >>> TOFFOLI.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] @@ -56,14 +69,20 @@ class TOFFOLI(ControlledGate): [0. 0. 0. 0. 0. 0. 1. 0.]] """ - latex_str = r"{\rm TOFFOLI}" - target_gate = X + __slots__ = () + namespace = NS_GATE - num_qubits: int = 3 - num_ctrl_qubits: int = 2 + num_qubits: Final[int] = 3 + num_ctrl_qubits: Final[int] = 2 + ctrl_value: Final[int] = 0b11 + + target_gate: Final[Type[Gate]] = X + self_inverse: Final[bool] = True + latex_str: Final[str] = r"{\rm TOFFOLI}" @staticmethod - def get_qobj() -> Qobj: + @cache + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [1, 0, 0, 0, 0, 0, 0, 0], @@ -76,6 +95,7 @@ def get_qobj() -> Qobj: [0, 0, 0, 0, 0, 0, 1, 0], ], dims=[[2, 2, 2], [2, 2, 2]], + dtype=dtype, ) @@ -85,8 +105,8 @@ class FREDKIN(ControlledGate): Examples -------- - >>> from qutip_qip.operations import FREDKIN - >>> FREDKIN([0, 1, 2]).get_qobj() # doctest: +NORMALIZE_WHITESPACE + >>> from qutip_qip.operations.gates import FREDKIN + >>> FREDKIN.get_qobj() # doctest: +NORMALIZE_WHITESPACE Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=Dense, isherm=True Qobj data = [[1. 0. 0. 0. 0. 0. 0. 0.] @@ -99,14 +119,20 @@ class FREDKIN(ControlledGate): [0. 0. 0. 0. 0. 0. 0. 1.]] """ - latex_str = r"{\rm FREDKIN}" - target_gate = SWAP + __slots__ = () + namespace = NS_GATE + + num_qubits: Final[int] = 3 + num_ctrl_qubits: Final[int] = 1 + ctrl_value: Final[int] = 1 - num_qubits: int = 3 - num_ctrl_qubits: int = 1 + target_gate: Final[Type[Gate]] = SWAP + self_inverse: Final[bool] = True + latex_str: Final[str] = r"{\rm FREDKIN}" @staticmethod - def get_qobj() -> Qobj: + @cache + def get_qobj(dtype: str = "dense") -> Qobj: return Qobj( [ [1, 0, 0, 0, 0, 0, 0, 0], @@ -119,4 +145,5 @@ def get_qobj() -> Qobj: [0, 0, 0, 0, 0, 0, 0, 1], ], dims=[[2, 2, 2], [2, 2, 2]], + dtype=dtype, ) diff --git a/src/qutip_qip/operations/gates/single_qubit_gate.py b/src/qutip_qip/operations/gates/single_qubit_gate.py new file mode 100644 index 000000000..13cad7e89 --- /dev/null +++ b/src/qutip_qip/operations/gates/single_qubit_gate.py @@ -0,0 +1,637 @@ +from functools import cache, lru_cache +from typing import Final, Type +import warnings +import numpy as np + +from qutip import Qobj, sigmax, sigmay, sigmaz, qeye +from qutip_qip.operations import Gate, AngleParametricGate +from qutip_qip.operations.namespace import NS_GATE, NameSpace +from qutip_qip.typing import Real + + +class _SingleQubitGate(Gate): + """Abstract one-qubit gate.""" + + __slots__ = () + namespace: NameSpace = NS_GATE + num_qubits: Final[int] = 1 + + +class _SingleQubitParametricGate(AngleParametricGate): + """Abstract one-qubit parametric gate.""" + + __slots__ = () + namespace: NameSpace = NS_GATE + num_qubits: Final[int] = 1 + + +class X(_SingleQubitGate): + """ + Single-qubit X gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import X + >>> X.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True + Qobj data = + [[0. 1.] + [1. 0.]] + """ + + __slots__ = () + + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"X" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return sigmax(dtype=dtype) + + +class Y(_SingleQubitGate): + """ + Single-qubit Y gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import Y + >>> Y.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True + Qobj data = + [[0.+0.j 0.-1.j] + [0.+1.j 0.+0.j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"Y" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return sigmay(dtype=dtype) + + +class Z(_SingleQubitGate): + """ + Single-qubit Z gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import Z + >>> Z.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True + Qobj data = + [[ 1. 0.] + [ 0. -1.]] + """ + + __slots__ = () + + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"Z" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return sigmaz(dtype=dtype) + + +class IDENTITY(_SingleQubitGate): + """ + IDENTITY gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import IDENTITY + """ + + __slots__ = () + + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm I}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return qeye(2, dtype=dtype) + + +class H(_SingleQubitGate): + """ + Hadamard gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import H + >>> H.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True + Qobj data = + [[ 0.70711 0.70711] + [ 0.70711 -0.70711]] + """ + + __slots__ = () + + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"H" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + sq_half = 1 / np.sqrt(2.0) + return Qobj([[sq_half, sq_half], [sq_half, -sq_half]], dtype=dtype) + + +SNOT = H + + +class SQRTX(_SingleQubitGate): + r""" + :math:`\sqrt{X}` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SQRTX + >>> SQRTX.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.5+0.5j 0.5-0.5j] + [0.5-0.5j 0.5+0.5j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{\rm X}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]], dtype=dtype + ) + + @staticmethod + def inverse() -> Type[Gate]: + return SQRTXdag + + +class SQRTXdag(_SingleQubitGate): + r""" + :math:`\sqrt{X}^\dagger` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SQRTXdag + >>> SQRTXdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.5-0.5j 0.5+0.5j] + [0.5+0.5j 0.5-0.5j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{\rm X}^\dagger" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[0.5 - 0.5j, 0.5 + 0.5j], [0.5 + 0.5j, 0.5 - 0.5j]], dtype=dtype + ) + + @staticmethod + def inverse() -> Type[Gate]: + return SQRTX + + +class SQRTNOT(SQRTX): + __slots__ = () + + def __init__(self): + warnings.warn( + "SQRTNOT is deprecated and will be removed in future versions. " + "Use SQRTX instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__() + + +class S(_SingleQubitGate): + r""" + S gate or :math:`\sqrt{Z}` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import S + >>> S.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j] + [0.+0.j 0.+1.j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm S}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, 1j]], dtype=dtype) + + @staticmethod + def inverse() -> Type[Gate]: + return Sdag + + +class Sdag(_SingleQubitGate): + r""" + S gate or :math:`\sqrt{Z}` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import S + >>> Sdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j] + [0.+0.j 0.-1.j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm S^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, -1j]], dtype=dtype) + + @staticmethod + def inverse() -> Type[Gate]: + return S + + +class T(_SingleQubitGate): + r""" + T gate or :math:`\sqrt[4]{Z}` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import T + >>> T.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j ] + [0. +0.j 0.70711+0.70711j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm T}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]], dtype=dtype) + + @staticmethod + def inverse() -> Type[Gate]: + return Tdag + + +class Tdag(_SingleQubitGate): + r""" + Tdag gate or :math:`\sqrt[4]{Z}^\dagger` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import Tdag + >>> Tdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j ] + [0. +0.j 0.70711-0.70711j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm Tdag}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj([[1, 0], [0, np.exp(-1j * np.pi / 4)]], dtype=dtype) + + @staticmethod + def inverse() -> Type[Gate]: + return T + + +class RX(_SingleQubitParametricGate): + """ + Single-qubit rotation RX. + + Examples + -------- + >>> from qutip_qip.operations.gates import RX + >>> RX(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.70711+0.j 0. -0.70711j] + [0. -0.70711j 0.70711+0.j ]] + """ + + __slots__ = () + + num_params: Final[int] = 1 + latex_str: Final[str] = r"R_x" + + def __init__(self, theta: float, arg_label=None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + phi = arg_value[0] + return Qobj( + [ + [np.cos(phi / 2), -1j * np.sin(phi / 2)], + [-1j * np.sin(phi / 2), np.cos(phi / 2)], + ], + dims=[[2], [2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + return RX(-theta) + + +class RY(_SingleQubitParametricGate): + """ + Single-qubit rotation RY. + + Examples + -------- + >>> from qutip_qip.operations.gates import RY + >>> RY(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[ 0.70711 -0.70711] + [ 0.70711 0.70711]] + """ + + __slots__ = () + + num_params: Final[int] = 1 + latex_str: Final[str] = r"R_y" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + phi = arg_value[0] + return Qobj( + [ + [np.cos(phi / 2), -np.sin(phi / 2)], + [np.sin(phi / 2), np.cos(phi / 2)], + ], + dims=[[2], [2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + return RY(-theta) + + +class RZ(_SingleQubitParametricGate): + """ + Single-qubit rotation RZ. + + Examples + -------- + >>> from qutip_qip.operations.gates import RZ + >>> RZ(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.70711-0.70711j 0. +0.j ] + [0. +0.j 0.70711+0.70711j]] + """ + + __slots__ = () + + num_params: Final[int] = 1 + latex_str: Final[str] = r"R_z" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + phi = arg_value[0] + return Qobj( + [[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]], + dims=[[2], [2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + return RZ(-theta) + + +class PHASE(_SingleQubitParametricGate): + """ + PHASE Gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import PHASE + """ + + __slots__ = () + + num_params: Final[int] = 1 + latex_str: Final[str] = r"PHASE" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + phi = arg_value[0] + return Qobj( + [[1, 0], [0, np.exp(1j * phi)]], + dims=[[2], [2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + return PHASE(-theta) + + +class R(_SingleQubitParametricGate): + r""" + Arbitrary single-qubit rotation + + .. math:: + + \begin{pmatrix} + \cos(\frac{\theta}{2}) & -ie^{-i\phi} \sin(\frac{\theta}{2}) \\ + -ie^{i\phi} \sin(\frac{\theta}{2}) & \cos(\frac{\theta}{2}) + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import R + >>> R(np.pi/2, np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[ 0.70711 -0.70711] + [ 0.70711 0.70711]] + """ + + __slots__ = () + + num_params: Final[int] = 2 + latex_str: Final[str] = r"{\rm R}" + + def __init__(self, phi: float, theta: float, arg_label: str | None = None): + super().__init__(phi, theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float, float], dtype: str) -> Qobj: + phi, theta = arg_value + return Qobj( + [ + [ + np.cos(theta / 2.0), + -1.0j * np.exp(-1.0j * phi) * np.sin(theta / 2.0), + ], + [ + -1.0j * np.exp(1.0j * phi) * np.sin(theta / 2.0), + np.cos(theta / 2.0), + ], + ], + dims=[[2], [2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float]]: + phi, theta = self.arg_value + return R(phi, -theta) + + +class QASMU(_SingleQubitParametricGate): + r""" + QASMU gate. + + .. math:: + U(\theta, \phi, \gamma) = RZ(\phi) RY(\theta) RZ(\gamma) + + Examples + -------- + >>> from qutip_qip.operations.gates import QASMU + >>> QASMU(0, (np.pi/2, np.pi, np.pi/2)).get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False + Qobj data = + [[-0.5-0.5j -0.5+0.5j] + [ 0.5+0.5j -0.5+0.5j]] + """ + + __slots__ = () + + num_params: Final[int] = 3 + latex_str: Final[str] = r"{\rm QASMU}" + + def __init__( + self, + theta: float, + phi: float, + gamma: float, + arg_label: str | None = None, + ): + super().__init__(theta, phi, gamma, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + theta, phi, gamma = arg_value + return Qobj( + [ + [ + np.exp(-1j * (phi + gamma) / 2) * np.cos(theta / 2), + -np.exp(-1j * (phi - gamma) / 2) * np.sin(theta / 2), + ], + [ + np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), + np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2), + ], + ], + dims=[[2], [2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float, float]]: + theta, phi, gamma = self.arg_value + return QASMU(-theta, -gamma, -phi) + + +class IDLE(AngleParametricGate): + """ + IDLE gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import IDLE + """ + + __slots__ = () + num_qubits = 1 + num_params = 1 + + def __init__(self, T: float, arg_label=None): + super().__init__(T, arg_label=arg_label) + + @staticmethod + def validate_params(args): + if not isinstance(args[0], Real): + raise TypeError(f"{args[0]} must be a float") + if args[0] < 0: + raise ValueError(f"IDLE time must be non-negative, got {args[0]}") + + @staticmethod + def compute_qobj(args, dtype: str = "dense") -> Qobj: + # Practically not required as this gate is only useful in pulse level + # simulation, and the pulse compiler implementation of it will be + # independent of get_qobj() + return qeye(2, dtype=dtype) diff --git a/src/qutip_qip/operations/gates/two_qubit_gate.py b/src/qutip_qip/operations/gates/two_qubit_gate.py new file mode 100644 index 000000000..c2c2c2c67 --- /dev/null +++ b/src/qutip_qip/operations/gates/two_qubit_gate.py @@ -0,0 +1,1165 @@ +from typing import Final, Type +from functools import cache, lru_cache + +import numpy as np +from qutip import Qobj + +from qutip_qip.operations import Gate, ControlledGate, AngleParametricGate +from qutip_qip.operations.gates import ( + X, + Y, + Z, + H, + S, + Sdag, + T, + Tdag, + RX, + RY, + RZ, + QASMU, + PHASE, +) +from qutip_qip.operations.namespace import NS_GATE + + +class _TwoQubitGate(Gate): + """Abstract two-qubit gate.""" + + __slots__ = () + namespace = NS_GATE + num_qubits: Final[int] = 2 + + +class _TwoQubitParametricGate(AngleParametricGate): + """Abstract two-qubit Parametric Gate (non-controlled).""" + + __slots__ = () + namespace = NS_GATE + num_qubits: Final[int] = 2 + + +class _ControlledTwoQubitGate(ControlledGate): + """Abstract two-qubit Controlled Gate (both parametric and non-parametric).""" + + __slots__ = () + namespace = NS_GATE + + num_qubits: Final[int] = 2 + num_ctrl_qubits: Final[int] = 1 + ctrl_value: Final[int] = 1 + + +class SWAP(_TwoQubitGate): + """ + SWAP gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SWAP + >>> SWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True + Qobj data = + [[1. 0. 0. 0.] + [0. 0. 1. 0.] + [0. 1. 0. 0.] + [0. 0. 0. 1.]] + """ + + __slots__ = () + + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm SWAP}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + +class ISWAP(_TwoQubitGate): + """ + iSWAP gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import ISWAP + >>> ISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+1.j 0.+0.j] + [0.+0.j 0.+1.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 1.+0.j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{i}{\rm SWAP}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return ISWAPdag + + +class ISWAPdag(_TwoQubitGate): + """ + iSWAPdag gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import ISWAPdag + >>> ISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.-1.j 0.+0.j] + [0.+0.j 0.-1.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 1.+0.j]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{i}{\rm SWAP^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 0, -1j, 0], [0, -1j, 0, 0], [0, 0, 0, 1]], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return ISWAP + + +class SQRTSWAP(_TwoQubitGate): + r""" + :math:`\sqrt{\mathrm{SWAP}}` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SQRTSWAP + >>> SQRTSWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.5+0.5j 0.5-0.5j 0. +0.j ] + [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + latex_str: Final[str] = r"\sqrt{\rm SWAP}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [ + [1, 0, 0, 0], + [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], + [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], + [0, 0, 0, 1], + ] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return SQRTSWAPdag + + +class SQRTSWAPdag(_TwoQubitGate): + r""" + :math:`\sqrt{\mathrm{SWAP}}^\dagger` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SQRTSWAPdag + >>> SQRTSWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] + [0. +0.j 0.5+0.5j 0.5-0.5j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + latex_str: Final[str] = r"\sqrt{\rm SWAP}^\dagger" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [ + [1, 0, 0, 0], + [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], + [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], + [0, 0, 0, 1], + ] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return SQRTSWAP + + +class SQRTISWAP(_TwoQubitGate): + r""" + :math:`\sqrt{\mathrm{iSWAP}}` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SQRTISWAP + >>> SQRTISWAP.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.70711+0.j 0. +0.70711j 0. +0.j ] + [0. +0.j 0. +0.70711j 0.70711+0.j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{{i}\rm SWAP}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [ + [1, 0, 0, 0], + [0, 1 / np.sqrt(2), 1j / np.sqrt(2), 0], + [0, 1j / np.sqrt(2), 1 / np.sqrt(2), 0], + [0, 0, 0, 1], + ] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return SQRTISWAPdag + + +class SQRTISWAPdag(_TwoQubitGate): + r""" + :math:`\sqrt{\mathrm{iSWAP}}^\dagger` gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import SQRTISWAPdag + >>> SQRTISWAPdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.70711+0.j 0. -0.70711j 0. +0.j ] + [0. +0.j 0. -0.70711j 0.70711+0.j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + is_clifford: Final[bool] = True + latex_str: Final[str] = r"\sqrt{{i}\rm SWAP}^\dagger" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [ + [1, 0, 0, 0], + [0, 1 / np.sqrt(2), -1j / np.sqrt(2), 0], + [0, -1j / np.sqrt(2), 1 / np.sqrt(2), 0], + [0, 0, 0, 1], + ] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return SQRTISWAP + + +class BERKELEY(_TwoQubitGate): + r""" + BERKELEY gate. + + .. math:: + + \begin{pmatrix} + \cos(\frac{\pi}{8}) & 0 & 0 & i\sin(\frac{\pi}{8}) \\ + 0 & \cos(\frac{3\pi}{8}) & i\sin(\frac{3\pi}{8}) & 0 \\ + 0 & i\sin(\frac{3\pi}{8}) & \cos(\frac{3\pi}{8}) & 0 \\ + i\sin(\frac{\pi}{8}) & 0 & 0 & \cos(\frac{\pi}{8}) + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import BERKELEY + >>> BERKELEY.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.92388+0.j 0. +0.j 0. +0.j 0. +0.38268j] + [0. +0.j 0.38268+0.j 0. +0.92388j 0. +0.j ] + [0. +0.j 0. +0.92388j 0.38268+0.j 0. +0.j ] + [0. +0.38268j 0. +0.j 0. +0.j 0.92388+0.j ]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm BERKELEY}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [ + [np.cos(np.pi / 8), 0, 0, 1.0j * np.sin(np.pi / 8)], + [0, np.cos(3 * np.pi / 8), 1.0j * np.sin(3 * np.pi / 8), 0], + [0, 1.0j * np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], + [1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return BERKELEYdag + + +class BERKELEYdag(_TwoQubitGate): + r""" + BERKELEY gate. + + .. math:: + + \begin{pmatrix} + \cos(\frac{\pi}{8}) & 0 & 0 & i\sin(\frac{\pi}{8}) \\ + 0 & \cos(\frac{3\pi}{8}) & i\sin(\frac{3\pi}{8}) & 0 \\ + 0 & i\sin(\frac{3\pi}{8}) & \cos(\frac{3\pi}{8}) & 0 \\ + i\sin(\frac{\pi}{8}) & 0 & 0 & \cos(\frac{\pi}{8}) + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import BERKELEYdag + >>> BERKELEYdag.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.92388+0.j 0. +0.j 0. +0.j 0. -0.38268j] + [0. +0.j 0.38268+0.j 0. -0.92388j 0. +0.j ] + [0. +0.j 0. -0.92388j 0.38268+0.j 0. +0.j ] + [0. -0.38268j 0. +0.j 0. +0.j 0.92388+0.j ]] + """ + + __slots__ = () + + self_inverse: Final[bool] = False + latex_str: Final[str] = r"{\rm BERKELEY^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [ + [np.cos(np.pi / 8), 0, 0, -1.0j * np.sin(np.pi / 8)], + [0, np.cos(3 * np.pi / 8), -1.0j * np.sin(3 * np.pi / 8), 0], + [0, -1.0j * np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], + [-1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return BERKELEY + + +class SWAPALPHA(_TwoQubitParametricGate): + r""" + SWAPALPHA gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \frac{1 + e^{i\pi\alpha}}{2} & \frac{1 - e^{i\pi\alpha}}{2} & 0 \\ + 0 & \frac{1 - e^{i\pi\alpha}}{2} & \frac{1 + e^{i\pi\alpha}}{2} & 0 \\ + 0 & 0 & 0 & 1 + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import SWAPALPHA + >>> SWAPALPHA(0.5).get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] + [0. +0.j 0.5+0.5j 0.5-0.5j 0. +0.j ] + [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] + [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] + """ + + __slots__ = "alpha" + + num_params: Final[int] = 1 + latex_str: Final[str] = r"{\rm SWAPALPHA}" + + def __init__(self, alpha: float, arg_label: str | None = None): + super().__init__(alpha, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + alpha = arg_value[0] + return Qobj( + [ + [1, 0, 0, 0], + [ + 0, + 0.5 * (1 + np.exp(1.0j * np.pi * alpha)), + 0.5 * (1 - np.exp(1.0j * np.pi * alpha)), + 0, + ], + [ + 0, + 0.5 * (1 - np.exp(1.0j * np.pi * alpha)), + 0.5 * (1 + np.exp(1.0j * np.pi * alpha)), + 0, + ], + [0, 0, 0, 1], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: + alpha = self.arg_value[0] + return SWAPALPHA(-alpha) + + +class MS(_TwoQubitParametricGate): + r""" + Mølmer–Sørensen gate. + + .. math:: + + \begin{pmatrix} + \cos(\frac{\theta}{2}) & 0 & 0 & -ie^{-i2\phi}\sin(\frac{\theta}{2}) \\ + 0 & \cos(\frac{\theta}{2}) & -i\sin(\frac{\theta}{2}) & 0 \\ + 0 & -i\sin(\frac{\theta}{2}) & \cos(\frac{\theta}{2}) & 0 \\ + -ie^{i2\phi}\sin(\frac{\theta}{2}) & 0 & 0 & \cos(\frac{\theta}{2}) + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import MS + >>> MS((np.pi/2, 0)).get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.70711+0.j 0. +0.j 0. +0.j 0. -0.70711j] + [0. +0.j 0.70711+0.j 0. -0.70711j 0. +0.j ] + [0. +0.j 0. -0.70711j 0.70711+0.j 0. +0.j ] + [0. -0.70711j 0. +0.j 0. +0.j 0.70711+0.j ]] + """ + + __slots__ = ("theta", "phi") + + num_params: Final[int] = 2 + latex_str: Final[str] = r"{\rm MS}" + + def __init__(self, theta: float, phi: float, arg_label: str | None = None): + super().__init__(theta, phi, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float, float], dtype: str) -> Qobj: + theta, phi = arg_value + return Qobj( + [ + [ + np.cos(theta / 2), + 0, + 0, + -1j * np.exp(-1j * 2 * phi) * np.sin(theta / 2), + ], + [0, np.cos(theta / 2), -1j * np.sin(theta / 2), 0], + [0, -1j * np.sin(theta / 2), np.cos(theta / 2), 0], + [ + -1j * np.exp(1j * 2 * phi) * np.sin(theta / 2), + 0, + 0, + np.cos(theta / 2), + ], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float, float]]: + theta, phi = self.arg_value + return MS(-theta, phi) + + +class RZX(_TwoQubitParametricGate): + r""" + RZX gate. + + .. math:: + + \begin{pmatrix} + \cos{\theta/2} & -i\sin{\theta/2} & 0 & 0 \\ + -i\sin{\theta/2} & \cos{\theta/2} & 0 & 0 \\ + 0 & 0 & \cos{\theta/2} & i\sin{\theta/2} \\ + 0 & 0 & i\sin{\theta/2} & \cos{\theta/2} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import RZX + >>> RZX(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[0.+0.j 0.-1.j 0.+0.j 0.+0.j] + [0.-1.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 0.+1.j] + [0.+0.j 0.+0.j 0.+1.j 0.+0.j]] + """ + + __slots__ = "theta" + + num_params: Final[int] = 1 + latex_str: Final[str] = r"{\rm RZX}" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + theta = arg_value[0] + return Qobj( + np.array( + [ + [np.cos(theta / 2), -1.0j * np.sin(theta / 2), 0.0, 0.0], + [-1.0j * np.sin(theta / 2), np.cos(theta / 2), 0.0, 0.0], + [0.0, 0.0, np.cos(theta / 2), 1.0j * np.sin(theta / 2)], + [0.0, 0.0, 1.0j * np.sin(theta / 2), np.cos(theta / 2)], + ] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def inverse(self) -> Gate | tuple[Type[Gate], tuple[float]]: + theta = self.arg_value[0] + return RZX(-theta) + + +class CX(_ControlledTwoQubitGate): + """ + CNOT gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import CX + >>> CX.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True + Qobj data = + [[1. 0. 0. 0.] + [0. 1. 0. 0.] + [0. 0. 0. 1.] + [0. 0. 1. 0.]] + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = X + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm CNOT}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + +CNOT = CX + + +class CY(_ControlledTwoQubitGate): + """ + Controlled CY gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import CY + >>> CY.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True + Qobj data = + [[ 1.+0j 0.+0j 0.+0j 0.+0j] + [ 0.+0j 1.+0j 0.+0j 0.+0j] + [ 0.+0j 0.+0j 0.+0j 0.-1j] + [ 0+0j. 0.+0j 0.+1j. 0.+0j]] + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = Y + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm CY}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + +class CZ(_ControlledTwoQubitGate): + """ + Controlled Z gate. + + Examples + -------- + >>> from qutip_qip.operations.gates import CZ + >>> CZ.get_qobj() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True + Qobj data = + [[ 1. 0. 0. 0.] + [ 0. 1. 0. 0.] + [ 0. 0. 1. 0.] + [ 0. 0. 0. -1.]] + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = Z + self_inverse: Final[bool] = True + is_clifford: Final[bool] = True + latex_str: Final[str] = r"{\rm CZ}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + +CSIGN = CZ + + +class CH(_ControlledTwoQubitGate): + r""" + CH gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CH + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = H + self_inverse: Final[bool] = True + latex_str: Final[str] = r"{\rm CH}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + sq_2 = 1 / np.sqrt(2) + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, sq_2, sq_2], + [0, 0, sq_2, -sq_2], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + +class CT(_ControlledTwoQubitGate): + r""" + CT gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CT + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = T + latex_str: Final[str] = r"{\rm CT}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, (1 + 1j) / np.sqrt(2)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return CTdag + + +class CTdag(_ControlledTwoQubitGate): + r""" + CTdag gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CTdag + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = Tdag + latex_str: Final[str] = r"{\rm CT^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, (1 - 1j) / np.sqrt(2)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return CT + + +class CS(_ControlledTwoQubitGate): + r""" + CS gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CS + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = S + latex_str: Final[str] = r"{\rm CS}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1j]] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return CSdag + + +class CSdag(_ControlledTwoQubitGate): + r""" + CS gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CS + """ + + __slots__ = () + + target_gate: Final[Type[Gate]] = Sdag + latex_str: Final[str] = r"{\rm CS^\dagger}" + + @staticmethod + @cache + def get_qobj(dtype: str = "dense") -> Qobj: + return Qobj( + np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1j]] + ), + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + @staticmethod + def inverse() -> Type[Gate]: + return CS + + +class CRX(_ControlledTwoQubitGate): + r""" + Controlled X rotation. + + Examples + -------- + >>> from qutip_qip.operations.gates import CRX + """ + + __slots__ = () + + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = RX + latex_str: Final[str] = r"{\rm CRX}" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + theta = arg_value[0] + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.cos(theta / 2), -1j * np.sin(theta / 2)], + [0, 0, -1j * np.sin(theta / 2), np.cos(theta / 2)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype=dtype) + + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CRX(-theta) + + +class CRY(_ControlledTwoQubitGate): + r""" + Controlled Y rotation. + + Examples + -------- + >>> from qutip_qip.operations.gates import CRY + """ + + __slots__ = () + + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = RY + latex_str: Final[str] = r"{\rm CRY}" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + theta = arg_value[0] + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.cos(theta / 2), -np.sin(theta / 2)], + [0, 0, np.sin(theta / 2), np.cos(theta / 2)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype) + + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CRY(-theta) + + +class CRZ(_ControlledTwoQubitGate): + r""" + CRZ gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & e^{-i\frac{\theta}{2}} & 0 \\ + 0 & 0 & 0 & e^{i\frac{\theta}{2}} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CRZ + >>> CRZ(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 1.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.-1.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] + """ + + __slots__ = () + + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = RZ + latex_str: Final[str] = r"{\rm CRZ}" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + theta = arg_value[0] + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.exp(-1j * theta / 2), 0], + [0, 0, 0, np.exp(1j * theta / 2)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype) + + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CRZ(-theta) + + +class CPHASE(_ControlledTwoQubitGate): + r""" + CPHASE gate. + + .. math:: + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\theta} \\ + \end{pmatrix} + + Examples + -------- + >>> from qutip_qip.operations.gates import CPHASE + >>> CPHASE(np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False + Qobj data = + [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] + [0.+0.j 1.+0.j 0.+0.j 0.+0.j] + [0.+0.j 0.+0.j 1.+0.j 0.+0.j] + [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] + """ + + __slots__ = () + + num_params: Final[int] = 1 + target_gate: Final[Type[Gate]] = PHASE + latex_str: Final[str] = r"{\rm CPHASE}" + + def __init__(self, theta: float, arg_label: str | None = None): + super().__init__(theta, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj(arg_value: tuple[float], dtype: str) -> Qobj: + theta = arg_value[0] + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, np.exp(1j * theta)], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype) + + def inverse(self) -> Gate: + theta = self.arg_value[0] + return CPHASE(-theta) + + +class CQASMU(_ControlledTwoQubitGate): + r""" + Controlled QASMU rotation. + + Examples + -------- + >>> from qutip_qip.operations.gates import CQASMU + """ + + __slots__ = () + + num_params: Final[int] = 3 + target_gate: Final[Type[Gate]] = QASMU + latex_str: Final[str] = r"{\rm CQASMU}" + + def __init__( + self, + theta: float, + phi: float, + gamma: float, + arg_label: str | None = None, + ): + super().__init__(theta, phi, gamma, arg_label=arg_label) + + @staticmethod + @lru_cache(maxsize=128) + def compute_qobj( + arg_value: tuple[float, float, float], dtype: str + ) -> Qobj: + theta, phi, gamma = arg_value + return Qobj( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [ + 0, + 0, + np.exp(-1j * (phi + gamma) / 2) * np.cos(theta / 2), + -np.exp(-1j * (phi - gamma) / 2) * np.sin(theta / 2), + ], + [ + 0, + 0, + np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), + np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2), + ], + ], + dims=[[2, 2], [2, 2]], + dtype=dtype, + ) + + def get_qobj(self, dtype: str = "dense") -> Qobj: + return self.compute_qobj(self.arg_value, dtype=dtype) + + def inverse(self) -> Gate: + theta, phi, gamma = self.arg_value + return CQASMU(-theta, -gamma, -phi) diff --git a/src/qutip_qip/operations/namespace.py b/src/qutip_qip/operations/namespace.py new file mode 100644 index 000000000..bf0e75ec9 --- /dev/null +++ b/src/qutip_qip/operations/namespace.py @@ -0,0 +1,191 @@ +# annotations can be removed when base version is Python 3.14 (PEP 749) +from __future__ import annotations +from functools import cached_property +from dataclasses import dataclass, field + + +class _SingletonMeta(type): + """ + Metaclass to implement the Singleton design pattern. + Note that this is not a thread-safe implementation of a Singleton. + """ + + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + + return cls._instances[cls] + + +class _GlobalNameSpaceRegistry(metaclass=_SingletonMeta): + """ + Global registry to manage and store all active namespaces. + + This class enforces a singleton pattern (using the metaclass) to ensure that only one global + registry exists during the runtime of the application. + """ + + def __init__(self): + self._registry: set[NameSpace] = set() + + def register_namespace(self, namespace: NameSpace) -> None: + """ + Safely adds a new namespace to the global registry. + + Note: This means that a gate (or operation) is never garbage + collected until the Namespace is destroyed. This is the desired + behavior for standard library gates. + + Parameters + ---------- + namespace : NameSpace + The namespace instance to be registered. + + Raises + ------ + ValueError + If the namespace already exists within the registry. + """ + if namespace in self._registry: + raise ValueError(f"Existing namespace {namespace}") + + self._registry.add(namespace) + + # Default behaviour for user defining his own gates is that namespace is None, + # Thus those gates are considered temporary by default, we use the same logic in + # QPE for Controlled Unitary gates, VQA untils Ops are implemented. + + +_GlobalRegistry = _GlobalNameSpaceRegistry() + + +@dataclass +class NameSpace: + """ + Represents a distinct, optionally hierarchical namespace for registering + quantum operations. + + Parameters + ---------- + local_name : str + The local identifier for the namespace. Must not contain periods ('.'). + parent : NameSpace or None, optional + The parent namespace, if this is a nested sub-namespace. Default is None. + """ + + local_name: str + parent: NameSpace | None = None + _registry: dict[str, any] = field(default_factory=dict) + + def __post_init__(self): + """ + Validates the namespace name and registers it globally upon creation. + + Raises + ------ + ValueError + If `local_name` contains a dot, as dots are reserved for hierarchy. + """ + if "." in self.local_name: + raise ValueError( + f"Namespace local_name '{self.local_name}' cannot contain dots. " + f"Dots are reserved for hierarchical resolution." + ) + _GlobalRegistry.register_namespace(self) + + @cached_property + def name(self) -> str: + """ + str: The fully qualified, hierarchical name of the namespace. + (e.g., 'std.gates'). + """ + if self.parent: + return f"{self.parent.name}.{self.local_name}" + return self.local_name + + def register( + self, name: str | tuple[str, int, int], operation_cls: any + ) -> None: + """ + Safely adds an item to this specific namespace. + + Parameters + ---------- + name : str or tuple of (str, int, int) + The identifier for the operation. Use a string for a non-controlled + gate. Use a tuple `(target_gate.name, num_ctrl_qubits, ctrl_values)` + as a second key for controlled gates. + operation_cls : any + The operation class or object to register. + + Raises + ------ + NameError + If an operation with the given name already exists in this namespace. + """ + if name in self._registry: + raise NameError( + f"'{operation_cls.__name__}' already exists in namespace '{self.name}'" + ) + self._registry[name] = operation_cls + + def get(self, name: str | tuple[str, int, int]) -> any: + """ + Retrieves a registered item from the namespace. + + Parameters + ---------- + name : str or tuple of (str, int, int) + The identifier of the registered operation. + + Returns + ------- + any + The registered operation class or object, or None if it is not found. + """ + if name not in self._registry: + return None + return self._registry[name] + + def _remove(self, name: str | tuple[str, int, int]) -> None: + """ + Removes an item from the namespace registry. + + Parameters + ---------- + name : str or tuple of (str, int, int) + The identifier of the operation to remove. + + Raises + ------ + KeyError + If the specified name does not exist in the namespace. + """ + if name not in self._registry: + raise KeyError( + f"{name} does not exists in namespace '{self.name} " + ) + del self._registry[name] + + def __str__(self) -> str: + return self.name + + def __repr__(self): + return str(self) + + def __eq__(self, other) -> bool: + """Checks equality based on the full namespace name.""" + if type(other) is not NameSpace: + return False + return self.name == other.name + + def __hash__(self) -> int: + """Hashes the namespace based on the full namespace name.""" + return hash(self.name) + + +# NS stands for namespace, std for Standard +NS_STD = NameSpace("std") # DEFAULT NAMESPACE +NS_GATE = NameSpace("gates", parent=NS_STD) # Default Gate Namespace diff --git a/src/qutip_qip/operations/gates.py b/src/qutip_qip/operations/old_gates.py similarity index 70% rename from src/qutip_qip/operations/gates.py rename to src/qutip_qip/operations/old_gates.py index 5a606e655..097603633 100644 --- a/src/qutip_qip/operations/gates.py +++ b/src/qutip_qip/operations/old_gates.py @@ -1,5 +1,7 @@ -import numbers -from collections.abc import Iterable +""" +Deprecated module, will be removed in future versions. +""" + from itertools import product from functools import partial, reduce from operator import mul @@ -8,8 +10,8 @@ import numpy as np import scipy.sparse as sp -import qutip from qutip import Qobj, identity, qeye, sigmax, sigmay, sigmaz, tensor, fock_dm +from qutip_qip.operations import expand_operator # Single Qubit Gates @@ -36,9 +38,7 @@ def x_gate(N=None, target=0): """ warnings.warn( "x_gate has been deprecated and will be removed in future version. \ - Use X.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use X.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -58,9 +58,7 @@ def y_gate(N=None, target=0): """ warnings.warn( "Y_gate has been deprecated and will be removed in future version. \ - Use Y.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use Y.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -80,9 +78,7 @@ def z_gate(N=None, target=0): """ warnings.warn( "z_gate has been deprecated and will be removed in future version. \ - Use Z.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use Z.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -101,9 +97,7 @@ def cy_gate(N=None, control=0, target=1): """ warnings.warn( "cy_gate has been deprecated and will be removed in future version. \ - Use CY.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CY.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -130,9 +124,7 @@ def cz_gate(N=None, control=0, target=1): """ warnings.warn( "cz_gate has been deprecated and will be removed in future version. \ - Use CZ.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CZ.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -160,9 +152,7 @@ def s_gate(N=None, target=0): """ warnings.warn( "s_gate has been deprecated and will be removed in future version. \ - Use S.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use S.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -181,9 +171,7 @@ def cs_gate(N=None, control=0, target=1): """ warnings.warn( "cs_gate has been deprecated and will be removed in future version. \ - Use CS.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CS.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -210,9 +198,7 @@ def t_gate(N=None, target=0): """ warnings.warn( "t_gate has been deprecated and will be removed in future version. \ - Use T.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use T.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -231,9 +217,7 @@ def ct_gate(N=None, control=0, target=1): """ warnings.warn( "ct_gate has been deprecated and will be removed in future version. \ - Use CT.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CT.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: N = 2 @@ -265,9 +249,7 @@ def rx(phi, N=None, target=0): """ warnings.warn( "rxRTNOT has been deprecated and will be removed in future version. \ - Use RX(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use RX(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -291,9 +273,7 @@ def ry(phi, N=None, target=0): """ warnings.warn( "ryRTNOT has been deprecated and will be removed in future version. \ - Use RY(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use RY(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -317,9 +297,7 @@ def rz(phi, N=None, target=0): """ warnings.warn( "rzRTNOT has been deprecated and will be removed in future version. \ - Use RZ(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use RZ(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -338,9 +316,7 @@ def sqrtnot(N=None, target=0): """ warnings.warn( "sqrtnot has been deprecated and will be removed in future version. \ - Use SQRTNOT.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SQRTNOT.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -368,9 +344,7 @@ def snot(N=None, target=0): """ warnings.warn( "snot has been deprecated and will be removed in future version. \ - Use H.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use H.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -404,9 +378,7 @@ def phasegate(theta, N=None, target=0): """ warnings.warn( "phase has been deprecated and will be removed in future version. \ - Use PHASE(angle).get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use PHASE(angle).get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if N is not None: _deprecation_warnings_gate_expansion() @@ -439,7 +411,7 @@ def qrot(theta, phi, N=None, target=0): "qrot has been deprecated and will be removed in future version. \ Use R([theta, phi]).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if N is not None: _deprecation_warnings_gate_expansion() @@ -484,9 +456,9 @@ def qasmu_gate(args, N=None, target=0): """ warnings.warn( "qasmu_gate has been deprecated and will be removed in future version. \ - Use QASMU([theta, phi, gamma]).get_qobj() instead.", + Use QASMU(theta, phi, gamma).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) theta, phi, gamma = args @@ -530,7 +502,7 @@ def cphase(theta, N=2, control=0, target=1): "cphase has been deprecated and will be removed in future version. \ Use CPHASE(angle).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if N != 2 or control != 0 or target != 1: @@ -577,9 +549,7 @@ def cnot(N=None, control=0, target=1): """ warnings.warn( "cnot has been deprecated and will be removed in future version. \ - Use CX.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CX.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: @@ -617,9 +587,7 @@ def csign(N=None, control=0, target=1): """ warnings.warn( "csign has been deprecated and will be removed in future version. \ - Use CZ.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use CZ.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (control == 1 and target == 0) and N is None: @@ -657,9 +625,7 @@ def berkeley(N=None, targets=[0, 1]): """ warnings.warn( "berkley has been deprecated and will be removed in future version. \ - Use BERKELEY.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use BERKELEY.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if (targets[0] == 1 and targets[1] == 0) and N is None: @@ -704,7 +670,7 @@ def swapalpha(alpha, N=None, targets=[0, 1]): "swapalpha has been deprecated and will be removed in future version. \ Use SWAPALPHA(angle).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if (targets[0] == 1 and targets[1] == 0) and N is None: @@ -756,9 +722,7 @@ def swap(N=None, targets=[0, 1]): """ warnings.warn( "SWAP has been deprecated and will be removed in future version. \ - Use SWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -793,9 +757,7 @@ def iswap(N=None, targets=[0, 1]): """ warnings.warn( "ISWAP has been deprecated and will be removed in future version. \ - Use ISWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use ISWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -820,9 +782,7 @@ def sqrtswap(N=None, targets=[0, 1]): """ warnings.warn( "SQRTSWAP has been deprecated and will be removed in future version. \ - Use SQRTSWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SQRTSWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -869,9 +829,7 @@ def sqrtiswap(N=None, targets=[0, 1]): """ warnings.warn( "SQRTISWAP has been deprecated and will be removed in future version. \ - Use SQRTISWAP.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use SQRTISWAP.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if targets != [0, 1] and N is None: N = 2 @@ -916,7 +874,7 @@ def molmer_sorensen(theta, phi=0.0, N=None, targets=[0, 1]): "MS has been deprecated and will be removed in future version. \ Use MS([theta, phi]).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) if targets != [0, 1] and N is None: N = 2 @@ -978,9 +936,7 @@ def fredkin(N=None, control=0, targets=[1, 2]): """ warnings.warn( "fredkin has been deprecated and will be removed in future version. \ - Use FREDKIN.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use FREDKIN.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if [control, targets[0], targets[1]] != [0, 1, 2] and N is None: N = 3 @@ -1031,9 +987,7 @@ def toffoli(N=None, controls=[0, 1], target=2): """ warnings.warn( "toffoli has been deprecated and will be removed in future version. \ - Use TOFFOLI.get_qobj() instead.", - DeprecationWarning, - stacklevel=2 + Use TOFFOLI.get_qobj() instead.", DeprecationWarning, stacklevel=2 ) if [controls[0], controls[1], target] != [0, 1, 2] and N is None: N = 3 @@ -1072,67 +1026,18 @@ def rotation(op, phi, N=None, target=0): Quantum object for operator describing the rotation. """ + warnings.warn( + "rotation has been deprecated and will be removed in future version.", + DeprecationWarning, + stacklevel=2, + ) + if N is not None: _deprecation_warnings_gate_expansion() return expand_operator(rotation(op, phi), N, target) return (-1j * op * phi / 2).expm() -def controlled_gate( - U, - controls=0, - targets=1, - N=None, - control_value=1, -): - """ - Create an N-qubit controlled gate from a single-qubit gate U with the given - control and target qubits. - - Parameters - ---------- - U : :class:`qutip.Qobj` - An arbitrary unitary gate. - controls : list of int - The index of the first control qubit. - targets : list of int - The index of the target qubit. - N : int - The total number of qubits. - control_value : int - The decimal value of the controlled qubits that activates the gate U. - - Returns - ------- - result : qobj - Quantum object representing the controlled-U gate. - """ - # Compatibility - if not isinstance(controls, Iterable): - controls = [controls] - if not isinstance(targets, Iterable): - targets = [targets] - num_controls = len(controls) - num_targets = len(U.dims[0]) - N = num_controls + num_targets if N is None else N - - # First, assume that the last qubit is the target and control qubits are - # in the increasing order. - # The control_value is the location of this unitary. - block_matrices = [np.array([[1, 0], [0, 1]])] * 2**num_controls - block_matrices[control_value] = U.full() - from scipy.linalg import block_diag # move this to the top of the file - - result = block_diag(*block_matrices) - result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) - - # Expand it to N qubits and permute qubits labelling - if controls + targets == list(range(N)): - return result - else: - return expand_operator(result, N, targets=controls + targets) - - def globalphase(theta, N=1): """ Returns quantum object representing the global phase shift gate. @@ -1161,7 +1066,7 @@ def globalphase(theta, N=1): "global_phase has been deprecated and will be removed in future version. \ Use GLOBALPHASE(phase).get_qobj() instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) data = np.exp(1.0j * theta) * sp.eye( 2**N, 2**N, dtype=complex, format="csr" @@ -1174,33 +1079,6 @@ def globalphase(theta, N=1): # -def _hamming_distance(x, bits=32): - """ - Calculate the bit-wise Hamming distance of x from 0: That is, the number - 1s in the integer x. - """ - tot = 0 - while x: - tot += 1 - x &= x - 1 - return tot - - -def hadamard_transform(N=1): - """Quantum object representing the N-qubit Hadamard gate. - - Returns - ------- - q : qobj - Quantum object representation of the N-qubit Hadamard gate. - - """ - data = [[1, 1], [1, -1]] - H = Qobj(data) / np.sqrt(2) - - return tensor([H] * N) - - def _powers(op, N): """ Generator that yields powers of an operator `op`, @@ -1238,6 +1116,12 @@ def qubit_clifford_group(N=None, target=0): """ + warnings.warn( + "qubit_clifford has been deprecated and will be removed in future version.", + DeprecationWarning, + stacklevel=2, + ) + # The Ross-Selinger presentation of the single-qubit Clifford # group expresses each element in the form C_{ijk} = E^i X^j S^k # for gates E, X and S, and for i in range(3), j in range(2) and @@ -1269,245 +1153,3 @@ def qubit_clifford_group(N=None, target=0): yield expand_operator(op, N, target) else: yield op - - -# -# Gate Expand -# - - -def _check_oper_dims(oper, dims=None, targets=None): - """ - Check if the given operator is valid. - - Parameters - ---------- - oper : :class:`qutip.Qobj` - The quantum object to be checked. - dims : list, optional - A list of integer for the dimension of each composite system. - e.g ``[2, 2, 2, 2, 2]`` for 5 qubits system. - targets : int or list of int, optional - The indices of subspace that are acted on. - """ - # if operator matches N - if not isinstance(oper, Qobj) or oper.dims[0] != oper.dims[1]: - raise ValueError( - "The operator is not an " - "Qobj with the same input and output dimensions." - ) - # if operator dims matches the target dims - if dims is not None and targets is not None: - targ_dims = [dims[t] for t in targets] - if oper.dims[0] != targ_dims: - raise ValueError( - "The operator dims {} do not match " - "the target dims {}.".format(oper.dims[0], targ_dims) - ) - - -def _targets_to_list(targets, oper=None, N=None): - """ - transform targets to a list and check validity. - - Parameters - ---------- - targets : int or list of int - The indices of subspace that are acted on. - oper : :class:`qutip.Qobj`, optional - An operator, the type of the :class:`qutip.Qobj` - has to be an operator - and the dimension matches the tensored qubit Hilbert space - e.g. dims = ``[[2, 2, 2], [2, 2, 2]]`` - N : int, optional - The number of subspace in the system. - """ - # if targets is a list of integer - if targets is None: - targets = list(range(len(oper.dims[0]))) - if not hasattr(targets, "__iter__"): - targets = [targets] - if not all([isinstance(t, numbers.Integral) for t in targets]): - raise TypeError("targets should be an integer or a list of integer") - # if targets has correct length - if oper is not None: - req_num = len(oper.dims[0]) - if len(targets) != req_num: - raise ValueError( - "The given operator needs {} " - "target qutbis, " - "but {} given.".format(req_num, len(targets)) - ) - # if targets is smaller than N - if N is not None: - if not all([t < N for t in targets]): - raise ValueError("Targets must be smaller than N={}.".format(N)) - return targets - - -def expand_operator( - oper, N=None, targets=None, dims=None, cyclic_permutation=False, dtype=None -): - """ - Expand an operator to one that acts on a system with desired dimensions. - - Parameters - ---------- - oper : :class:`qutip.Qobj` - An operator that act on the subsystem, has to be an operator and the - dimension matches the tensored dims Hilbert space - e.g. oper.dims = ``[[2, 3], [2, 3]]`` - dims : list - A list of integer for the dimension of each composite system. - E.g ``[2, 3, 2, 3, 4]``. - targets : int or list of int - The indices of subspace that are acted on. - Permutation can also be realized by changing the orders of the indices. - N : int - Deprecated. Number of qubits. Please use `dims`. - cyclic_permutation : boolean, optional - Deprecated. - Expand for all cyclic permutation of the targets. - E.g. if ``N=3`` and `oper` is a 2-qubit operator, - the result will be a list of three operators, - each acting on qubits 0 and 1, 1 and 2, 2 and 0. - dtype : str, optional - Data type of the output `Qobj`. Only for qutip version larger than 5. - - - Returns - ------- - expanded_oper : :class:`qutip.Qobj` - The expanded operator acting on a system with desired dimension. - - Examples - -------- - >>> from qutip_qip.operations import expand_operator, X, CNOT - >>> import qutip - >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True - Qobj data = - [[0. 0. 0. 1. 0. 0.] - [0. 0. 0. 0. 1. 0.] - [0. 0. 0. 0. 0. 1.] - [1. 0. 0. 0. 0. 0.] - [0. 1. 0. 0. 0. 0.] - [0. 0. 1. 0. 0. 0.]] - >>> expand_operator(CNOT.get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True - Qobj data = - [[1. 0. 0. 0. 0. 0. 0. 0.] - [0. 1. 0. 0. 0. 0. 0. 0.] - [0. 0. 0. 1. 0. 0. 0. 0.] - [0. 0. 1. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 1. 0. 0. 0.] - [0. 0. 0. 0. 0. 1. 0. 0.] - [0. 0. 0. 0. 0. 0. 0. 1.] - [0. 0. 0. 0. 0. 0. 1. 0.]] - >>> expand_operator(CNOT.get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True - Qobj data = - [[1. 0. 0. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 0. 1. 0. 0.] - [0. 0. 1. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 0. 0. 0. 1.] - [0. 0. 0. 0. 1. 0. 0. 0.] - [0. 1. 0. 0. 0. 0. 0. 0.] - [0. 0. 0. 0. 0. 0. 1. 0.] - [0. 0. 0. 1. 0. 0. 0. 0.]] - """ - dtype = dtype or qutip.settings.core["default_dtype"] or qutip.data.CSR - oper = oper.to(dtype) - - if N is not None: - warnings.warn( - "The function expand_operator has been generalized to " - "arbitrary subsystems instead of only qubit systems." - "Please use the new signature e.g.\n" - "expand_operator(oper, dims=[2, 3, 2, 2], targets=2)", - DeprecationWarning, - stacklevel=2 - ) - - if dims is not None and N is None: - if not isinstance(dims, Iterable): - f"dims needs to be an interable {not type(dims)}." - N = len(dims) # backward compatibility - - if dims is None: - dims = [2] * N - targets = _targets_to_list(targets, oper=oper, N=N) - _check_oper_dims(oper, dims=dims, targets=targets) - - # Call expand_operator for all cyclic permutation of the targets. - if cyclic_permutation: - warnings.warn( - "cyclic_permutation is deprecated, " - "please use loop through different targets manually.", - DeprecationWarning, - stacklevel=2 - ) - oper_list = [] - for i in range(N): - new_targets = np.mod(np.array(targets) + i, N) - oper_list.append( - expand_operator(oper, N=N, targets=new_targets, dims=dims) - ) - return oper_list - - # Generate the correct order for permutation, - # eg. if N = 5, targets = [3,0], the order is [1,2,3,0,4]. - # If the operator is cnot, - # this order means that the 3rd qubit controls the 0th qubit. - new_order = [0] * N - for i, t in enumerate(targets): - new_order[t] = i - # allocate the rest qutbits (not targets) to the empty - # position in new_order - rest_pos = [q for q in list(range(N)) if q not in targets] - rest_qubits = list(range(len(targets), N)) - for i, ind in enumerate(rest_pos): - new_order[ind] = rest_qubits[i] - id_list = [identity(dims[i]) for i in rest_pos] - out = tensor([oper] + id_list).permute(new_order) - return out.to(dtype) - - -def gate_sequence_product( - U_list, left_to_right=True, inds_list=None, expand=False -): - """ - Calculate the overall unitary matrix for a given list of unitary operations. - - Parameters - ---------- - U_list: list - List of gates implementing the quantum circuit. - - left_to_right: Boolean, optional - Check if multiplication is to be done from left to right. - - inds_list: list of list of int, optional - If expand=True, list of qubit indices corresponding to U_list - to which each unitary is applied. - - expand: Boolean, optional - Check if the list of unitaries need to be expanded to full dimension. - - Returns - ------- - U_overall : qobj - Unitary matrix corresponding to U_list. - - overall_inds : list of int, optional - List of qubit indices on which U_overall applies. - """ - from qutip_qip.circuit.simulator import ( - gate_sequence_product, - gate_sequence_product_with_expansion, - ) - - if expand: - return gate_sequence_product(U_list, inds_list) - else: - return gate_sequence_product_with_expansion(U_list, left_to_right) diff --git a/src/qutip_qip/operations/parametric.py b/src/qutip_qip/operations/parametric.py new file mode 100644 index 000000000..6ac864f60 --- /dev/null +++ b/src/qutip_qip/operations/parametric.py @@ -0,0 +1,186 @@ +import inspect +from abc import abstractmethod +from collections.abc import Sequence + +from qutip import Qobj +from qutip_qip.operations import Gate +from qutip_qip.typing import Real + + +class ParametricGate(Gate): + r""" + Abstract base class for parametric quantum gates. + + Parameters + ---------- + arg_value : float or Sequence + The argument value(s) for the gate. If a single float is provided, + it is converted to a list. These values are saved as attributes + and can be accessed or modified later. + + arg_label : str, optional + Label for the argument to be shown in the circuit plot. + + Example: + If ``arg_label="\phi"``, the LaTeX name for the gate in the circuit + plot will be rendered as ``$U(\phi)$``. + + Attributes + ---------- + num_params : int + The number of parameters required by the gate. This is a mandatory + class attribute for subclasses. + + arg_value : Sequence + The numerical values of the parameters provided to the gate. + + arg_label : str, optional + The LaTeX string representing the parameter variable in circuit plots. + + Raises + ------ + ValueError + If the number of provided arguments does not match `num_params`. + """ + + __slots__ = ("_arg_value", "arg_label") + num_params: int + + def __init_subclass__(cls, **kwargs) -> None: + """ + Validates the subclass definition. + + Ensures that `num_params` is defined as a positive integer. + """ + super().__init_subclass__(**kwargs) + if inspect.isabstract(cls): + return + + # Assert num_params is a positive integer + num_params = getattr(cls, "num_params", None) + if (type(num_params) is not int) or (num_params < 1): + raise TypeError( + f"Class '{cls.__name__}' attribute 'num_params' must be a postive integer, " + f"got {type(num_params)} with value {num_params}." + ) + + # Validate params must take only one argument 'args' + validate_params_func = getattr(cls, "validate_params") + if len(inspect.signature(validate_params_func).parameters) > 1: + raise SyntaxError( + f"Class '{cls.name}' method 'validate_params()' must take exactly 1 " + f"additional arguments (only the implicit 'args')," + f" but it takes {len(inspect.signature(validate_params_func).parameters)}." + ) + + # compute_qobj method must take only two arguments arg_value, dtype + compute_qobj_func = getattr(cls, "compute_qobj") + if len(inspect.signature(compute_qobj_func).parameters) != 2: + raise SyntaxError( + f"Class '{cls.name}' method 'compute_qobj()' must take exactly 2 " + f"arguments (only the implicit 'arg_value, dtype')," + f" but it takes {len(inspect.signature(compute_qobj_func).parameters)}." + ) + + if not cls.is_parametric(): + raise ValueError( + f"Class '{cls.name}' method 'is_parametric()' must always return True." + ) + + if cls.is_controlled(): + raise ValueError( + f"Class '{cls.name}' method 'is_controlled()' must always return False." + ) + + def __init__(self, *args, arg_label: str | None = None) -> None: + # This auto triggers a call to arg_value setter (where checks happen) + self.arg_value = args + self.arg_label = arg_label + + @property + def arg_value(self) -> tuple[any, ...]: + return self._arg_value + + @arg_value.setter + def arg_value(self, new_args: Sequence) -> None: + if not isinstance(new_args, Sequence): + new_args = [new_args] + + if len(new_args) != self.num_params: + raise ValueError( + f"Requires {self.num_params} parameters, got {len(new_args)}" + ) + + self.validate_params(new_args) + self._arg_value = tuple(new_args) + + @staticmethod + @abstractmethod + def validate_params(arg_value): + r""" + Validate the provided parameters. + + This method should be implemented by subclasses to check if the + parameters are valid type and within valid range (e.g., $0 \le \theta < 2\pi$). + + Parameters + ---------- + arg_value : list of float + The parameters to validate. + """ + raise NotImplementedError + + def get_qobj(self, dtype: str = "dense") -> Qobj: + """ + Get the QuTiP quantum object representation using the current parameters. + + Returns + ------- + qobj : qutip.Qobj + The unitary matrix representing the gate with the specific `arg_value`. + """ + return self.compute_qobj(self.arg_value, dtype) + + @staticmethod + @abstractmethod + def compute_qobj(args: tuple, dtype: str) -> Qobj: + raise NotImplementedError + + def inverse(self) -> Gate: + if self.self_inverse: + return self + raise NotImplementedError + + @staticmethod + def is_parametric() -> bool: + return True + + def __str__(self) -> str: + return f""" + Gate({self.name}, arg_value={self.arg_value}, + arg_label={self.arg_label}), + """ + + def __eq__(self, other) -> bool: + # Returns false for RX(0.5), RY(0.5) + if type(self) is not type(other): + return False + + # Returns false for RX(0.5), RX(0.6) + if self.arg_value != other.arg_value: + return False + + return True + + def __hash__(self) -> int: + return hash((type(self), self.arg_value)) + + +class AngleParametricGate(ParametricGate): + __slots__ = () + + @staticmethod + def validate_params(arg_value) -> None: + for arg in arg_value: + if not isinstance(arg, Real): + raise TypeError(f"Invalid arg {arg} in arg_value") diff --git a/src/qutip_qip/operations/std/__init__.py b/src/qutip_qip/operations/std/__init__.py deleted file mode 100644 index 1cb59de6e..000000000 --- a/src/qutip_qip/operations/std/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -from .single_qubit_gate import ( - X, - Y, - Z, - RX, - RY, - RZ, - PHASE, - H, - SNOT, - SQRTNOT, - SQRTX, - S, - T, - R, - QASMU, - IDLE, -) -from .two_qubit_gate import ( - SWAP, - ISWAP, - SQRTSWAP, - SQRTISWAP, - SWAPALPHA, - BERKELEY, - MS, - RZX, - CX, - CY, - CZ, - CRX, - CRY, - CRZ, - CS, - CT, - CH, - CNOT, - CPHASE, - CSIGN, - CQASMU, -) -from .other_gates import ( - GLOBALPHASE, - TOFFOLI, - FREDKIN, -) - - -__all__ = [ - "IDLE", - "X", - "Y", - "Z", - "RX", - "RY", - "RZ", - "PHASE", - "H", - "SNOT", - "SQRTNOT", - "SQRTX", - "S", - "T", - "R", - "QASMU", - "SWAP", - "ISWAP", - "CNOT", - "SQRTSWAP", - "SQRTISWAP", - "SWAPALPHA", - "MS", - "BERKELEY", - "CSIGN", - "CRX", - "CRY", - "CRZ", - "CY", - "CX", - "CZ", - "CH", - "CS", - "CT", - "CPHASE", - "RZX", - "CQASMU", - "GLOBALPHASE", - "TOFFOLI", - "FREDKIN", -] \ No newline at end of file diff --git a/src/qutip_qip/operations/std/single_qubit_gate.py b/src/qutip_qip/operations/std/single_qubit_gate.py deleted file mode 100644 index 42cacfbbc..000000000 --- a/src/qutip_qip/operations/std/single_qubit_gate.py +++ /dev/null @@ -1,377 +0,0 @@ -import warnings -import numpy as np - -from qutip import Qobj, sigmax, sigmay, sigmaz, qeye -from qutip_qip.operations import Gate, AngleParametricGate - -class _SingleQubitGate(Gate): - """Abstract one-qubit gate.""" - num_qubits = 1 - - -class _SingleQubitParametricGate(AngleParametricGate): - """Abstract one-qubit parametric gate.""" - num_qubits = 1 - - -class X(_SingleQubitGate): - """ - Single-qubit X gate. - - Examples - -------- - >>> from qutip_qip.operations import X - >>> X.get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True - Qobj data = - [[0. 1.] - [1. 0.]] - """ - self_inverse = True - is_clifford = True - latex_str = r"X" - - @staticmethod - def get_qobj(): - return sigmax(dtype="dense") - - -class Y(_SingleQubitGate): - """ - Single-qubit Y gate. - - Examples - -------- - >>> from qutip_qip.operations import Y - >>> Y(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True - Qobj data = - [[0.+0.j 0.-1.j] - [0.+1.j 0.+0.j]] - """ - self_inverse = True - is_clifford = True - latex_str = r"Y" - - @staticmethod - def get_qobj(): - return sigmay(dtype="dense") - - -class Z(_SingleQubitGate): - """ - Single-qubit Z gate. - - Examples - -------- - >>> from qutip_qip.operations import Z - >>> Z(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True - Qobj data = - [[ 1. 0.] - [ 0. -1.]] - """ - self_inverse = True - is_clifford = True - latex_str = r"Z" - - @staticmethod - def get_qobj(): - return sigmaz(dtype="dense") - - -class IDLE(_SingleQubitGate): - """ - IDLE gate. - - Examples - -------- - >>> from qutip_qip.operations import IDLE - """ - self_inverse = True - is_clifford = True - latex_str = r"{\rm IDLE}" - - @staticmethod - def get_qobj(): - return qeye(2) - - -class H(_SingleQubitGate): - """ - Hadamard gate. - - Examples - -------- - >>> from qutip_qip.operations import H - >>> H(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True - Qobj data = - [[ 0.70711 0.70711] - [ 0.70711 -0.70711]] - """ - self_inverse = True - is_clifford = True - latex_str = r"H" - - @staticmethod - def get_qobj(): - return 1 / np.sqrt(2.0) * Qobj([[1, 1], [1, -1]]) - - -class SNOT(H): - def __init__(self): - warnings.warn( - "SNOT is deprecated and will be removed in future versions. " - "Use H instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__() - - -class SQRTX(_SingleQubitGate): - r""" - :math:`\sqrt{X}` gate. - - Examples - -------- - >>> from qutip_qip.operations import SQRTNOT - >>> SQRTNOT(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[0.5+0.5j 0.5-0.5j] - [0.5-0.5j 0.5+0.5j]] - """ - self_inverse = False - is_clifford = True - latex_str = r"\sqrt{\rm NOT}" - - @staticmethod - def get_qobj(): - return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]) - - -class SQRTNOT(SQRTX): - def __init__(self): - warnings.warn( - "SQRTNOT is deprecated and will be removed in future versions. " - "Use SQRTX instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__() - - -class S(_SingleQubitGate): - r""" - S gate or :math:`\sqrt{Z}` gate. - - Examples - -------- - >>> from qutip_qip.operations import S - >>> S(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1.+0.j 0.+0.j] - [0.+0.j 0.+1.j]] - """ - self_inverse = False - is_clifford = True - latex_str = r"{\rm S}" - - @staticmethod - def get_qobj(): - return Qobj([[1, 0], [0, 1j]]) - - -class T(_SingleQubitGate): - r""" - T gate or :math:`\sqrt[4]{Z}` gate. - - Examples - -------- - >>> from qutip_qip.operations import T - >>> T(0).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1. +0.j 0. +0.j ] - [0. +0.j 0.70711+0.70711j]] - """ - self_inverse = False - latex_str = r"{\rm T}" - - @staticmethod - def get_qobj(): - return Qobj([[1, 0], [0, np.exp(1j * np.pi / 4)]]) - - -class RX(_SingleQubitParametricGate): - """ - Single-qubit rotation RX. - - Examples - -------- - >>> from qutip_qip.operations import RX - >>> RX(3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[0.70711+0.j 0. -0.70711j] - [0. -0.70711j 0.70711+0.j ]] - """ - num_params = 1 - latex_str = r"R_x" - - def get_qobj(self): - phi = self.arg_value[0] - return Qobj( - [ - [np.cos(phi / 2), -1j * np.sin(phi / 2)], - [-1j * np.sin(phi / 2), np.cos(phi / 2)], - ], - dims = [[2], [2]] - ) - - -class RY(_SingleQubitParametricGate): - """ - Single-qubit rotation RY. - - Examples - -------- - >>> from qutip_qip.operations import RY - >>> RY(0, 3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[ 0.70711 -0.70711] - [ 0.70711 0.70711]] - """ - num_params = 1 - latex_str = r"R_y" - - def get_qobj(self): - phi = self.arg_value[0] - return Qobj( - [ - [np.cos(phi / 2), -np.sin(phi / 2)], - [np.sin(phi / 2), np.cos(phi / 2)], - ] - ) - - -class RZ(_SingleQubitParametricGate): - """ - Single-qubit rotation RZ. - - Examples - -------- - >>> from qutip_qip.operations import RZ - >>> RZ(0, 3.14159/2).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[0.70711-0.70711j 0. +0.j ] - [0. +0.j 0.70711+0.70711j]] - """ - num_params = 1 - latex_str = r"R_z" - - def get_qobj(self): - phi = self.arg_value[0] - return Qobj([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) - - -class PHASE(_SingleQubitParametricGate): - """ - PHASE Gate. - - Examples - -------- - >>> from qutip_qip.operations import PHASE - """ - num_params = 1 - latex_str = r"PHASE" - - def get_qobj(self): - phi = self.arg_value[0] - return Qobj( - [ - [1, 0], - [0, np.exp(1j * phi)], - ] - ) - - -class R(_SingleQubitParametricGate): - r""" - Arbitrary single-qubit rotation - - .. math:: - - \begin{pmatrix} - \cos(\frac{\theta}{2}) & -ie^{-i\phi} \sin(\frac{\theta}{2}) \\ - -ie^{i\phi} \sin(\frac{\theta}{2}) & \cos(\frac{\theta}{2}) - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import R - >>> R(0, (np.pi/2, np.pi/2)).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[ 0.70711 -0.70711] - [ 0.70711 0.70711]] - """ - - num_params = 2 - latex_str = r"{\rm R}" - - def get_qobj(self): - phi, theta = self.arg_value - return Qobj( - [ - [ - np.cos(theta / 2.0), - -1.0j * np.exp(-1.0j * phi) * np.sin(theta / 2.0), - ], - [ - -1.0j * np.exp(1.0j * phi) * np.sin(theta / 2.0), - np.cos(theta / 2.0), - ], - ] - ) - - -class QASMU(_SingleQubitParametricGate): - r""" - QASMU gate. - - .. math:: - U(\theta, \phi, \gamma) = RZ(\phi) RY(\theta) RZ(\gamma) - - Examples - -------- - >>> from qutip_qip.operations import QASMU - >>> QASMU(0, (np.pi/2, np.pi, np.pi/2)).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=False - Qobj data = - [[-0.5-0.5j -0.5+0.5j] - [ 0.5+0.5j -0.5+0.5j]] - """ - - num_params = 3 - latex_str = r"{\rm QASMU}" - - def get_qobj(self): - theta, phi, gamma = self.arg_value - return Qobj( - [ - [ - np.exp(-1j * (phi + gamma) / 2) * np.cos(theta / 2), - -np.exp(-1j * (phi - gamma) / 2) * np.sin(theta / 2), - ], - [ - np.exp(1j * (phi - gamma) / 2) * np.sin(theta / 2), - np.exp(1j * (phi + gamma) / 2) * np.cos(theta / 2), - ], - ] - ) diff --git a/src/qutip_qip/operations/std/two_qubit_gate.py b/src/qutip_qip/operations/std/two_qubit_gate.py deleted file mode 100644 index fd28f3888..000000000 --- a/src/qutip_qip/operations/std/two_qubit_gate.py +++ /dev/null @@ -1,670 +0,0 @@ -from typing import Final -import warnings - -import numpy as np -from qutip import Qobj - -from qutip_qip.operations import ( - Gate, - ControlledGate, - AngleParametricGate, -) -from qutip_qip.operations.std import ( - X, Y, Z, H, S, T, RX, RY, RZ, QASMU, PHASE -) - -class _TwoQubitGate(Gate): - """Abstract two-qubit gate.""" - num_qubits: Final[int] = 2 - - -class _ControlledTwoQubitGate(ControlledGate): - """ - This class allows correctly generating the gate instance - when a redundant control_value is given, e.g. - ``CNOT(0, 1, control_value=1)``, - and raise an error if it is 0. - """ - - num_qubits: Final[int] = 2 - num_ctrl_qubits: Final[int] = 1 - - -class SWAP(_TwoQubitGate): - """ - SWAP gate. - - Examples - -------- - >>> from qutip_qip.operations import SWAP - >>> SWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True - Qobj data = - [[1. 0. 0. 0.] - [0. 0. 1. 0.] - [0. 1. 0. 0.] - [0. 0. 0. 1.]] - """ - self_inverse = True - is_clifford = True - latex_str = r"{\rm SWAP}" - - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], - dims=[[2, 2], [2, 2]], - ) - - -class ISWAP(_TwoQubitGate): - """ - iSWAP gate. - - Examples - -------- - >>> from qutip_qip.operations import ISWAP - >>> ISWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] - [0.+0.j 0.+0.j 0.+1.j 0.+0.j] - [0.+0.j 0.+1.j 0.+0.j 0.+0.j] - [0.+0.j 0.+0.j 0.+0.j 1.+0.j]] - """ - self_inverse = False - is_clifford = True - latex_str = r"{i}{\rm SWAP}" - - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], - dims=[[2, 2], [2, 2]], - ) - - -class SQRTSWAP(_TwoQubitGate): - r""" - :math:`\sqrt{\mathrm{SWAP}}` gate. - - Examples - -------- - >>> from qutip_qip.operations import SQRTSWAP - >>> SQRTSWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] - [0. +0.j 0.5+0.5j 0.5-0.5j 0. +0.j ] - [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] - [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] - """ - self_inverse = False - latex_str = r"\sqrt{\rm SWAP}" - - @staticmethod - def get_qobj(): - return Qobj( - np.array( - [ - [1, 0, 0, 0], - [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], - [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], - [0, 0, 0, 1], - ] - ), - dims=[[2, 2], [2, 2]], - ) - - -class SQRTISWAP(_TwoQubitGate): - r""" - :math:`\sqrt{\mathrm{iSWAP}}` gate. - - Examples - -------- - >>> from qutip_qip.operations import SQRTISWAP - >>> SQRTISWAP([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] - [0. +0.j 0.70711+0.j 0. +0.70711j 0. +0.j ] - [0. +0.j 0. +0.70711j 0.70711+0.j 0. +0.j ] - [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] - """ - self_inverse = False - is_clifford = True - latex_str = r"\sqrt{{i}\rm SWAP}" - - @staticmethod - def get_qobj(): - return Qobj( - np.array( - [ - [1, 0, 0, 0], - [0, 1 / np.sqrt(2), 1j / np.sqrt(2), 0], - [0, 1j / np.sqrt(2), 1 / np.sqrt(2), 0], - [0, 0, 0, 1], - ] - ), - dims=[[2, 2], [2, 2]], - ) - - -class BERKELEY(_TwoQubitGate): - r""" - BERKELEY gate. - - .. math:: - - \begin{pmatrix} - \cos(\frac{\pi}{8}) & 0 & 0 & i\sin(\frac{\pi}{8}) \\ - 0 & \cos(\frac{3\pi}{8}) & i\sin(\frac{3\pi}{8}) & 0 \\ - 0 & i\sin(\frac{3\pi}{8}) & \cos(\frac{3\pi}{8}) & 0 \\ - i\sin(\frac{\pi}{8}) & 0 & 0 & \cos(\frac{\pi}{8}) - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import BERKELEY - >>> BERKELEY([0, 1]).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[0.92388+0.j 0. +0.j 0. +0.j 0. +0.38268j] - [0. +0.j 0.38268+0.j 0. +0.92388j 0. +0.j ] - [0. +0.j 0. +0.92388j 0.38268+0.j 0. +0.j ] - [0. +0.38268j 0. +0.j 0. +0.j 0.92388+0.j ]] - """ - self_inverse = False - latex_str = r"{\rm BERKELEY}" - - @staticmethod - def get_qobj(): - return Qobj( - [ - [np.cos(np.pi / 8), 0, 0, 1.0j * np.sin(np.pi / 8)], - [0, np.cos(3 * np.pi / 8), 1.0j * np.sin(3 * np.pi / 8), 0], - [0, 1.0j * np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], - [1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], - ], - dims=[[2, 2], [2, 2]], - ) - - -class SWAPALPHA(AngleParametricGate): - r""" - SWAPALPHA gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \frac{1 + e^{i\pi\alpha}}{2} & \frac{1 - e^{i\pi\alpha}}{2} & 0 \\ - 0 & \frac{1 - e^{i\pi\alpha}}{2} & \frac{1 + e^{i\pi\alpha}}{2} & 0 \\ - 0 & 0 & 0 & 1 - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import SWAPALPHA - >>> SWAPALPHA([0, 1], 0.5).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1. +0.j 0. +0.j 0. +0.j 0. +0.j ] - [0. +0.j 0.5+0.5j 0.5-0.5j 0. +0.j ] - [0. +0.j 0.5-0.5j 0.5+0.5j 0. +0.j ] - [0. +0.j 0. +0.j 0. +0.j 1. +0.j ]] - """ - - num_qubits = 2 - num_params: int = 1 - latex_str = r"{\rm SWAPALPHA}" - - def get_qobj(self): - alpha = self.arg_value[0] - return Qobj( - [ - [1, 0, 0, 0], - [ - 0, - 0.5 * (1 + np.exp(1.0j * np.pi * alpha)), - 0.5 * (1 - np.exp(1.0j * np.pi * alpha)), - 0, - ], - [ - 0, - 0.5 * (1 - np.exp(1.0j * np.pi * alpha)), - 0.5 * (1 + np.exp(1.0j * np.pi * alpha)), - 0, - ], - [0, 0, 0, 1], - ], - dims=[[2, 2], [2, 2]], - ) - - -class MS(AngleParametricGate): - r""" - Mølmer–Sørensen gate. - - .. math:: - - \begin{pmatrix} - \cos(\frac{\theta}{2}) & 0 & 0 & -ie^{-i2\phi}\sin(\frac{\theta}{2}) \\ - 0 & \cos(\frac{\theta}{2}) & -i\sin(\frac{\theta}{2}) & 0 \\ - 0 & -i\sin(\frac{\theta}{2}) & \cos(\frac{\theta}{2}) & 0 \\ - -ie^{i2\phi}\sin(\frac{\theta}{2}) & 0 & 0 & \cos(\frac{\theta}{2}) - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import MS - >>> MS([0, 1], (np.pi/2, 0)).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[0.70711+0.j 0. +0.j 0. +0.j 0. -0.70711j] - [0. +0.j 0.70711+0.j 0. -0.70711j 0. +0.j ] - [0. +0.j 0. -0.70711j 0.70711+0.j 0. +0.j ] - [0. -0.70711j 0. +0.j 0. +0.j 0.70711+0.j ]] - """ - - num_qubits = 2 - num_params: int = 2 - latex_str = r"{\rm MS}" - - def get_qobj(self): - theta, phi = self.arg_value - return Qobj( - [ - [ - np.cos(theta / 2), - 0, - 0, - -1j * np.exp(-1j * 2 * phi) * np.sin(theta / 2), - ], - [0, np.cos(theta / 2), -1j * np.sin(theta / 2), 0], - [0, -1j * np.sin(theta / 2), np.cos(theta / 2), 0], - [ - -1j * np.exp(1j * 2 * phi) * np.sin(theta / 2), - 0, - 0, - np.cos(theta / 2), - ], - ], - dims=[[2, 2], [2, 2]], - ) - - -class RZX(AngleParametricGate): - r""" - RZX gate. - - .. math:: - - \begin{pmatrix} - \cos{\theta/2} & -i\sin{\theta/2} & 0 & 0 \\ - -i\sin{\theta/2} & \cos{\theta/2} & 0 & 0 \\ - 0 & 0 & \cos{\theta/2} & i\sin{\theta/2} \\ - 0 & 0 & i\sin{\theta/2} & \cos{\theta/2} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import RZX - >>> RZX(np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[0.+0.j 0.-1.j 0.+0.j 0.+0.j] - [0.-1.j 0.+0.j 0.+0.j 0.+0.j] - [0.+0.j 0.+0.j 0.+0.j 0.+1.j] - [0.+0.j 0.+0.j 0.+1.j 0.+0.j]] - """ - - num_qubits = 2 - num_params = 1 - latex_str = r"{\rm RZX}" - - def get_qobj(self): - theta = self.arg_value[0] - return Qobj( - np.array( - [ - [np.cos(theta / 2), -1.0j * np.sin(theta / 2), 0.0, 0.0], - [-1.0j * np.sin(theta / 2), np.cos(theta / 2), 0.0, 0.0], - [0.0, 0.0, np.cos(theta / 2), 1.0j * np.sin(theta / 2)], - [0.0, 0.0, 1.0j * np.sin(theta / 2), np.cos(theta / 2)], - ] - ), - dims=[[2, 2], [2, 2]], - ) - - -class CX(_ControlledTwoQubitGate): - """ - CNOT gate. - - Examples - -------- - >>> from qutip_qip.operations import CNOT - >>> CNOT(0, 1).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True - Qobj data = - [[1. 0. 0. 0.] - [0. 1. 0. 0.] - [0. 0. 0. 1.] - [0. 0. 1. 0.]] - """ - - target_gate = X - is_clifford = True - latex_str = r"{\rm CNOT}" - - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], - dims=[[2, 2], [2, 2]], - ) - - -class CNOT(CX): - def __init__(self, control_value=None): - warnings.warn( - "CNOT is deprecated and will be removed in future versions. " - "Use CX instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(control_value) - - -class CY(_ControlledTwoQubitGate): - """ - Controlled CY gate. - - Examples - -------- - >>> from qutip_qip.operations import CY - >>> CY(0, 1).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True - Qobj data = - [[ 1.+0j 0.+0j 0.+0j 0.+0j] - [ 0.+0j 1.+0j 0.+0j 0.+0j] - [ 0.+0j 0.+0j 0.+0j 0.-1j] - [ 0+0j. 0.+0j 0.+1j. 0.+0j]] - """ - - target_gate = Y - is_clifford = True - latex_str = r"{\rm CY}" - - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], - dims=[[2, 2], [2, 2]], - ) - - -class CZ(_ControlledTwoQubitGate): - """ - Controlled Z gate. - - Examples - -------- - >>> from qutip_qip.operations import CZ - >>> CZ(0, 1).get_qobj() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=True - Qobj data = - [[ 1. 0. 0. 0.] - [ 0. 1. 0. 0.] - [ 0. 0. 1. 0.] - [ 0. 0. 0. -1.]] - """ - - target_gate = Z - is_clifford = True - latex_str = r"{\rm CZ}" - - @staticmethod - def get_qobj(): - return Qobj( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], - dims=[[2, 2], [2, 2]], - ) - - -class CSIGN(CZ): - def __init__(self): - warnings.warn( - "CSIGN is deprecated and will be removed in future versions. " - "Use CZ instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__() - - -class CH(_ControlledTwoQubitGate): - r""" - CH gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\theta} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import CH - """ - - target_gate = H - latex_str = r"{\rm CH}" - - @staticmethod - def get_qobj(): - sq_2 = 1 / np.sqrt(2) - return Qobj( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, sq_2, sq_2], - [0, 0, sq_2, -sq_2], - ], - dims=[[2, 2], [2, 2]], - ) - - -class CT(_ControlledTwoQubitGate): - r""" - CT gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\theta} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import CPHASE - """ - - target_gate = T - latex_str = r"{\rm CT}" - - @staticmethod - def get_qobj(): - return Qobj( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, (1 + 1j) / np.sqrt(2)], - ], - dims=[[2, 2], [2, 2]], - ) - - -class CS(_ControlledTwoQubitGate): - r""" - CS gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\theta} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import CPHASE - """ - - target_gate = S - latex_str = r"{\rm CS}" - - @staticmethod - def get_qobj(): - return Qobj( - np.array( - [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1j]] - ), - dims=[[2, 2], [2, 2]], - ) - - -class _ControlledParamTwoQubitGate(ControlledGate, AngleParametricGate): - """ - This class allows correctly generating the gate instance - when a redundant control_value is given, e.g. - ``CNOT(0, 1, control_value=1)``, - and raise an error if it is 0. - """ - - num_qubits: Final[int] = 2 - num_ctrl_qubits: Final[int] = 1 - - -class CPHASE(_ControlledParamTwoQubitGate): - r""" - CPHASE gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\theta} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import CPHASE - >>> CPHASE(0, 1, np.pi/2).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] - [0.+0.j 1.+0.j 0.+0.j 0.+0.j] - [0.+0.j 0.+0.j 1.+0.j 0.+0.j] - [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] - """ - - num_params: int = 1 - target_gate = PHASE - latex_str = r"{\rm CPHASE}" - - def get_qobj(self): - return Qobj( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, np.exp(1j * self.arg_value[0])], - ], - dims=[[2, 2], [2, 2]], - ) - - -class CRX(_ControlledParamTwoQubitGate): - r""" - Controlled X rotation. - - Examples - -------- - >>> from qutip_qip.operations import CRX - """ - - num_params: int = 1 - target_gate = RX - latex_str = r"{\rm CRX}" - - -class CRY(_ControlledParamTwoQubitGate): - r""" - Controlled Y rotation. - - Examples - -------- - >>> from qutip_qip.operations import CRY - """ - - latex_str = r"{\rm CRY}" - target_gate = RY - num_params: int = 1 - - -class CRZ(_ControlledParamTwoQubitGate): - r""" - CRZ gate. - - .. math:: - - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & e^{-i\frac{\theta}{2}} & 0 \\ - 0 & 0 & 0 & e^{i\frac{\theta}{2}} \\ - \end{pmatrix} - - Examples - -------- - >>> from qutip_qip.operations import CRZ - >>> CRZ(0, 1, np.pi).get_qobj().tidyup() # doctest: +NORMALIZE_WHITESPACE - Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False - Qobj data = - [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] - [0.+0.j 1.+0.j 0.+0.j 0.+0.j] - [0.+0.j 0.+0.j 0.-1.j 0.+0.j] - [0.+0.j 0.+0.j 0.+0.j 0.+1.j]] - """ - - num_params: int = 1 - target_gate = RZ - latex_str = r"{\rm CRZ}" - - -class CQASMU(_ControlledParamTwoQubitGate): - r""" - Controlled QASMU rotation. - - Examples - -------- - >>> from qutip_qip.operations import CQASMU - """ - - num_params: int = 3 - target_gate = QASMU - latex_str = r"{\rm CQASMU}" diff --git a/src/qutip_qip/operations/utils.py b/src/qutip_qip/operations/utils.py new file mode 100644 index 000000000..b86c4e5ca --- /dev/null +++ b/src/qutip_qip/operations/utils.py @@ -0,0 +1,531 @@ +from collections.abc import Iterable +from itertools import chain +import numbers + +from qutip import Qobj, identity, tensor +from scipy.linalg import block_diag +import numpy as np +import qutip + + +def controlled_gate_unitary( + U: Qobj, + num_controls: int, + control_value: int, +) -> Qobj: + """ + Create an N-qubit controlled gate from a single-qubit gate U with the given + control and target qubits. + + Parameters + ---------- + U : :class:`qutip.Qobj` + An arbitrary unitary gate. + controls : list of int + The index of the first control qubit. + targets : list of int + The index of the target qubit. + N : int + The total number of qubits. + control_value : int + The decimal value of the controlled qubits that activates the gate U. + + Returns + ------- + result : qobj + Quantum object representing the controlled-U gate. + """ + # Compatibility + num_targets = len(U.dims[0]) + + # First, assume that the last qubit is the target and control qubits are + # in the increasing order. + # The control_value is the location of this unitary. + target_dim = U.shape[0] + block_matrices = [np.eye(target_dim) for _ in range(2**num_controls)] + block_matrices[control_value] = U.full() + + result = block_diag(*block_matrices) + result = Qobj(result, dims=[[2] * (num_controls + num_targets)] * 2) + + # Expand it to N qubits and permute qubits labelling + return result + + +def _check_oper_dims( + oper: Qobj, + dims: Iterable[int] | None = None, + targets: Iterable[int] | None = None, +) -> None: + """ + Check if the given operator is valid. + + Parameters + ---------- + oper : :class:`qutip.Qobj` + The quantum object to be checked. + dims : list, optional + A list of integer for the dimension of each composite system. + e.g ``[2, 2, 2, 2, 2]`` for 5 qubits system. + targets : int or list of int, optional + The indices of subspace that are acted on. + """ + # If operator matches N + if not isinstance(oper, Qobj) or oper.dims[0] != oper.dims[1]: + raise ValueError( + "The operator is not an " + "Qobj with the same input and output dimensions." + ) + + # If operator dims matches the target dims + if dims is not None and targets is not None: + targ_dims = [dims[t] for t in targets] + if oper.dims[0] != targ_dims: + raise ValueError( + f"The operator dims {oper.dims[0]} do not match " + f"the target dims {targ_dims}." + ) + + +def _targets_to_list( + targets: int | Iterable[int], + oper: Qobj | None = None, + N: int | None = None, +) -> list[int]: + """ + transform targets to a list and check validity. + + Parameters + ---------- + targets : int or list of int + The indices of subspace that are acted on. + oper : :class:`qutip.Qobj`, optional + An operator, the type of the :class:`qutip.Qobj` + has to be an operator + and the dimension matches the tensored qubit Hilbert space + e.g. dims = ``[[2, 2, 2], [2, 2, 2]]`` + N : int, optional + The number of subspace in the system. + """ + # if targets is a list of integer + if targets is None: + targets = list(range(len(oper.dims[0]))) + if not isinstance(targets, Iterable): + targets = [targets] + if not all([isinstance(t, numbers.Integral) for t in targets]): + raise TypeError("targets should be an integer or a list of integer") + + # if targets has correct length + if oper is not None: + req_num = len(oper.dims[0]) + if len(targets) != req_num: + raise ValueError( + f"The given operator needs {req_num} " + f"target qubits, but {len(targets)} given." + ) + + # If targets is smaller than N + if N is not None: + if not all([t < N for t in targets]): + raise ValueError(f"Targets must be smaller than N={N}.") + return targets + + +def expand_operator( + oper: Qobj, + dims: Iterable[int], + targets: int | Iterable[int] | None = None, + dtype: str | None = None, +) -> Qobj: + """ + Expand an operator to one that acts on a system with desired dimensions. + + Parameters + ---------- + oper : :class:`qutip.Qobj` + An operator that act on the subsystem, has to be an operator and the + dimension matches the tensored dims Hilbert space + e.g. oper.dims = ``[[2, 3], [2, 3]]`` + dims : list + A list of integer for the dimension of each composite system. + E.g ``[2, 3, 2, 3, 4]``. + targets : int or list of int + The indices of subspace that are acted on. + Permutation can also be realized by changing the orders of the indices. + dtype : str, optional + Data type of the output `Qobj`. + + + Returns + ------- + expanded_oper : :class:`qutip.Qobj` + The expanded operator acting on a system with desired dimension. + + Examples + -------- + >>> import qutip + >>> from qutip_qip.operations import expand_operator + >>> from qutip_qip.operations.gates import X, CX + >>> expand_operator(X.get_qobj(), dims=[2,3], targets=[0]) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 3], [2, 3]], shape=(6, 6), type='oper', dtype=CSR, isherm=True + Qobj data = + [[0. 0. 0. 1. 0. 0.] + [0. 0. 0. 0. 1. 0.] + [0. 0. 0. 0. 0. 1.] + [1. 0. 0. 0. 0. 0.] + [0. 1. 0. 0. 0. 0.] + [0. 0. 1. 0. 0. 0.]] + >>> expand_operator(CX.get_qobj(), dims=[2,2,2], targets=[1, 2]) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True + Qobj data = + [[1. 0. 0. 0. 0. 0. 0. 0.] + [0. 1. 0. 0. 0. 0. 0. 0.] + [0. 0. 0. 1. 0. 0. 0. 0.] + [0. 0. 1. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 1. 0. 0. 0.] + [0. 0. 0. 0. 0. 1. 0. 0.] + [0. 0. 0. 0. 0. 0. 0. 1.] + [0. 0. 0. 0. 0. 0. 1. 0.]] + >>> expand_operator(CX.get_qobj(), dims=[2, 2, 2], targets=[2, 0]) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2, 2, 2], [2, 2, 2]], shape=(8, 8), type='oper', dtype=CSR, isherm=True + Qobj data = + [[1. 0. 0. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 0. 1. 0. 0.] + [0. 0. 1. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 0. 0. 0. 1.] + [0. 0. 0. 0. 1. 0. 0. 0.] + [0. 1. 0. 0. 0. 0. 0. 0.] + [0. 0. 0. 0. 0. 0. 1. 0.] + [0. 0. 0. 1. 0. 0. 0. 0.]] + """ + dtype = dtype or qutip.settings.core["default_dtype"] or qutip.data.CSR + oper = oper.to(dtype) + + if not isinstance(dims, Iterable): + raise ValueError(f"dims needs to be an interable {not type(dims)}.") + + N = len(dims) + targets = _targets_to_list(targets, oper=oper, N=N) + _check_oper_dims(oper, dims=dims, targets=targets) + + # Generate the correct order for permutation, + # eg. if N = 5, targets = [3,0], the order is [1,2,3,0,4]. + # If the operator is cnot, + # this order means that the 3rd qubit controls the 0th qubit. + new_order = [0] * N + for i, t in enumerate(targets): + new_order[t] = i + + # allocate the rest qutbits (not targets) to the empty + # position in new_order + rest_pos = [q for q in list(range(N)) if q not in targets] + rest_qubits = list(range(len(targets), N)) + for i, ind in enumerate(rest_pos): + new_order[ind] = rest_qubits[i] + + id_list = [identity(dims[i]) for i in rest_pos] + out = tensor([oper] + id_list).permute(new_order) + return out.to(dtype) + + +def hadamard_transform(N=1): + """Quantum object representing the N-qubit Hadamard gate. + + Returns + ------- + q : qobj + Quantum object representation of the N-qubit Hadamard gate. + + """ + data = [[1, 1], [1, -1]] + H = Qobj(data) / np.sqrt(2) + + return tensor([H] * N) + + +def _flatten(lst): + """ + Helper to flatten lists. + """ + + return [item for sublist in lst for item in sublist] + + +def _mult_sublists(tensor_list, overall_inds, U, inds): + """ + Calculate the revised indices and tensor list by multiplying a new unitary + U applied to inds. + + Parameters + ---------- + tensor_list : list of Qobj + List of gates (unitaries) acting on disjoint qubits. + + overall_inds : list of list of int + List of qubit indices corresponding to each gate in tensor_list. + + U: Qobj + Unitary to be multiplied with the the unitary specified by tensor_list. + + inds: list of int + List of qubit indices corresponding to U. + + Returns + ------- + tensor_list_revised: list of Qobj + List of gates (unitaries) acting on disjoint qubits incorporating U. + + overall_inds_revised: list of list of int + List of qubit indices corresponding to each gate in tensor_list_revised. + + Examples + -------- + + First, we get some imports out of the way, + + >>> from qutip_qip.operations.gates import _mult_sublists + >>> from qutip_qip.operations.gates import X, Y, Z + + Suppose we have a unitary list of already processed gates, + X, Y, Z applied on qubit indices 0, 1, 2 respectively and + encounter a new TOFFOLI gate on qubit indices (0, 1, 3). + + >>> tensor_list = [X.get_qobj(), Y.get_qobj(), Z.get_qobj()] + >>> overall_inds = [[0], [1], [2]] + >>> U = toffoli() + >>> U_inds = [0, 1, 3] + + Then, we can use _mult_sublists to produce a new list of unitaries by + multiplying TOFFOLI (and expanding) only on the qubit indices involving + TOFFOLI gate (and any multiplied gates). + + >>> U_list, overall_inds = _mult_sublists(tensor_list, overall_inds, U, U_inds) + >>> np.testing.assert_allclose(U_list[0]) == Z.get_qobj()) + >>> toffoli_xy = toffoli() * tensor(X.get_qobj(), Y.get_qobj(), identity(2)) + >>> np.testing.assert_allclose(U_list[1]), toffoli_xy) + >>> overall_inds = [[2], [0, 1, 3]] + """ + + tensor_sublist = [] + inds_sublist = [] + + tensor_list_revised = [] + overall_inds_revised = [] + + for sub_inds, sub_U in zip(overall_inds, tensor_list): + if len(set(sub_inds).intersection(inds)) > 0: + tensor_sublist.append(sub_U) + inds_sublist.append(sub_inds) + else: + overall_inds_revised.append(sub_inds) + tensor_list_revised.append(sub_U) + + inds_sublist = _flatten(inds_sublist) + U_sublist = tensor(tensor_sublist) + + revised_inds = list(set(inds_sublist).union(set(inds))) + N = len(revised_inds) + + sorted_positions = sorted(range(N), key=lambda key: revised_inds[key]) + ind_map = {ind: pos for ind, pos in zip(revised_inds, sorted_positions)} + + U_sublist = expand_operator( + U_sublist, dims=[2] * N, targets=[ind_map[ind] for ind in inds_sublist] + ) + U = expand_operator( + U, dims=[2] * N, targets=[ind_map[ind] for ind in inds] + ) + + U_sublist = U * U_sublist + inds_sublist = revised_inds + + overall_inds_revised.append(inds_sublist) + tensor_list_revised.append(U_sublist) + + return tensor_list_revised, overall_inds_revised + + +def _expand_overall(tensor_list, overall_inds): + """ + Tensor unitaries in tensor list and then use expand_operator to rearrange + them appropriately according to the indices in overall_inds. + """ + + U_overall = tensor(tensor_list) + overall_inds = _flatten(overall_inds) + + # Map indices to a contiguous 0...N-1 range to prevent out-of-bounds in expand_operator + N = len(overall_inds) + sorted_positions = sorted(range(N), key=lambda key: overall_inds[key]) + ind_map = {ind: pos for ind, pos in zip(overall_inds, sorted_positions)} + mapped_targets = [ind_map[ind] for ind in overall_inds] + + U_overall = expand_operator( + U_overall, dims=[2] * len(overall_inds), targets=mapped_targets + ) + overall_inds = sorted(overall_inds) + return U_overall, overall_inds + + +def _gate_sequence_product(U_list, ind_list): + """ + Calculate the overall unitary matrix for a given list of unitary operations + that are still of original dimension. + + Parameters + ---------- + U_list : list of Qobj + List of gates(unitaries) implementing the quantum circuit. + + ind_list : list of list of int + List of qubit indices corresponding to each gate in tensor_list. + + Returns + ------- + U_overall : qobj + Unitary matrix corresponding to U_list. + + overall_inds : list of int + List of qubit indices on which U_overall applies. + + Examples + -------- + + First, we get some imports out of the way, + + >>> from qutip_qip.operations.gates import gate_sequence_product + >>> from qutip_qip.operations.gates import X, Y, Z, TOFFOLI + + Suppose we have a circuit with gates X, Y, Z, TOFFOLI + applied on qubit indices 0, 1, 2 and [0, 1, 3] respectively. + + >>> tensor_lst = [X.get_qobj(), Y.get_qobj(), Z.get_qobj(), TOFFOLI.get_qobj()] + >>> overall_inds = [[0], [1], [2], [0, 1, 3]] + + Then, we can use gate_sequence_product to produce a single unitary + obtained by multiplying unitaries in the list using heuristic methods + to reduce the size of matrices being multiplied. + + >>> U_list, overall_inds = gate_sequence_product(tensor_lst, overall_inds) + """ + if not U_list: + return None, [] + + num_qubits = len(set(chain(*ind_list))) + sorted_inds = sorted(set(_flatten(ind_list))) + ind_list = [[sorted_inds.index(ind) for ind in inds] for inds in ind_list] + + U_overall = None + overall_inds = [] + tensor_list = [] + + for i, (U, inds) in enumerate(zip(U_list, ind_list)): + # when the tensor_list covers the full dimension of the circuit, we + # expand the tensor_list to a unitary and call gate_sequence_product + # recursively on the rest of the U_list. + if len(overall_inds) == 1 and len(overall_inds[0]) == num_qubits: + # FIXME undefined variable tensor_list + U_overall, overall_inds = _expand_overall( + tensor_list, overall_inds + ) + U_left, rem_inds = _gate_sequence_product(U_list[i:], ind_list[i:]) + U_left = expand_operator( + U_left, dims=[2] * num_qubits, targets=rem_inds + ) + return U_left * U_overall, [ + sorted_inds[ind] for ind in overall_inds + ] + + if U_overall is None: + U_overall = U + overall_inds = [ind_list[0]] + tensor_list = [U_overall] + continue + + # case where the next unitary interacts on some subset of qubits + # with the unitaries already in tensor_list. + elif len(set(_flatten(overall_inds)).intersection(set(inds))) > 0: + tensor_list, overall_inds = _mult_sublists( + tensor_list, overall_inds, U, inds + ) + + # case where the next unitary does not interact with any unitary in + # tensor_list + else: + overall_inds.append(inds) + tensor_list.append(U) + + U_overall, overall_inds = _expand_overall(tensor_list, overall_inds) + + return U_overall, [sorted_inds[ind] for ind in overall_inds] + + +def _gate_sequence_product_with_expansion(U_list, left_to_right=True): + """ + Calculate the overall unitary matrix for a given list of unitary + operations, assuming that all operations have the same dimension. + This is only for backward compatibility. + + Parameters + ---------- + U_list : list + List of gates(unitaries) implementing the quantum circuit. + + left_to_right : Boolean + Check if multiplication is to be done from left to right. + + Returns + ------- + U_overall : qobj + Unitary matrix corresponding to U_list. + """ + + if len(U_list) == 0: + raise ValueError("Got an empty U_list") + + U_overall = U_list[0] + for U in U_list[1:]: + if left_to_right: + U_overall = U * U_overall + else: + U_overall = U_overall * U + + return U_overall + + +def gate_sequence_product( + U_list: list[Qobj], + left_to_right: bool = True, + inds_list: list[list[int]] | None = None, + expand: bool = False, +) -> Qobj | tuple[Qobj, list[int]]: + """ + Calculate the overall unitary matrix for a given list of unitary operations. + + Parameters + ---------- + U_list: list + List of gates implementing the quantum circuit. + + left_to_right: Boolean, optional + Check if multiplication is to be done from left to right. + + inds_list: list of list of int, optional + If expand=True, list of qubit indices corresponding to U_list + to which each unitary is applied. + + expand: Boolean, optional + Check if the list of unitaries need to be expanded to full dimension. + + Returns + ------- + U_overall : qobj + Unitary matrix corresponding to U_list. + + overall_inds : list of int, optional + List of qubit indices on which U_overall applies. + """ + if expand: + return _gate_sequence_product(U_list, inds_list) + else: + return _gate_sequence_product_with_expansion(U_list, left_to_right) diff --git a/src/qutip_qip/pulse/evo_element.py b/src/qutip_qip/pulse/evo_element.py index f81f2414a..fd21ceef2 100644 --- a/src/qutip_qip/pulse/evo_element.py +++ b/src/qutip_qip/pulse/evo_element.py @@ -68,7 +68,7 @@ def _get_qobjevo_helper( if self.tlist is None and self.coeff is None: qu = QobjEvo(mat) * 0.0 - elif isinstance(self.coeff, bool): + elif type(self.coeff) is bool: if self.coeff: if self.tlist is None: qu = QobjEvo(mat, tlist=self.tlist) @@ -134,9 +134,7 @@ def get_qobjevo( try: return self._get_qobjevo_helper(spline_kind, dims=dims) except Exception as err: - print( - "The Evolution element went wrong was\n {}".format(str(self)) - ) + print(f"The Evolution element went wrong was\n {str(self)}") raise (err) def __str__(self) -> str: diff --git a/src/qutip_qip/pulse/pulse.py b/src/qutip_qip/pulse/pulse.py index 75b39d859..f78d18b3e 100644 --- a/src/qutip_qip/pulse/pulse.py +++ b/src/qutip_qip/pulse/pulse.py @@ -338,10 +338,8 @@ def print_info(self): if self.label is not None: print("Pulse label:", self.label) print( - "The pulse contains: {} coherent noise elements and {} " - "Lindblad noise elements.".format( - len(self.coherent_noise), len(self.lindblad_noise) - ) + f"The pulse contains: {len(self.coherent_noise)} coherent noise " + f"elements and {len(self.lindblad_noise)} Lindblad noise element." ) print() print("Ideal pulse:") diff --git a/src/qutip_qip/qasm.py b/src/qutip_qip/qasm.py index fe343cd04..5dc65aafb 100644 --- a/src/qutip_qip/qasm.py +++ b/src/qutip_qip/qasm.py @@ -2,16 +2,18 @@ import re import os +import warnings from itertools import chain from copy import deepcopy -import warnings +from collections.abc import Iterable, Sequence +from math import pi # Don't remove +from typing import Type import numpy as np -from math import pi # Don't remove from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import custom_gate_factory -import qutip_qip.operations.std as std +from qutip_qip.operations import Gate, get_unitary_gate +import qutip_qip.operations.gates as gates __all__ = ["read_qasm", "save_qasm", "print_qasm", "circuit_to_qasm_str"] @@ -54,9 +56,7 @@ def _tokenize_line(command): if groups: tokens = ["if", "(", groups.group(1), ")"] tokens_gate = _tokenize_line( - "{} ({}) {}".format( - groups.group(2), groups.group(3), groups.group(4) - ) + f"{groups.group(2)} ({groups.group(3)}) {groups.group(4)}" ) tokens += tokens_gate # for classically controlled gates without arguments @@ -185,6 +185,7 @@ def __init__(self, commands, mode="default", version="2.0"): "crz", "cu1", "cu3", + "swap", ] ) self.predefined_gates = self.predefined_gates.union( @@ -281,10 +282,8 @@ def _initialize_pass(self): elif command[0] == "}": if not curr_gate.gates_inside: raise NotImplementedError( - "QASM: opaque gate {} are \ - not allowed, please define \ - or omit \ - them".format(curr_gate.name) + f"QASM: opaque gate {curr_gate.name} are \ + not allowed, please define or omit them" ) open_bracket_mode = False self.gate_names.add(curr_gate.name) @@ -493,7 +492,7 @@ def _regs_processor(self, regs, reg_type): *list( map( lambda x: ( - x if isinstance(x, list) else [x] * expand + x if isinstance(x, Iterable) else [x] * expand ), new_regs, ) @@ -508,7 +507,7 @@ def _add_qiskit_gates( name, regs, args=None, - classical_controls=[], + classical_controls=(), classical_control_value=None, ): """ @@ -536,14 +535,14 @@ def _add_qiskit_gates( """ gate_name_map_1q = { - "x": std.X, - "y": std.Y, - "z": std.Z, - "h": std.H, - "t": std.T, - "s": std.S, - # "sdg": sdg, - # "tdg": tdg, + "x": gates.X, + "y": gates.Y, + "z": gates.Z, + "h": gates.H, + "t": gates.T, + "s": gates.S, + "sdg": gates.Sdag, + "tdg": gates.Tdag, } if len(args) == 0: args = None @@ -552,50 +551,35 @@ def _add_qiskit_gates( if name == "u3": qc.add_gate( - std.QASMU(args), + gates.QASMU(*args), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "u2": - u2_args = [np.pi / 2, args[0], args[1]] qc.add_gate( - std.QASMU(u2_args), + gates.QASMU(np.pi / 2, args[0], args[1]), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "id": qc.add_gate( - std.IDLE, - targets=regs[0], - classical_controls=classical_controls, - classical_control_value=classical_control_value, - ) - elif name == "sdg": - qc.add_gate( - std.RZ(-np.pi / 2), - targets=regs[0], - classical_controls=classical_controls, - classical_control_value=classical_control_value, - ) - elif name == "tdg": - qc.add_gate( - std.RZ(-np.pi / 4), + gates.IDENTITY, targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "u1": qc.add_gate( - std.RZ(args), + gates.RZ(args), targets=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "cz": qc.add_gate( - std.CZ, + gates.CZ, targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -603,7 +587,7 @@ def _add_qiskit_gates( ) elif name == "cy": qc.add_gate( - std.CY, + gates.CY, targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -611,15 +595,22 @@ def _add_qiskit_gates( ) elif name == "ch": qc.add_gate( - std.CH, + gates.CH, targets=regs[1], controls=regs[0], classical_controls=classical_controls, classical_control_value=classical_control_value, ) + elif name == "swap": + qc.add_gate( + gates.SWAP, + controls=regs, + classical_controls=classical_controls, + classical_control_value=classical_control_value, + ) elif name == "ccx": qc.add_gate( - std.TOFFOLI, + gates.TOFFOLI, targets=regs[2], controls=regs[:2], classical_controls=classical_controls, @@ -627,7 +618,7 @@ def _add_qiskit_gates( ) elif name == "crz": qc.add_gate( - std.CRZ(arg_value = args), + gates.CRZ(args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -635,7 +626,7 @@ def _add_qiskit_gates( ) elif name == "cu1": qc.add_gate( - std.CPHASE(arg_value = args), + gates.CPHASE(args), targets=regs[1], controls=regs[0], classical_controls=classical_controls, @@ -643,7 +634,7 @@ def _add_qiskit_gates( ) elif name == "cu3": qc.add_gate( - std.CQASMU(args), + gates.CQASMU(*args), controls=regs[0], targets=[regs[1]], classical_controls=classical_controls, @@ -651,7 +642,7 @@ def _add_qiskit_gates( ) elif name == "cx": qc.add_gate( - std.CX, + gates.CX, targets=int(regs[1]), controls=int(regs[0]), classical_controls=classical_controls, @@ -659,21 +650,21 @@ def _add_qiskit_gates( ) elif name == "rx": qc.add_gate( - std.RX(args), + gates.RX(args), targets=int(regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "ry": qc.add_gate( - std.RY(args), + gates.RY(args), targets=int(regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, ) elif name == "rz": qc.add_gate( - std.RZ(args), + gates.RZ(args), targets=int(regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, @@ -692,7 +683,7 @@ def _add_predefined_gates( name, com_regs, com_args, - classical_controls=[], + classical_controls=(), classical_control_value=None, ): """ @@ -721,7 +712,7 @@ def _add_predefined_gates( if name == "CX": qc.add_gate( - std.CX, + gates.CX, targets=int(com_regs[1]), controls=int(com_regs[0]), classical_controls=classical_controls, @@ -729,7 +720,7 @@ def _add_predefined_gates( ) elif name == "U": qc.add_gate( - std.QASMU(arg_value=[float(arg) for arg in com_args]), + gates.QASMU(*com_args), targets=int(com_regs[0]), classical_controls=classical_controls, classical_control_value=classical_control_value, @@ -748,7 +739,7 @@ def _gate_add( self, qc, command, - classical_controls=[], + classical_controls=(), classical_control_value=None, ): """ @@ -777,9 +768,9 @@ def _gate_add( reg_set = self._regs_processor(regs, "gate") if args: - gate_name = "{}({})".format(command[0], ",".join(args)) + gate_name = f"{command[0]}({','.join(args)})" else: - gate_name = "{}".format(command[0]) + gate_name = f"{command[0]}" # creates custom-gate (if required) using gate defn and provided args custom_gate_unitary = None @@ -805,12 +796,12 @@ def _gate_add( classical_control_value=classical_control_value, ) else: - if not isinstance(regs, list): + if not isinstance(regs, Sequence): regs = [regs] if custom_gate_unitary is not None: # Instantiate the wrapper gate - gate_obj = custom_gate_factory( + gate_obj = get_unitary_gate( gate_name=gate_name, U=custom_gate_unitary, ) @@ -856,9 +847,7 @@ def _final_pass(self, qc): classical_control_value, ) else: - err = "QASM: {} is not a valid QASM command.".format( - command[0] - ) + err = f"QASM: {command[0]} is not a valid QASM command." raise SyntaxError(err) @@ -946,7 +935,6 @@ def read_qasm(qasm_input, mode="default", version="2.0", strmode=False): "T": "t", "CRZ": "crz", "CX": "cx", - "CNOT": "cx", "TOFFOLI": "ccx", } @@ -1000,19 +988,19 @@ def _qasm_str(self, q_name, q_targets, q_controls=None, q_args=None): q_controls = [] q_regs = q_controls + q_targets - if isinstance(q_targets[0], int): - q_regs = ",".join(["q[{}]".format(reg) for reg in q_regs]) + if type(q_targets[0]) is int: + q_regs = ",".join([f"q[{reg}]" for reg in q_regs]) else: q_regs = ",".join(q_regs) if q_args: - if isinstance(q_args, list): + if isinstance(q_args, Iterable): q_args = ",".join([str(arg) for arg in q_args]) - return "{}({}) {};".format(q_name, q_args, q_regs) + return f"{q_name}({q_args}) {q_regs};" else: - return "{} {};".format(q_name, q_regs) + return f"{q_name} {q_regs};" - def _qasm_defns(self, gate): + def _qasm_defns(self, gate: Gate | Type[Gate]): """ Define QASM gates for QuTiP gates that do not have QASM counterparts. @@ -1022,25 +1010,25 @@ def _qasm_defns(self, gate): QuTiP gate which needs to be defined in QASM format. """ - if gate.name == "CRY": + if type(gate) is gates.CRY: gate_def = "gate cry(theta) a,b { cu3(theta,0,0) a,b; }" - elif gate.name == "CRX": + elif type(gate) is gates.CRX: gate_def = "gate crx(theta) a,b { cu3(theta,-pi/2,pi/2) a,b; }" - elif gate.name == "SQRTX": + elif gate == gates.SQRTX: gate_def = "gate sqrtnot a {h a; u1(-pi/2) a; h a; }" - elif gate.name == "CZ": + elif gate == gates.CZ: gate_def = "gate cz a,b { cu1(pi) a,b; }" - elif gate.name == "CS": + elif gate == gates.CS: gate_def = "gate cs a,b { cu1(pi/2) a,b; }" - elif gate.name == "CT": + elif gate == gates.CT: gate_def = "gate ct a,b { cu1(pi/4) a,b; }" - elif gate.name == "SWAP": + elif gate == gates.SWAP: gate_def = "gate swap a,b { cx a,b; cx b,a; cx a,b; }" else: err_msg = f"No definition specified for {gate.name} gate" raise NotImplementedError(err_msg) - self.output("// QuTiP definition for gate {}".format(gate.name)) + self.output(f"// QuTiP definition for gate {gate.name}") self.output(gate_def) self.gate_name_map[gate.name] = gate.name.lower() @@ -1151,4 +1139,4 @@ def save_qasm(qc, file_loc): lines = qasm_out._qasm_output(qc) with open(file_loc, "w") as f: for line in lines: - f.write("{}\n".format(line)) + f.write(f"{line}\n") diff --git a/src/qutip_qip/qiskit/backend.py b/src/qutip_qip/qiskit/backend.py index f509a6973..fe52783f2 100644 --- a/src/qutip_qip/qiskit/backend.py +++ b/src/qutip_qip/qiskit/backend.py @@ -1,6 +1,7 @@ """Backends for simulating qiskit circuits.""" from abc import abstractmethod +from collections.abc import Sequence import uuid from qiskit.circuit import QuantumCircuit @@ -176,7 +177,7 @@ def run( Job object that stores results and execution data. """ - if not isinstance(run_input, list): + if not isinstance(run_input, Sequence): run_input = [run_input] for circuit in run_input: diff --git a/src/qutip_qip/qiskit/utils/converter.py b/src/qutip_qip/qiskit/utils/converter.py index bf7afa79f..1725d2734 100644 --- a/src/qutip_qip/qiskit/utils/converter.py +++ b/src/qutip_qip/qiskit/utils/converter.py @@ -1,36 +1,45 @@ """Conversion of circuits from qiskit to qutip_qip.""" +from collections.abc import Iterable +from typing import Type from qiskit.circuit import QuantumCircuit from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ( - X, Y, Z, H, S, T, RX, RY, RZ, SWAP, QASMU, PHASE, - CX, CY, CZ, CPHASE, CRX, CRY, CRZ, Gate -) - -# TODO Expand this dictionary for other gates like CS etc. -_map_gates: dict[str, Gate] = { - "p": PHASE, - "x": X, - "y": Y, - "z": Z, - "h": H, - "s": S, - "t": T, - "rx": RX, - "ry": RY, - "rz": RZ, - "swap": SWAP, - "u": QASMU, +from qutip_qip.operations import Gate +import qutip_qip.operations.gates as gates + +# TODO Expand this dictionary for all the valid qiskit gates +# https://quantum.cloud.ibm.com/docs/en/api/qiskit/circuit_library#standard-gates +_map_gates: dict[str, Type[Gate]] = { + "x": gates.X, + "y": gates.Y, + "z": gates.Z, + "h": gates.H, + "s": gates.S, + "sdag": gates.Sdag, + "t": gates.T, + "tdag": gates.Tdag, + "sx": gates.SQRTX, + "sxdag": gates.SQRTXdag, + "rx": gates.RX, + "ry": gates.RY, + "rz": gates.RZ, + "p": gates.PHASE, + "u3": gates.QASMU, + "swap": gates.SWAP, } -_map_controlled_gates: dict[str, Gate] = { - "cx": CX, - "cy": CY, - "cz": CZ, - "crx": CRX, - "cry": CRY, - "crz": CRZ, - "cp": CPHASE, +_map_controlled_gates: dict[str, Type[Gate]] = { + "cx": gates.CX, + "cy": gates.CY, + "cz": gates.CZ, + "ch": gates.CH, + "cs": gates.CS, + "ct": gates.CT, + "crx": gates.CRX, + "cry": gates.CRY, + "crz": gates.CRZ, + "cp": gates.CPHASE, + "cu3": gates.CQASMU, } _ignore_gates: list[str] = ["id", "barrier"] @@ -56,13 +65,13 @@ def get_qutip_index(bit_index: int | list, total_bits: int) -> int: Note ---- When we convert a circuit from qiskit to qutip, - the 0st bit is mapped to the 0th bit and (n-1)th bit to (n-q)th bit - and so on. Essentially the bit order stays the same. + the 0st bit is mapped to the (n-1)th bit and 1st bit to (n-2)th bit + and so on. Essentially the bit order is reversed. """ - if isinstance(bit_index, list): + if isinstance(bit_index, Iterable): return [get_qutip_index(bit, total_bits) for bit in bit_index] else: - return bit_index + return total_bits - 1 - bit_index def _get_mapped_bits(bits: list | tuple, bit_map: dict[int, int]) -> list: @@ -120,8 +129,8 @@ def convert_qiskit_circuit_to_qutip( # add the corresponding gate in qutip_qip if qiskit_instruction.name in _map_gates.keys(): gate = _map_gates[qiskit_instruction.name] - if gate.is_parametric_gate(): - gate = gate(arg_value) + if gate.is_parametric(): + gate = gate(*arg_value) qutip_circuit.add_gate( gate, @@ -129,8 +138,8 @@ def convert_qiskit_circuit_to_qutip( ) elif qiskit_instruction.name in _map_controlled_gates.keys(): - gate = _map_controlled_gates[qiskit_instruction.name] - if gate.is_parametric_gate(): + gate = _map_controlled_gates[qiskit_instruction.name] + if gate.is_parametric(): gate = gate(arg_value) # FIXME This doesn't work for multicontrolled gates diff --git a/src/qutip_qip/qiskit/utils/target_gate_set.py b/src/qutip_qip/qiskit/utils/target_gate_set.py index 6f3d0b4f8..31c907d87 100644 --- a/src/qutip_qip/qiskit/utils/target_gate_set.py +++ b/src/qutip_qip/qiskit/utils/target_gate_set.py @@ -1,4 +1,4 @@ -from qiskit.circuit.gate import Gate +import qiskit from qiskit.circuit.library import ( PhaseGate, XGate, @@ -21,7 +21,7 @@ CPhaseGate, ) -QUTIP_TO_QISKIT_GATE_MAP: dict[str, Gate] = { +QUTIP_TO_QISKIT_GATE_MAP: dict[str, qiskit.circuit.Gate] = { # Single Qubit Gates "PHASEGATE": PhaseGate(theta=0.0), "X": XGate(), diff --git a/src/qutip_qip/transpiler/chain.py b/src/qutip_qip/transpiler/chain.py index 558a2500e..356ac5ea4 100644 --- a/src/qutip_qip/transpiler/chain.py +++ b/src/qutip_qip/transpiler/chain.py @@ -1,5 +1,5 @@ from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import SWAP, RX +from qutip_qip.operations import gates as std def to_chain_structure(qc: QubitCircuit, setup="linear"): @@ -29,12 +29,12 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): qc_t = QubitCircuit(N) qc_t.add_global_phase(qc.global_phase) swap_gates = [ - SWAP, - "ISWAP", - "SQRTISWAP", - "SQRTSWAP", - "BERKELEY", - "SWAPalpha", + std.SWAP, + std.ISWAP, + std.SQRTISWAP, + std.SQRTSWAP, + std.BERKELEY, + std.SWAPALPHA, ] for circ_instruction in qc.instructions: @@ -42,7 +42,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): controls = circ_instruction.controls targets = circ_instruction.targets - if gate.name in ["CNOT", "CX", "CSIGN", "CZ"]: + if gate in [std.CX, std.CSIGN, std.CZ]: start = min([targets[0], controls[0]]) end = max([targets[0], controls[0]]) @@ -67,7 +67,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): # then the required gate if and then another swap # if control and target have one qubit between # them, provided |control-target| is odd. - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) if end == controls[0]: qc_t.add_gate( gate, targets=[i + 1], controls=[i + 2] @@ -76,15 +76,15 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): qc_t.add_gate( gate, targets=[i + 2], controls=[i + 1] ) - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: # Swap the target/s and/or control with their # adjacent qubit to bring them closer. - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) qc_t.add_gate( - SWAP, + std.SWAP, targets=[start + end - i - 1, start + end - i], ) i += 1 @@ -112,7 +112,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): N + start - end - i - i == 2 and (N - end + start + 1) % 2 == 1 ): - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) if end == controls[0]: temp.add_gate( gate, targets=[i + 2], controls=[i + 1] @@ -121,13 +121,13 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): temp.add_gate( gate, targets=[i + 1], controls=[i + 2] ) - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) temp.add_gate( - SWAP, + std.SWAP, targets=[ N + start - end - i - 1, N + start - end - i, @@ -143,7 +143,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): controls = circ_instruction.controls if j < N - end - 2: - if gate.name in ["CNOT", "CX" "CSIGN", "CZ"]: + if gate in [std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=end + targets[0], @@ -158,7 +158,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): ], ) elif j == N - end - 2: - if gate.name in ["CNOT", "CX", "CSIGN", "CZ"]: + if gate in [std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=end + targets[0], @@ -173,7 +173,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): ], ) else: - if gate.name in ["CNOT", "CX", "CSIGN", "CZ"]: + if gate in [std.CX, std.CSIGN, std.CZ]: qc_t.add_gate( gate, targets=(end + targets[0]) % N, @@ -192,7 +192,7 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): elif (end - start) == N - 1: qc_t.add_gate(gate, targets=targets, controls=controls) - elif gate.name in swap_gates: + elif gate in swap_gates: start = min(targets) end = max(targets) @@ -207,15 +207,15 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): elif (start + end - i - i) == 2 and ( end - start + 1 ) % 2 == 1: - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) qc_t.add_gate(gate, targets=[i + 1, i + 2]) - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: - qc_t.add_gate(SWAP, targets=[i, i + 1]) + qc_t.add_gate(std.SWAP, targets=[i, i + 1]) qc_t.add_gate( - SWAP, + std.SWAP, targets=[start + end - i - 1, start + end - i], ) i += 1 @@ -234,15 +234,15 @@ def to_chain_structure(qc: QubitCircuit, setup="linear"): N + start - end - i - i == 2 and (N - end + start + 1) % 2 == 1 ): - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) temp.add_gate(gate, targets=[i + 1, i + 2]) - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) i += 1 else: - temp.add_gate(SWAP, targets=[i, i + 1]) + temp.add_gate(std.SWAP, targets=[i, i + 1]) temp.add_gate( - SWAP, + std.SWAP, targets=[ N + start - end - i - 1, N + start - end - i, diff --git a/src/qutip_qip/typing.py b/src/qutip_qip/typing.py index 3d55aa9e9..b9ed57736 100644 --- a/src/qutip_qip/typing.py +++ b/src/qutip_qip/typing.py @@ -3,7 +3,13 @@ import numpy as np __all__ = [ - "Int", "Real", "Number", "ScalarList", "IntList", "RealList", "ScalarList", "ArrayLike", + "Int", + "Real", + "Number", + "IntSequence", + "RealSequence", + "ScalarSequence", + "ArrayLike", ] # TODO When minimum version is updated to 3.12, use type (PEP 695) in place of TypeAlias @@ -12,8 +18,8 @@ Real: TypeAlias = int | float | np.integer | np.floating Number: TypeAlias = int | float | complex | np.number -IntList = Sequence[Int] -RealList = Sequence[Real] -ScalarList = Sequence[Number] +IntSequence = Sequence[Int] +RealSequence = Sequence[Real] +ScalarSequence = Sequence[Number] ArrayLike = Sequence[any] | np.ndarray diff --git a/src/qutip_qip/utils.py b/src/qutip_qip/utils.py new file mode 100644 index 000000000..cab031421 --- /dev/null +++ b/src/qutip_qip/utils.py @@ -0,0 +1,84 @@ +""" +Module for Helper functions. +""" + +from typing import TypeVar, Sequence +from qutip import Qobj + +__all__ = [ + "valid_unitary", + "check_limit", + "convert_type_input_to_sequence", +] + +T = TypeVar("T") # T can be any type + + +def valid_unitary(gate, num_qubits): + """Verifies input is a valid quantum gate i.e. unitary Qobj. + + Parameters + ---------- + gate : :class:`qutip.Qobj` + The matrix that's supposed to be decomposed should be a Qobj. + num_qubits: + Total number of qubits in the circuit. + Raises + ------ + TypeError + If the gate is not a Qobj. + ValueError + If the gate is not a unitary operator on qubits. + """ + if not isinstance(gate, Qobj): + raise TypeError("The input matrix is not a Qobj.") + + if not gate.isunitary: + raise ValueError("Input is not unitary.") + + if gate.dims != [[2] * num_qubits] * 2: + raise ValueError(f"Input is not a unitary on {num_qubits} qubits.") + + +def check_limit( + input_name: str, input_value: Sequence[T], lower_limit: T, upper_limit: T +): + if len(input_value) == 0: + return + + min_element = min(input_value) + if min_element < lower_limit: + raise ValueError( + f"Each entry of {input_name} must be greater than {lower_limit}, but found {min_element}." + ) + + max_element = max(input_value) + if max_element > upper_limit: + raise ValueError( + f"Each entry of {input_name} must be less than {upper_limit}, but found {max_element}." + ) + + +def convert_type_input_to_sequence( + input_type: T, + input_name: str, + input_value: T | Sequence[T], +) -> Sequence[T]: + if isinstance(input_value, input_type): + return [input_value] + + elif isinstance(input_value, Sequence) and not isinstance( + input_value, str + ): + for i, val in enumerate(input_value): + if not isinstance(val, input_type): + raise TypeError( + f"All elements in '{input_name}' must be {input_type}. " + f"Found {type(val).__name__} ({val}) at index {i}." + ) + return input_value + + else: + raise TypeError( + f"{input_name} must be an {input_type} or sequence of {input_type}, got {input_value}." + ) diff --git a/src/qutip_qip/vqa.py b/src/qutip_qip/vqa.py index f59436b21..7b0d46001 100644 --- a/src/qutip_qip/vqa.py +++ b/src/qutip_qip/vqa.py @@ -2,12 +2,15 @@ import types import random +from collections.abc import Sequence + import numpy as np from qutip import basis, tensor, Qobj, qeye, expect -from qutip_qip.circuit import QubitCircuit from scipy.optimize import minimize from scipy.linalg import expm_frechet -from qutip_qip.operations import gate_sequence_product, custom_gate_factory + +from qutip_qip.circuit import QubitCircuit +from qutip_qip.operations import gate_sequence_product, get_unitary_gate class VQA: @@ -49,12 +52,16 @@ def __init__(self, num_qubits, num_layers=1, cost_method="OBSERVABLE"): if self.num_qubits < 1: raise ValueError("Expected 1 or more qubits") - if not isinstance(self.num_qubits, int): + + if type(self.num_qubits) is not int: raise TypeError("Expected an integer number of qubits") + if self.num_layers < 1: raise ValueError("Expected 1 or more layer") - if not isinstance(self.num_layers, int): + + if type(self.num_layers) is not int: raise TypeError("Expected an integer number of layers") + if self.cost_method not in self._cost_methods: raise ValueError( f"Cost method {self.cost_method} not one of " @@ -136,8 +143,8 @@ def construct_circuit(self, angles): n = block.get_free_parameters_num() current_params = angles[i : i + n] if n > 0 else [] - gate_instance = custom_gate_factory( - gate_name=block.name, + gate_instance = get_unitary_gate( + gate_name=f"{block.name}{layer_num}", U=block.get_unitary(current_params), ) @@ -291,20 +298,22 @@ def optimize_parameters( n_free_params = self.get_free_parameters_num() # Set initial circuit parameters - if isinstance(initial, str): + if type(initial) is str: if initial == "random": angles = [random.random() for i in range(n_free_params)] elif initial == "ones": angles = [1 for i in range(n_free_params)] else: raise ValueError("Invalid initial condition string") - elif isinstance(initial, list) or isinstance(initial, np.ndarray): + + elif isinstance(initial, Sequence): if len(initial) != n_free_params: raise ValueError( f"Expected {n_free_params} initial parameters" f"but got {len(initial)}." ) angles = initial + else: raise ValueError( "Initial conditions were neither a list of values" @@ -493,7 +502,7 @@ class ParameterizedHamiltonian: Hamiltonian term which does not require parameters. """ - def __init__(self, parameterized_terms=[], constant_term=None): + def __init__(self, parameterized_terms=(), constant_term=None): self.p_terms = parameterized_terms self.c_term = constant_term self.num_parameters = len(parameterized_terms) @@ -565,14 +574,18 @@ def __init__( if isinstance(operator, Qobj): if not self.is_unitary: self.num_parameters = 1 - elif isinstance(operator, str): + + elif type(operator) is str: self.is_native_gate = True if targets is None: raise ValueError("Targets must be specified for native gates") + elif isinstance(operator, ParameterizedHamiltonian): self.num_parameters = operator.num_parameters + elif isinstance(operator, types.FunctionType): self.num_parameters = 1 + else: raise ValueError( "operator should be either: Qobj | function which" @@ -610,13 +623,15 @@ def get_unitary(self, angles=None): # Case where the operator is a string referring to an existing gate. if self.is_native_gate: raise TypeError("Can't compute unitary of native gate") + # Function returning Qobj unitary if isinstance(self.operator, types.FunctionType): - # In the future, this could be generalized to multiple angles + # TODO In the future, this could be generalized to multiple angles unitary = self.operator(angles[0]) if not isinstance(unitary, Qobj): raise TypeError("Provided function does not return Qobj") return unitary + # ParameterizedHamiltonian instance if isinstance(self.operator, ParameterizedHamiltonian): return (-1j * self.operator.get_hamiltonian(angles)).expm() @@ -654,6 +669,7 @@ def get_unitary_derivative(self, angles, term_index=0): "Can only take derivative of block specified " "by Hamiltonians or ParameterizedHamiltonian instances." ) + if isinstance(self.operator, ParameterizedHamiltonian): arg = -1j * self.operator.get_hamiltonian(angles) direction = -1j * self.operator.p_terms[term_index] @@ -661,6 +677,7 @@ def get_unitary_derivative(self, angles, term_index=0): expm_frechet(arg.full(), direction.full(), compute_expm=False), dims=direction.dims, ) + if len(angles) != 1: raise ValueError( "Expected a single angle for non-" diff --git a/tests/conftest.py b/tests/conftest.py index 0989992d1..8b572ac87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ def _add_repeats_if_marked(metafunc): metafunc.parametrize( "_repeat_count", range(count), - ids=["rep({})".format(x + 1) for x in range(count)], + ids=[f"rep({x + 1})" for x in range(count)], ) @@ -103,7 +103,7 @@ def _patched_build_err_msg( with np.printoptions(threshold=np.inf): r = r_func(a) except Exception as exc: - r = "[repr failed for <{}>: {}]".format(type(a).__name__, exc) + r = f"[repr failed for <{type(a).__name__}>: {exc}]" # [diff] The original truncates the output to 3 lines here. msg.append(" %s: %s" % (names[i], r)) return "\n".join(msg) diff --git a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py index 1fad30195..ab1c75683 100644 --- a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py +++ b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py @@ -9,7 +9,7 @@ ) from qutip_qip.decompose import decompose_one_qubit_gate from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import H, X, Y, Z, S, T, SQRTX +from qutip_qip.operations.gates import H, X, Y, Z, S, T, SQRTX # Fidelity closer to 1 means the two states are similar to each other target = 0 @@ -17,10 +17,9 @@ # TODO Add a custom gate - rand_unitary(2) + # Tests for private functions -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize( "method", [_ZYZ_rotation, _ZXZ_rotation, _ZYZ_pauli_X] ) @@ -38,9 +37,7 @@ def test_single_qubit_to_rotations(gate, method): assert np.isclose(fidelity_of_input_output, 1.0) -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize("method", ["ZXZ", "ZYZ", "ZYZ_PauliX"]) def test_check_single_qubit_to_decompose_to_rotations(gate, method): """Initial matrix and product of final decompositions are same within some @@ -57,9 +54,7 @@ def test_check_single_qubit_to_decompose_to_rotations(gate, method): assert np.isclose(fidelity_of_input_output, 1.0) -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize( "method", [_ZYZ_rotation, _ZXZ_rotation, _ZYZ_pauli_X] ) @@ -71,9 +66,7 @@ def test_output_is_tuple(gate, method): # Tests for public functions -@pytest.mark.parametrize( - "gate", gate_list -) +@pytest.mark.parametrize("gate", gate_list) @pytest.mark.parametrize("method", ["ZXZ", "ZYZ", "ZYZ_PauliX"]) def test_check_single_qubit_to_decompose_to_rotations_tuple(gate, method): """Initial matrix and product of final decompositions are same within some diff --git a/tests/test_bit_flip.py b/tests/test_bit_flip.py index 0ad163c33..3aa3949ca 100644 --- a/tests/test_bit_flip.py +++ b/tests/test_bit_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import BitFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import X +from qutip_qip.operations.gates import X, CX @pytest.fixture @@ -25,7 +25,7 @@ def test_encode_circuit_structure(code, data_qubits): code.encode_circuit(qc, data_qubits) assert len(qc.instructions) == 2 - assert qc.instructions[0].operation.name == "CX" + assert qc.instructions[0].operation == CX assert qc.instructions[0].controls == (0,) assert qc.instructions[0].targets == (1,) assert qc.instructions[1].controls == (0,) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 56665307d..3c0dc7da8 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -5,8 +5,7 @@ from pathlib import Path -from qutip_qip.circuit import QubitCircuit, CircuitSimulator -from qutip_qip.circuit.draw import TeXRenderer +import qutip from qutip import ( tensor, Qobj, @@ -18,13 +17,20 @@ ket2dm, identity, ) -from qutip_qip.qasm import read_qasm + +from qutip_qip.circuit import ( + QubitCircuit, + CircuitSimulator, + CircuitInstruction, + GateInstruction, + MeasurementInstruction, +) +from qutip_qip.circuit.draw import TeXRenderer +from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation from qutip_qip.operations import Gate, Measurement, gate_sequence_product -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates from qutip_qip.transpiler import to_chain_structure -from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation - -import qutip as qp +from qutip_qip.qasm import read_qasm def _op_dist(A, B): @@ -36,14 +42,14 @@ def _teleportation_circuit(): 3, num_cbits=2, input_states=["q0", "0", "0", "c0", "c1"] ) - teleportation.add_gate(std.H, targets=[1]) - teleportation.add_gate(std.CX, targets=[2], controls=[1]) - teleportation.add_gate(std.CX, targets=[1], controls=[0]) - teleportation.add_gate(std.H, targets=[0]) + teleportation.add_gate(gates.H, targets=[1]) + teleportation.add_gate(gates.CX, targets=[2], controls=[1]) + teleportation.add_gate(gates.CX, targets=[1], controls=[0]) + teleportation.add_gate(gates.H, targets=[0]) teleportation.add_measurement("M0", targets=[0], classical_store=1) teleportation.add_measurement("M1", targets=[1], classical_store=0) - teleportation.add_gate(std.X, targets=[2], classical_controls=[0]) - teleportation.add_gate(std.Z, targets=[2], classical_controls=[1]) + teleportation.add_gate(gates.X, targets=[2], classical_controls=[0]) + teleportation.add_gate(gates.Z, targets=[2], classical_controls=[1]) return teleportation @@ -53,12 +59,12 @@ def _teleportation_circuit2(): 3, num_cbits=2, input_states=["q0", "0", "0", "c0", "c1"] ) - teleportation.add_gate(std.H, targets=[1]) - teleportation.add_gate(std.CX, targets=[2], controls=[1]) - teleportation.add_gate(std.CX, targets=[1], controls=[0]) - teleportation.add_gate(std.H, targets=[0]) - teleportation.add_gate(std.CX, targets=[2], controls=[1]) - teleportation.add_gate(std.CZ, targets=[2], controls=[0]) + teleportation.add_gate(gates.H, targets=[1]) + teleportation.add_gate(gates.CX, targets=[2], controls=[1]) + teleportation.add_gate(gates.CX, targets=[1], controls=[0]) + teleportation.add_gate(gates.H, targets=[0]) + teleportation.add_gate(gates.CX, targets=[2], controls=[1]) + teleportation.add_gate(gates.CZ, targets=[2], controls=[0]) return teleportation @@ -80,13 +86,13 @@ class TestQubitCircuit: @pytest.mark.parametrize( ["gate_from", "gate_to", "targets", "controls"], [ - pytest.param(std.SWAP, "CX", [0, 1], [], id="SWAPtoCX"), - pytest.param(std.ISWAP, "CX", [0, 1], [], id="ISWAPtoCX"), - pytest.param(std.CZ, "CX", [1], [0], id="CZtoCX"), - pytest.param(std.CX, "CZ", [0], [1], id="CXtoCZ"), - pytest.param(std.CX, "SQRTSWAP", [0], [1], id="CXtoSQRTSWAP"), - pytest.param(std.CX, "SQRTISWAP", [0], [1], id="CXtoSQRTISWAP"), - pytest.param(std.CX, "ISWAP", [0], [1], id="CXtoISWAP"), + pytest.param(gates.SWAP, "CX", [0, 1], [], id="SWAPtoCX"), + pytest.param(gates.ISWAP, "CX", [0, 1], [], id="ISWAPtoCX"), + pytest.param(gates.CZ, "CX", [1], [0], id="CZtoCX"), + pytest.param(gates.CX, "CZ", [0], [1], id="CXtoCZ"), + pytest.param(gates.CX, "SQRTSWAP", [0], [1], id="CXtoSQRTSWAP"), + pytest.param(gates.CX, "SQRTISWAP", [0], [1], id="CXtoSQRTISWAP"), + pytest.param(gates.CX, "ISWAP", [0], [1], id="CXtoISWAP"), ], ) def testresolve(self, gate_from, gate_to, targets, controls): @@ -103,7 +109,7 @@ def testHdecompose(self): resolved matrices in terms of rotation gates. """ qc1 = QubitCircuit(1) - qc1.add_gate(std.H, targets=0) + qc1.add_gate(gates.H, targets=0) U1 = qc1.compute_unitary() qc2 = qc1.resolve_gates() U2 = qc2.compute_unitary() @@ -115,7 +121,7 @@ def testFREDKINdecompose(self): resolved matrices in terms of rotation gates and CNOT. """ qc1 = QubitCircuit(3) - qc1.add_gate(std.FREDKIN, targets=[0, 1], controls=[2]) + qc1.add_gate(gates.FREDKIN, targets=[0, 1], controls=[2]) U1 = qc1.compute_unitary() qc2 = qc1.resolve_gates() U2 = qc2.compute_unitary() @@ -126,40 +132,40 @@ def test_add_gate(self): Addition of a gate object directly to a `QubitCircuit` """ qc = QubitCircuit(6) - qc.add_gate(std.CX, targets=[1], controls=[0]) - qc.add_gate(std.SWAP, targets=[1, 4]) - qc.add_gate(std.TOFFOLI, controls=[0, 1], targets=[2]) - qc.add_gate(std.H, targets=[3]) - qc.add_gate(std.SWAP, targets=[1, 4]) - qc.add_gate(std.RY(arg_value=1.570796), targets=4) - qc.add_gate(std.RY(arg_value=1.570796), targets=5) - qc.add_gate(std.RX(arg_value=-1.570796), targets=[3]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) + qc.add_gate(gates.SWAP, targets=[1, 4]) + qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) + qc.add_gate(gates.H, targets=[3]) + qc.add_gate(gates.SWAP, targets=[1, 4]) + qc.add_gate(gates.RY(np.pi / 2), targets=4) + qc.add_gate(gates.RY(np.pi / 2), targets=5) + qc.add_gate(gates.RX(-np.pi / 2), targets=[3]) # Test explicit gate addition - assert qc.instructions[0].operation.name == "CX" + assert qc.instructions[0].operation == gates.CX assert qc.instructions[0].targets == (1,) assert qc.instructions[0].controls == (0,) # Test direct gate addition - assert qc.instructions[1].operation.name == "SWAP" + assert qc.instructions[1].operation == gates.SWAP assert qc.instructions[1].targets == (1, 4) # Test specified position gate addition - assert qc.instructions[3].operation.name == "H" + assert qc.instructions[3].operation == gates.H assert qc.instructions[3].targets == (3,) # Test adding 1 qubit gate on [start, end] qubits - assert qc.instructions[5].operation.name == "RY" + assert isinstance(qc.instructions[5].operation, gates.RY) assert qc.instructions[5].targets == (4,) - assert qc.instructions[5].operation.arg_value[0] == 1.570796 - assert qc.instructions[6].operation.name == "RY" + assert qc.instructions[5].operation.arg_value[0] == np.pi / 2 + assert isinstance(qc.instructions[6].operation, gates.RY) assert qc.instructions[6].targets == (5,) - assert qc.instructions[6].operation.arg_value[0] == 1.570796 + assert qc.instructions[6].operation.arg_value[0] == np.pi / 2 # Test adding 1 qubit gate on qubits [3] - assert qc.instructions[7].operation.name == "RX" + assert isinstance(qc.instructions[7].operation, gates.RX) assert qc.instructions[7].targets == (3,) - assert qc.instructions[7].operation.arg_value[0] == -1.570796 + assert qc.instructions[7].operation.arg_value[0] == -np.pi / 2 class DUMMY1(Gate): num_qubits = 1 @@ -201,15 +207,15 @@ def test_add_circuit(self): """ qc = QubitCircuit(6) - qc.add_gate(std.CX, targets=[1], controls=[0]) - qc.add_gate(std.SWAP, targets=[1, 4]) - qc.add_gate(std.TOFFOLI, controls=[0, 1], targets=[2]) - qc.add_gate(std.H, targets=[3]) - qc.add_gate(std.SWAP, targets=[1, 4]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) + qc.add_gate(gates.SWAP, targets=[1, 4]) + qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) + qc.add_gate(gates.H, targets=[3]) + qc.add_gate(gates.SWAP, targets=[1, 4]) qc.add_measurement("M0", targets=[0], classical_store=[1]) - qc.add_gate(std.RY(1.570796), targets=4) - qc.add_gate(std.RY(1.570796), targets=5) - qc.add_gate(std.CRX(np.pi / 2), controls=[1], targets=[2]) + qc.add_gate(gates.RY(1.570796), targets=4) + qc.add_gate(gates.RY(1.570796), targets=5) + qc.add_gate(gates.CRX(np.pi / 2), controls=[1], targets=[2]) qc1 = QubitCircuit(6) qc1.add_circuit(qc) @@ -228,7 +234,7 @@ def test_add_circuit(self): if qc1.instructions[i].is_gate_instruction() and ( qc.instructions[i].is_gate_instruction() ): - if qc.instructions[i].operation.is_controlled_gate(): + if qc.instructions[i].operation.is_controlled(): assert ( qc1.instructions[i].controls == qc.instructions[i].controls @@ -237,7 +243,10 @@ def test_add_circuit(self): qc1.instructions[i].cbits_ctrl_value == qc.instructions[i].cbits_ctrl_value ) - elif qc1.instructions[i].is_measurement_instruction() and qc.instructions[i].is_measurement_instruction(): + elif ( + qc1.instructions[i].is_measurement_instruction() + and qc.instructions[i].is_measurement_instruction() + ): assert qc1.instructions[i].cbits == qc.instructions[i].cbits # Test exception when qubit out of range @@ -256,7 +265,7 @@ def test_add_circuit(self): qc2.instructions[i].targets[0] == qc.instructions[i].targets[0] + 2 ) - if qc.instructions[i].operation.is_controlled_gate(): + if qc.instructions[i].operation.is_controlled(): assert ( qc2.instructions[i].controls[0] == qc.instructions[i].controls[0] + 2 @@ -306,10 +315,10 @@ def test_add_measurement(self): qc = QubitCircuit(3, num_cbits=3) qc.add_measurement("M0", targets=[0], classical_store=0) - qc.add_gate(std.CX, targets=[1], controls=[0]) - qc.add_gate(std.TOFFOLI, controls=[0, 1], targets=[2]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) + qc.add_gate(gates.TOFFOLI, controls=[0, 1], targets=[2]) qc.add_measurement("M1", targets=[2], classical_store=1) - qc.add_gate(std.H, targets=[1], classical_controls=[0, 1]) + qc.add_gate(gates.H, targets=[1], classical_controls=[0, 1]) qc.add_measurement("M2", targets=[1], classical_store=2) # checking correct addition of measurements @@ -319,11 +328,12 @@ def test_add_measurement(self): assert qc.instructions[5].cbits[0] == 2 # checking if gates are added correctly with measurements - assert qc.instructions[2].operation.name == "TOFFOLI" + assert qc.instructions[2].operation == gates.TOFFOLI assert qc.instructions[4].cbits == (0, 1) - @pytest.mark.skip(reason="Changing the interface completely") - @pytest.mark.parametrize("gate", ["X", "Y", "Z", "S", "T"]) + @pytest.mark.parametrize( + "gate", [gates.X, gates.Y, gates.Z, gates.S, gates.T] + ) def test_exceptions(self, gate): """ Text exceptions are thrown correctly for inadequate inputs @@ -337,25 +347,25 @@ def test_single_qubit_gates(self): """ qc = QubitCircuit(3) - qc.add_gate(std.X, targets=[0]) - qc.add_gate(std.CY, targets=[1], controls=[0]) - qc.add_gate(std.Y, targets=[2]) - qc.add_gate(std.CS, targets=[0], controls=[1]) - qc.add_gate(std.Z, targets=[1]) - qc.add_gate(std.CT, targets=[1], controls=[2]) - qc.add_gate(std.CZ, targets=[0], controls=[1]) - qc.add_gate(std.S, targets=[1]) - qc.add_gate(std.T, targets=[2]) - - assert qc.instructions[8].operation.name == "T" - assert qc.instructions[7].operation.name == "S" - assert qc.instructions[6].operation.name == "CZ" - assert qc.instructions[5].operation.name == "CT" - assert qc.instructions[4].operation.name == "Z" - assert qc.instructions[3].operation.name == "CS" - assert qc.instructions[2].operation.name == "Y" - assert qc.instructions[1].operation.name == "CY" - assert qc.instructions[0].operation.name == "X" + qc.add_gate(gates.X, targets=[0]) + qc.add_gate(gates.CY, targets=[1], controls=[0]) + qc.add_gate(gates.Y, targets=[2]) + qc.add_gate(gates.CS, targets=[0], controls=[1]) + qc.add_gate(gates.Z, targets=[1]) + qc.add_gate(gates.CT, targets=[1], controls=[2]) + qc.add_gate(gates.CZ, targets=[0], controls=[1]) + qc.add_gate(gates.S, targets=[1]) + qc.add_gate(gates.T, targets=[2]) + + assert qc.instructions[8].operation == gates.T + assert qc.instructions[7].operation == gates.S + assert qc.instructions[6].operation == gates.CZ + assert qc.instructions[5].operation == gates.CT + assert qc.instructions[4].operation == gates.Z + assert qc.instructions[3].operation == gates.CS + assert qc.instructions[2].operation == gates.Y + assert qc.instructions[1].operation == gates.CY + assert qc.instructions[0].operation == gates.X assert qc.instructions[8].targets == (2,) assert qc.instructions[7].targets == (1,) @@ -378,10 +388,10 @@ def test_reverse(self): """ qc = QubitCircuit(3, num_cbits=1) - qc.add_gate(std.RX(arg_value=3.141, arg_label=r"\pi/2"), targets=[0]) - qc.add_gate(std.CX, targets=[1], controls=[0]) + qc.add_gate(gates.RX(3.141, arg_label=r"\pi/2"), targets=[0]) + qc.add_gate(gates.CX, targets=[1], controls=[0]) qc.add_measurement("M1", targets=[1], classical_store=0) - qc.add_gate(std.H, targets=[2]) + qc.add_gate(gates.H, targets=[2]) # Keep input output same qc.add_state("0", targets=[0]) @@ -390,10 +400,10 @@ def test_reverse(self): qc_rev = qc.reverse_circuit() - assert qc_rev.instructions[0].operation.name == "H" + assert qc_rev.instructions[0].operation == gates.H assert qc_rev.instructions[1].operation.name == "M1" - assert qc_rev.instructions[2].operation.name == "CX" - assert qc_rev.instructions[3].operation.name == "RX" + assert qc_rev.instructions[2].operation == gates.CX + assert isinstance(qc_rev.instructions[3].operation, gates.RX) assert qc_rev.input_states[0] == "0" assert qc_rev.input_states[2] is None @@ -407,7 +417,7 @@ def test_user_gate(self): def customer_gate1(arg_values): mat = np.zeros((4, 4), dtype=np.complex128) mat[0, 0] = mat[1, 1] = 1.0 - mat[2:4, 2:4] = std.RX(arg_values).get_qobj().full() + mat[2:4, 2:4] = gates.RX(arg_values).get_qobj().full() return Qobj(mat, dims=[[2, 2], [2, 2]]) class T1(Gate): @@ -423,7 +433,7 @@ def get_qobj(): return Qobj(mat, dims=[[2], [2]]) qc = QubitCircuit(3) - qc.add_gate(std.CRX(np.pi / 2), targets=[2], controls=[1]) + qc.add_gate(gates.CRX(np.pi / 2), targets=[2], controls=[1]) qc.add_gate(T1, targets=[1]) props = qc.propagators() result1 = tensor(identity(2), customer_gate1(np.pi / 2)) @@ -435,7 +445,7 @@ def test_N_level_system(self): """ Test for circuit with N-level system. """ - mat3 = qp.rand_unitary(3) + mat3 = qutip.rand_unitary(3) class CTRLMAT3(Gate): num_qubits = 2 @@ -457,12 +467,12 @@ def get_qobj(): qc = QubitCircuit(2, dims=[3, 2]) qc.add_gate(CTRLMAT3, targets=[1, 0]) props = qc.propagators() - final_fid = qp.average_gate_fidelity(mat3, ptrace(props[0], 0) - 1) + final_fid = qutip.average_gate_fidelity(mat3, ptrace(props[0], 0) - 1) assert pytest.approx(final_fid, 1.0e-6) == 1 init_state = basis([3, 2], [0, 1]) result = qc.run(init_state) - final_fid = qp.fidelity(result, props[0] * init_state) + final_fid = qutip.fidelity(result, props[0] * init_state) assert pytest.approx(final_fid, 1.0e-6) == 1.0 @pytest.mark.repeat(10) @@ -494,24 +504,24 @@ def test_run_teleportation(self): def test_classical_control(self): qc = QubitCircuit(1, num_cbits=2) qc.add_gate( - std.X, + gates.X, targets=[0], classical_controls=[0, 1], classical_control_value=1, ) result = qc.run(basis(2, 0), cbits=[1, 0]) - fid = qp.fidelity(result, basis(2, 0)) + fid = qutip.fidelity(result, basis(2, 0)) assert pytest.approx(fid, 1.0e-6) == 1 qc = QubitCircuit(1, num_cbits=2) qc.add_gate( - std.X, + gates.X, targets=[0], classical_controls=[0, 1], classical_control_value=2, ) result = qc.run(basis(2, 0), cbits=[1, 0]) - fid = qp.fidelity(result, basis(2, 1)) + fid = qutip.fidelity(result, basis(2, 1)) assert pytest.approx(fid, 1.0e-6) == 1 def test_runstatistics_teleportation(self): @@ -570,19 +580,19 @@ def test_measurement_circuit(self): def test_circuit_with_selected_measurement_result(self): qc = QubitCircuit(num_qubits=1, num_cbits=1) - qc.add_gate(std.H, targets=0) + qc.add_gate(gates.H, targets=0) qc.add_measurement("M0", targets=0, classical_store=0) # We reset the random seed so that # if we don's select the measurement result, # the two circuit should return the same value. np.random.seed(0) - final_state = qc.run(qp.basis(2, 0), cbits=[0], measure_results=[0]) - fid = pytest.approx(qp.fidelity(final_state, basis(2, 0))) + final_state = qc.run(qutip.basis(2, 0), cbits=[0], measure_results=[0]) + fid = pytest.approx(qutip.fidelity(final_state, basis(2, 0))) assert fid == 1.0 np.random.seed(0) - final_state = qc.run(qp.basis(2, 0), cbits=[0], measure_results=[1]) - fid = pytest.approx(qp.fidelity(final_state, basis(2, 1))) + final_state = qc.run(qutip.basis(2, 0), cbits=[0], measure_results=[1]) + fid = pytest.approx(qutip.fidelity(final_state, basis(2, 1))) assert fid == 1.0 def test_gate_product(self): @@ -666,7 +676,7 @@ def test_latex_code_teleportation_circuit(self): def test_latex_code_classical_controls(self): qc = QubitCircuit(1, num_cbits=1, reverse_states=True) - qc.add_gate(std.X, targets=0, classical_controls=[0]) + qc.add_gate(gates.X, targets=0, classical_controls=[0]) renderer = TeXRenderer(qc) latex = TeXRenderer(qc).latex_code() assert latex == renderer._latex_template % "\n".join( @@ -678,7 +688,7 @@ def test_latex_code_classical_controls(self): ) qc = QubitCircuit(1, num_cbits=1, reverse_states=False) - qc.add_gate(std.X, targets=0, classical_controls=[0]) + qc.add_gate(gates.X, targets=0, classical_controls=[0]) renderer = TeXRenderer(qc) latex = TeXRenderer(qc).latex_code() assert latex == renderer._latex_template % "\n".join( @@ -696,14 +706,17 @@ def test_latex_code_classical_controls(self): H_zyz_quantum_circuit = QubitCircuit(1) for g in H_zyz_gates: H_zyz_quantum_circuit.add_gate(g, targets=[0]) # TODO CHECK - sigmax_zyz_gates = _ZYZ_rotation(std.X.get_qobj()) + sigmax_zyz_gates = _ZYZ_rotation(gates.X.get_qobj()) sigmax_zyz_quantum_circuit = QubitCircuit(1) for g in sigmax_zyz_gates: sigmax_zyz_quantum_circuit.add_gate(g, targets=[0]) @pytest.mark.parametrize( "valid_input, correct_result", - [(H_zyz_quantum_circuit, H), (sigmax_zyz_quantum_circuit, std.X.get_qobj())], + [ + (H_zyz_quantum_circuit, H), + (sigmax_zyz_quantum_circuit, gates.X.get_qobj()), + ], ) def test_compute_unitary(self, valid_input, correct_result): final_output = valid_input.compute_unitary() @@ -734,10 +747,10 @@ def test_latex_code_non_reversed(self): shutil.which("pdflatex") is None, reason="requires pdflatex" ) def test_export_image(self, in_temporary_directory): - from qutip_qip.circuit.texrenderer import CONVERTERS + from qutip_qip.circuit.draw import CONVERTERS qc = QubitCircuit(2, reverse_states=False) - qc.add_gate(std.CZ, controls=[0], targets=[1]) + qc.add_gate(gates.CZ, controls=[0], targets=[1]) if "png" in CONVERTERS: file_png200 = "exported_pic_200.png" @@ -757,8 +770,217 @@ def test_circuit_chain_structure(self): Test if the transpiler correctly inherit the properties of a circuit. """ qc = QubitCircuit(3, reverse_states=True) - qc.add_gate(std.CX, targets=[2], controls=[0]) + qc.add_gate(gates.CX, targets=[2], controls=[0]) qc2 = to_chain_structure(qc) assert qc2.reverse_states is True assert qc2.input_states == [None] * 3 + + +class TestAddGateError: + def test_add_gate_errors(self): + qc = QubitCircuit(3, num_cbits=1) + + with pytest.raises(KeyError): + qc.add_gate("123") # Can only pass standard gate name as strings + + with pytest.raises(TypeError): + + class BadGate: ... # Doesn't inherit for Gate + + qc.add_gate(BadGate) + + with pytest.raises(TypeError): + + class AbstractGate( + Gate + ): ... # Doesn't define num_qubits, get_qobj + + qc.add_gate(AbstractGate) + + with pytest.raises(TypeError): + qc.add_gate(gates.RX, targets=[0]) # RX must be initialized + + with pytest.raises(TypeError): + qc.add_gate(gates.X(), targets=[0]) # X can't be initialized + + with pytest.raises(ValueError): + qc.add_gate( + gates.CX, targets=[], controls=[] + ) # targets, controls are empty + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[0], controls=[]) + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[1, 2], controls=[]) + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[1], controls=[-1]) + + with pytest.raises(ValueError): + qc.add_gate(gates.CX, targets=[-1], controls=[1]) + + with pytest.raises(ValueError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[-1], # Each entry must be non -negative + classical_control_value=1, + ) + + with pytest.raises(TypeError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[0], + classical_control_value="1", # Can't be string + ) + + with pytest.raises(ValueError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[0], + classical_control_value=2, # Incorrect + ) + + with pytest.raises(ValueError): + qc.add_gate( + gates.X, + targets=[0], + classical_controls=[0], + classical_control_value=-1, # Can't be negative + ) + + +class TestInstructionErrors: + def test_instruction_post_init_errors(self): + class MockInstruction(CircuitInstruction): + def to_qasm(self, qasm_out): + pass + + def __str__(self): + return "" + + with pytest.raises(ValueError): + # Circuit Instruction must operate on at least one qubit or cbit + MockInstruction("gate", qubits=(), cbits=()) + + with pytest.raises(TypeError): + MockInstruction( + "gate", qubits=[0], cbits=() + ) # Must pass a tuple for qubits + + with pytest.raises(ValueError): + MockInstruction( + "gate", qubits=(0.5,), cbits=() + ) # All qubit indices must be an int + + with pytest.raises(ValueError): + MockInstruction( + "gate", qubits=(-1,), cbits=() + ) # qubit indices must be non-negative + + with pytest.raises(ValueError, match="Found repeated qubits"): + MockInstruction("gate", qubits=(0, 0), cbits=()) + + with pytest.raises(ValueError, match="Found repeated cbits"): + MockInstruction("gate", qubits=(0,), cbits=(0, 0)) + + def test_gate_instruction_errors(self): + with pytest.raises(TypeError): + GateInstruction(operation="not a gate", qubits=(0,)) + + with pytest.raises(ValueError): + GateInstruction( + operation=gates.X, qubits=(0, 1) + ) # requires 1 qubits + + with pytest.raises(ValueError): + GateInstruction(operation=gates.X, qubits=(0,), cbits=(0,)) + # cbits_ctrl_value can't be None if classical controls are provided + + with pytest.raises(ValueError): + # Classical Control value can't be negative + GateInstruction( + operation=gates.X, qubits=(0,), cbits=(0,), cbits_ctrl_value=-1 + ) + + with pytest.raises(ValueError): + # Classical Control value can't be greater than 1 in this case + GateInstruction( + operation=gates.X, qubits=(0,), cbits=(0,), cbits_ctrl_value=2 + ) + + def test_measurement_instruction_errors(self): + with pytest.raises(TypeError): + # Operation must be of type Measurement + MeasurementInstruction(operation="M0", qubits=(0,), cbits=(0,)) + + meas = Measurement("M0", targets=[0], classical_store=0) + with pytest.raises(ValueError): + # Measurement requires equal number of qubits and cbits + MeasurementInstruction(operation=meas, qubits=(0, 1), cbits=(0,)) + + +def test_gates_class(): + init_state = qutip.rand_ket([2, 2, 2]) + + circuit1 = QubitCircuit(3) + circuit1.add_gate(gates.X, targets=1) + circuit1.add_gate(gates.Y, targets=1) + circuit1.add_gate(gates.Z, targets=2) + circuit1.add_gate(gates.RX(np.pi / 4), targets=0) + circuit1.add_gate(gates.RY(np.pi / 4), targets=0) + circuit1.add_gate(gates.RZ(np.pi / 4), targets=1) + circuit1.add_gate(gates.H, targets=0) + circuit1.add_gate(gates.SQRTX, targets=0) + circuit1.add_gate(gates.S, targets=2) + circuit1.add_gate(gates.T, targets=1) + circuit1.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) + circuit1.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) + circuit1.add_gate(gates.CX, controls=0, targets=1) + circuit1.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) + circuit1.add_gate(gates.SWAP, targets=[0, 1]) + circuit1.add_gate(gates.ISWAP, targets=[2, 1]) + circuit1.add_gate(gates.CZ, controls=[0], targets=[2]) + circuit1.add_gate(gates.SQRTSWAP, [2, 0]) + circuit1.add_gate(gates.SQRTISWAP, [0, 1]) + circuit1.add_gate(gates.SWAPALPHA(np.pi / 4), [1, 2]) + circuit1.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) + circuit1.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) + circuit1.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) + circuit1.add_gate(gates.BERKELEY, targets=[1, 0]) + circuit1.add_gate(gates.RZX(1.0), targets=[1, 0]) + result1 = circuit1.run(init_state) + + circuit2 = QubitCircuit(3) + circuit2.add_gate(gates.X, targets=1) + circuit2.add_gate(gates.Y, targets=1) + circuit2.add_gate(gates.Z, targets=2) + circuit2.add_gate(gates.RX(np.pi / 4), targets=0) + circuit2.add_gate(gates.RY(np.pi / 4), targets=0) + circuit2.add_gate(gates.RZ(np.pi / 4), targets=1) + circuit2.add_gate(gates.H, targets=0) + circuit2.add_gate(gates.SQRTX, targets=0) + circuit2.add_gate(gates.S, targets=2) + circuit2.add_gate(gates.T, targets=1) + circuit2.add_gate(gates.R(np.pi / 4, np.pi / 6), targets=1) + circuit2.add_gate(gates.QASMU(np.pi / 4, np.pi / 4, np.pi / 4), targets=0) + circuit2.add_gate(gates.CX, controls=0, targets=1) + circuit2.add_gate(gates.CPHASE(np.pi / 4), controls=0, targets=1) + circuit2.add_gate(gates.SWAP, targets=[0, 1]) + circuit2.add_gate(gates.ISWAP, targets=[2, 1]) + circuit2.add_gate(gates.CZ, controls=[0], targets=[2]) + circuit2.add_gate(gates.SQRTSWAP, targets=[2, 0]) + circuit2.add_gate(gates.SQRTISWAP, targets=[0, 1]) + circuit2.add_gate(gates.SWAPALPHA(np.pi / 4), targets=[1, 2]) + circuit2.add_gate(gates.MS(np.pi / 4, np.pi / 7), targets=[1, 0]) + circuit2.add_gate(gates.TOFFOLI, controls=[2, 0], targets=[1]) + circuit2.add_gate(gates.FREDKIN, controls=[0], targets=[1, 2]) + circuit2.add_gate(gates.BERKELEY, targets=[1, 0]) + circuit2.add_gate(gates.RZX(1.0), targets=[1, 0]) + result2 = circuit2.run(init_state) + + assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 diff --git a/tests/test_compiler.py b/tests/test_compiler.py index f3c26876a..101079355 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -15,7 +15,8 @@ GateCompiler, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ParametricGate, X, RX +from qutip_qip.operations import AngleParametricGate +from qutip_qip.operations.gates import X, RX from qutip import basis, fidelity @@ -55,8 +56,8 @@ def test_compiling_gates_different_sampling_number(): class MockCompiler(GateCompiler): def __init__(self, num_qubits, params=None): super().__init__(num_qubits, params=params) - self.gate_compiler["U1"] = self.single_qubit_gate_compiler - self.gate_compiler["U2"] = self.two_qubit_gate_compiler + self.gate_compiler[U1] = self.single_qubit_gate_compiler + self.gate_compiler[U2] = self.two_qubit_gate_compiler self.args.update({"params": params}) def single_qubit_gate_compiler(self, circuit_instruction, args): @@ -79,31 +80,25 @@ def two_qubit_gate_compiler(self, circuit_instruction, args): ) ] - class U1(ParametricGate): + class U1(AngleParametricGate): num_qubits = 1 num_params = 1 self_inverse = False - def get_qobj(self): + def compute_qobj(args, dtype): pass - def validate_params(self, arg_value): - pass - - class U2(ParametricGate): + class U2(AngleParametricGate): num_qubits = 2 num_params = 1 self_inverse = False - def get_qobj(self): - pass - - def validate_params(self, arg_value): + def compute_qobj(args, dtype): pass num_qubits = 2 circuit = QubitCircuit(num_qubits) - circuit.add_gate(U1(arg_value=1.0), targets=0) + circuit.add_gate(U1(1.0), targets=0) circuit.add_gate(U2(1.0), targets=[0, 1]) circuit.add_gate(U1(1.0), targets=0) @@ -127,7 +122,7 @@ class MyCompiler(GateCompiler): # compiler class def __init__(self, num_qubits, params): super().__init__(num_qubits, params=params) # pass our compiler function as a compiler for RX (rotation around X) gate. - self.gate_compiler["RX"] = self.rx_compiler + self.gate_compiler[RX] = self.rx_compiler self.args.update({"params": params}) def rx_compiler(self, circuit_instruction, args): @@ -137,7 +132,10 @@ def rx_compiler(self, circuit_instruction, args): 1000, maximum=args["params"]["sx"][targets[0]], # The operator is Pauli Z/X/Y, without 1/2. - area=circuit_instruction.operation.arg_value[0] / 2.0 / np.pi * 0.5, + area=circuit_instruction.operation.arg_value[0] + / 2.0 + / np.pi + * 0.5, ) pulse_info = [("sx" + str(targets[0]), coeff)] return [PulseInstruction(circuit_instruction, tlist, pulse_info)] @@ -199,7 +197,7 @@ def test_compiler_without_pulse_dict(): compiler = SpinChainCompiler( num_qubits, params=processor.params, setup="circular" ) - compiler.gate_compiler["RX"] = rx_compiler_without_pulse_dict + compiler.gate_compiler[RX] = rx_compiler_without_pulse_dict compiler.args = {"params": processor.params} processor.load_circuit(circuit, compiler=compiler) result = processor.run_state(basis([2, 2], [0, 0])) @@ -231,11 +229,11 @@ def test_compiler_result_format(): assert_array_equal(processor.pulses[0].coeff, coeffs["sx0"]) assert_array_equal(processor.pulses[0].tlist, tlist["sx0"]) - compiler.gate_compiler["RX"] = rx_compiler_without_pulse_dict + compiler.gate_compiler[RX] = rx_compiler_without_pulse_dict tlist, coeffs = compiler.compile(circuit) - assert isinstance(tlist, dict) + assert type(tlist) is dict assert 0 in tlist - assert isinstance(coeffs, dict) + assert type(coeffs) is dict assert 0 in coeffs processor.coeffs = coeffs processor.set_all_tlist(tlist) diff --git a/tests/test_device.py b/tests/test_device.py index 37ebb43ba..fd242cca5 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -9,7 +9,8 @@ import qutip from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import ( +from qutip_qip.operations import gate_sequence_product +from qutip_qip.operations.gates import ( X, Y, Z, @@ -22,7 +23,6 @@ ISWAP, SQRTISWAP, RZX, - gate_sequence_product, ) from qutip_qip.device import ( DispersiveCavityQED, @@ -34,9 +34,9 @@ _tol = 3.0e-2 num_qubits = 2 -_rx = RX(arg_value=np.pi / 2, arg_label=r"\pi/2") -_ry = RY(arg_value=np.pi / 2, arg_label=r"\pi/2") -_rz = RZ(arg_value=np.pi / 2, arg_label=r"\pi/2") +_rx = RX(np.pi / 2, arg_label=r"\pi/2") +_ry = RY(np.pi / 2, arg_label=r"\pi/2") +_rz = RZ(np.pi / 2, arg_label=r"\pi/2") single_gate_tests = [ @@ -127,7 +127,7 @@ def test_numerical_evolution(num_qubits, gates, targets, device_class, kwargs): # Test for RZX gate, only available on SCQubits. -_rzx = RZX(arg_value=np.pi / 2) +_rzx = RZX(np.pi / 2) @pytest.mark.parametrize( @@ -155,21 +155,25 @@ def _test_numerical_evolution_helper( state = qutip.rand_ket(2**num_qubits) state.dims = [[2] * num_qubits, [1] * num_qubits] target = circuit.run(state) + if isinstance(device, DispersiveCavityQED): num_ancilla = len(device.dims) - num_qubits ancilla_indices = slice(0, num_ancilla) extra = qutip.basis(device.dims[ancilla_indices], [0] * num_ancilla) init_state = qutip.tensor(extra, state) + elif isinstance(device, SCQubits): # expand to 3-level represetnation init_state = _ket_expaned_dims(state, device.dims) else: init_state = state + options = {"store_final_state": True, "nsteps": 50000} result = device.run_state( init_state=init_state, analytical=False, options=options ) numerical_result = result.final_state + if isinstance(device, DispersiveCavityQED): target = qutip.tensor(extra, target) elif isinstance(device, SCQubits): @@ -184,7 +188,7 @@ def _test_numerical_evolution_helper( circuit.add_gate(ISWAP, targets=[2, 1]) circuit.add_gate(Y, targets=[2]) circuit.add_gate(Z, targets=[0]) -circuit.add_gate(IDLE, targets=[1]) +circuit.add_gate(IDLE(0), targets=[1]) circuit.add_gate(CX, targets=[0], controls=[2]) circuit.add_gate(Z, targets=[1]) circuit.add_gate(X, targets=[1]) @@ -230,10 +234,12 @@ def test_numerical_circuit(circuit, device_class, kwargs, schedule_mode): init_state = _ket_expaned_dims(state, device.dims) else: init_state = state + options = {"store_final_state": True, "nsteps": 50000} result = device.run_state( init_state=init_state, analytical=False, options=options ) + if isinstance(device, DispersiveCavityQED): target = qutip.tensor(extra, target) elif isinstance(device, SCQubits): diff --git a/tests/test_gates.py b/tests/test_gates.py index e8eccfb7b..683108bfc 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -1,12 +1,26 @@ from copy import deepcopy +from typing import Type import pytest import itertools + import numpy as np import qutip -from qutip_qip.operations import gates -from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import Gate, expand_operator -import qutip_qip.operations.std as std + +from qutip_qip.operations import ( + AngleParametricGate, + ControlledGate, + Gate, + ParametricGate, + NameSpace, + get_controlled_gate, + expand_operator, + get_unitary_gate, + hadamard_transform, + qubit_clifford_group, +) +import qutip_qip.operations.gates as gates + +rng = np.random.default_rng(seed=101) def _permutation_id(permutation): @@ -18,20 +32,6 @@ def _infidelity(a, b): return 1 - abs(a.overlap(b)) -def _make_random_three_qubit_gate(): - """Create a random three-qubit gate.""" - operation = qutip.rand_unitary([2] * 3) - - def gate(N=None, controls=None, target=None): - if N is None: - return operation - return expand_operator( - operation, dims=[2] * N, targets=controls + [target] - ) - - return gate - - def _tensor_with_entanglement(all_qubits, entangled, entangled_locations): """ Create a full tensor product when a subspace component is already in an @@ -93,7 +93,7 @@ def test_swap(self): states = [qutip.rand_ket(2) for _ in [None] * 2] start = qutip.tensor(states) swapped = qutip.tensor(states[::-1]) - swap = std.SWAP.get_qobj() + swap = gates.SWAP.get_qobj() assert _infidelity(swapped, swap * start) < 1e-12 assert _infidelity(start, swap * swap * start) < 1e-12 @@ -104,7 +104,7 @@ def test_swap(self): ) def test_toffoli(self, permutation): test = expand_operator( - std.TOFFOLI.get_qobj(), dims=[2] * 3, targets=permutation + gates.TOFFOLI.get_qobj(), dims=[2] * 3, targets=permutation ) base = qutip.tensor( 1 - qutip.basis([2, 2], [1, 1]).proj(), qutip.qeye(2) @@ -129,22 +129,22 @@ def test_toffoli(self, permutation): ) def test_molmer_sorensen(self, angle, expected): np.testing.assert_allclose( - std.MS(angle).get_qobj().full(), expected.full(), atol=1e-15 + gates.MS(*angle).get_qobj().full(), expected.full(), atol=1e-15 ) @pytest.mark.parametrize( ["gate", "n_angles"], [ - pytest.param(std.RX, 1, id="Rx"), - pytest.param(std.RY, 1, id="Ry"), - pytest.param(std.RZ, 1, id="Rz"), - pytest.param(std.PHASE, 1, id="phase"), - pytest.param(std.R, 2, id="Rabi rotation"), + pytest.param(gates.RX, 1, id="Rx"), + pytest.param(gates.RY, 1, id="Ry"), + pytest.param(gates.RZ, 1, id="Rz"), + pytest.param(gates.PHASE, 1, id="phase"), + pytest.param(gates.R, 2, id="Rabi rotation"), ], ) def test_zero_rotations_are_identity(self, gate, n_angles): np.testing.assert_allclose( - np.eye(2), gate([0] * n_angles).get_qobj().full(), atol=1e-15 + np.eye(2), gate(*[0] * n_angles).get_qobj().full(), atol=1e-15 ) @@ -154,7 +154,8 @@ class TestCliffordGroup: group for a single qubit. """ - clifford = list(gates.qubit_clifford_group()) + with pytest.warns(DeprecationWarning): + clifford = list(qubit_clifford_group()) pauli = [qutip.qeye(2), qutip.sigmax(), qutip.sigmay(), qutip.sigmaz()] def test_single_qubit_group_dimension_is_24(self): @@ -168,7 +169,7 @@ def test_all_elements_different(self): fid = qutip.average_gate_fidelity(gate, other) assert not np.allclose(fid, 1.0, atol=1e-3) - @pytest.mark.parametrize("gate", gates.qubit_clifford_group()) + @pytest.mark.parametrize("gate", clifford) def test_gate_normalises_pauli_group(self, gate): """ Test the fundamental definition of the Clifford group, i.e. that it @@ -201,29 +202,35 @@ class TestGateExpansion: @pytest.mark.parametrize( ["gate", "n_angles"], [ - pytest.param(std.RX, 1, id="Rx"), - pytest.param(std.RY, 1, id="Ry"), - pytest.param(std.RZ, 1, id="Rz"), - pytest.param(std.X, 0, id="X"), - pytest.param(std.Y, 0, id="Y"), - pytest.param(std.Z, 0, id="Z"), - pytest.param(std.S, 0, id="S"), - pytest.param(std.T, 0, id="T"), - pytest.param(std.PHASE, 1, id="phase"), - pytest.param(std.R, 2, id="Rabi rotation"), + pytest.param(gates.RX, 1, id="Rx"), + pytest.param(gates.RY, 1, id="Ry"), + pytest.param(gates.RZ, 1, id="Rz"), + pytest.param(gates.X, 0, id="X"), + pytest.param(gates.Y, 0, id="Y"), + pytest.param(gates.Z, 0, id="Z"), + pytest.param(gates.S, 0, id="S"), + pytest.param(gates.T, 0, id="T"), + pytest.param(gates.PHASE, 1, id="phase"), + pytest.param(gates.R, 2, id="Rabi rotation"), ], ) - def test_single_qubit_rotation(self, gate: Gate, n_angles: int): + def test_single_qubit_rotation(self, gate: Type[Gate], n_angles: int): base = qutip.rand_ket(2) if n_angles > 0: - gate = gate(2 * np.pi * np.random.rand(n_angles)) + angles = 2 * np.pi * (np.random.rand(n_angles)) + gate = gate(*angles) applied = gate.get_qobj() * base random = [qutip.rand_ket(2) for _ in [None] * (self.n_qubits - 1)] for target in range(self.n_qubits): start = qutip.tensor(random[:target] + [base] + random[target:]) - test = expand_operator(gate.get_qobj(), self.n_qubits, target) * start + test = ( + expand_operator( + gate.get_qobj(), targets=target, dims=[2] * self.n_qubits + ) + * start + ) expected = qutip.tensor( random[:target] + [applied] + random[target:] ) @@ -232,51 +239,43 @@ def test_single_qubit_rotation(self, gate: Gate, n_angles: int): @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.CX, 1, id="CX"), - pytest.param(std.CY, 1, id="CY"), - pytest.param(std.CZ, 1, id="CZ"), - pytest.param(std.CS, 1, id="CS"), - pytest.param(std.CT, 1, id="CT"), - pytest.param(std.SWAP, 0, id="SWAP"), - pytest.param(std.ISWAP, 0, id="ISWAP"), - pytest.param(std.SQRTSWAP, 0, id="SQRTSWAP"), - pytest.param(std.MS([0.5 * np.pi, 0.0]), 0, id="Molmer-Sorensen"), + pytest.param(gates.CX, 1, id="CX"), + pytest.param(gates.CY, 1, id="CY"), + pytest.param(gates.CZ, 1, id="CZ"), + pytest.param(gates.CS, 1, id="CS"), + pytest.param(gates.CT, 1, id="CT"), + pytest.param(gates.SWAP, 0, id="SWAP"), + pytest.param(gates.ISWAP, 0, id="ISWAP"), + pytest.param(gates.SQRTSWAP, 0, id="SQRTSWAP"), + pytest.param(gates.MS(0.5 * np.pi, 0.0), 0, id="Molmer-Sorensen"), ], ) def test_two_qubit(self, gate, n_controls): - targets = [ qutip.rand_ket(2) for _ in [None] * 2] + targets = [qutip.rand_ket(2) for _ in [None] * 2] others = [qutip.rand_ket(2) for _ in [None] * self.n_qubits] reference = gate.get_qobj() * qutip.tensor(*targets) for q1, q2 in itertools.permutations(range(self.n_qubits), 2): qubits = others.copy() qubits[q1], qubits[q2] = targets - test = ( - expand_operator(gate.get_qobj(), dims=[2]*self.n_qubits, targets=[q1,q2]) - * qutip.tensor(*qubits) - ) + test = expand_operator( + gate.get_qobj(), dims=[2] * self.n_qubits, targets=[q1, q2] + ) * qutip.tensor(*qubits) expected = _tensor_with_entanglement(qubits, reference, [q1, q2]) assert _infidelity(test, expected) < 1e-12 - # class RandomThreeQubitGate(ControlledGate): - # num_qubits = 3 - # num_ctrl_qubits = 2 - # target_gate = None - - # def get_qobj(self): - # if self._U is None: - # self._U = _make_random_three_qubit_gate() - # return self._U + random_gate = get_unitary_gate("random", qutip.rand_unitary([2] * 1)) + RandomThreeQubitGate = get_controlled_gate(random_gate, 2) @pytest.mark.parametrize( ["gate", "n_controls"], [ - pytest.param(std.FREDKIN, 1, id="Fredkin"), - pytest.param(std.TOFFOLI, 2, id="Toffoli"), - # pytest.param(RandomThreeQubitGate(), 2, id="random"), + pytest.param(gates.FREDKIN, 1, id="Fredkin"), + pytest.param(gates.TOFFOLI, 2, id="Toffoli"), + pytest.param(RandomThreeQubitGate, 2, id="random"), ], ) - def test_three_qubit(self, gate: Gate, n_controls): + def test_three_qubit(self, gate: Type[Gate], n_controls): targets = [qutip.rand_ket(2) for _ in [None] * 3] others = [qutip.rand_ket(2) for _ in [None] * self.n_qubits] reference = gate.get_qobj() * qutip.tensor(targets) @@ -284,7 +283,9 @@ def test_three_qubit(self, gate: Gate, n_controls): for q1, q2, q3 in itertools.permutations(range(self.n_qubits), 3): qubits = others.copy() qubits[q1], qubits[q2], qubits[q3] = targets - test = expand_operator(gate.get_qobj(), self.n_qubits, [q1,q2,q3]) * qutip.tensor(*qubits) + test = expand_operator( + gate.get_qobj(), targets=[q1, q2, q3], dims=[2] * self.n_qubits + ) * qutip.tensor(*qubits) expected = _tensor_with_entanglement( qubits, reference, [q1, q2, q3] ) @@ -308,7 +309,7 @@ class Test_expand_operator: ) def test_permutation_without_expansion(self, permutation): base = qutip.tensor([qutip.rand_unitary(2) for _ in permutation]) - test = gates.expand_operator( + test = expand_operator( base, dims=[2] * len(permutation), targets=permutation ) expected = base.permute(_apply_permutation(permutation)) @@ -324,7 +325,7 @@ def test_general_qubit_expansion(self, n_targets): expected = _tensor_with_entanglement( [qutip.qeye(2)] * n_qubits, operation, targets ) - test = gates.expand_operator( + test = expand_operator( operation, dims=[2] * n_qubits, targets=targets ) np.testing.assert_allclose( @@ -332,8 +333,8 @@ def test_general_qubit_expansion(self, n_targets): ) def test_cnot_explicit(self): - test = gates.expand_operator( - std.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] + test = expand_operator( + gates.CX.get_qobj(), dims=[2] * 3, targets=[2, 0] ).full() expected = np.array( [ @@ -350,7 +351,7 @@ def test_cnot_explicit(self): np.testing.assert_allclose(test, expected, atol=1e-15) def test_hadamard_explicit(self): - test = gates.hadamard_transform(3).full() + test = hadamard_transform(3).full() expected = np.array( [ [1, 1, 1, 1, 1, 1, 1, 1], @@ -387,82 +388,449 @@ def test_non_qubit_systems(self, dimensions): ] expected = qutip.tensor(*operators) base_test = qutip.tensor(*[operators[x] for x in targets]) - test = gates.expand_operator( - base_test, dims=dimensions, targets=targets - ) + test = expand_operator(base_test, dims=dimensions, targets=targets) assert test.dims == expected.dims np.testing.assert_allclose(test.full(), expected.full()) def test_dtype(self): - expanded_qobj = expand_operator(std.CX.get_qobj(), dims=[2, 2, 2]).data + expanded_qobj = expand_operator( + gates.CX.get_qobj(), dims=[2, 2, 2] + ).data assert isinstance(expanded_qobj, qutip.data.CSR) expanded_qobj = expand_operator( - std.CX.get_qobj(), dims=[2, 2, 2], dtype="dense" + gates.CX.get_qobj(), dims=[2, 2, 2], dtype="dense" ).data assert isinstance(expanded_qobj, qutip.data.Dense) -def test_gates_class(): - init_state = qutip.rand_ket([2, 2, 2]) - - circuit1 = QubitCircuit(3) - circuit1.add_gate(std.X, targets=1) - circuit1.add_gate(std.Y, targets=1) - circuit1.add_gate(std.Z, targets=2) - circuit1.add_gate(std.RX(np.pi / 4), targets=0) - circuit1.add_gate(std.RY(np.pi / 4), targets=0) - circuit1.add_gate(std.RZ(np.pi / 4), targets=1) - circuit1.add_gate(std.H, targets=0) - circuit1.add_gate(std.SQRTX, targets=0) - circuit1.add_gate(std.S, targets=2) - circuit1.add_gate(std.T, targets=1) - circuit1.add_gate(std.R(arg_value=(np.pi / 4, np.pi / 6)), targets=1) - circuit1.add_gate( - std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0 - ) - circuit1.add_gate(std.CX, controls=0, targets=1) - circuit1.add_gate(std.CPHASE(np.pi / 4), controls=0, targets=1) - circuit1.add_gate(std.SWAP, targets=[0, 1]) - circuit1.add_gate(std.ISWAP, targets=[2, 1]) - circuit1.add_gate(std.CZ, controls=[0], targets=[2]) - circuit1.add_gate(std.SQRTSWAP, [2, 0]) - circuit1.add_gate(std.SQRTISWAP, [0, 1]) - circuit1.add_gate(std.SWAPALPHA(np.pi / 4), [1, 2]) - circuit1.add_gate(std.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0]) - circuit1.add_gate(std.TOFFOLI, controls=[2, 0], targets=[1]) - circuit1.add_gate(std.FREDKIN, controls=[0], targets=[1, 2]) - circuit1.add_gate(std.BERKELEY, targets=[1, 0]) - circuit1.add_gate(std.RZX(1.0), targets=[1, 0]) - result1 = circuit1.run(init_state) - - circuit2 = QubitCircuit(3) - circuit2.add_gate(std.X, targets=1) - circuit2.add_gate(std.Y, targets=1) - circuit2.add_gate(std.Z, targets=2) - circuit2.add_gate(std.RX(np.pi / 4), targets=0) - circuit2.add_gate(std.RY(np.pi / 4), targets=0) - circuit2.add_gate(std.RZ(np.pi / 4), targets=1) - circuit2.add_gate(std.H, targets=0) - circuit2.add_gate(std.SQRTX, targets=0) - circuit2.add_gate(std.S, targets=2) - circuit2.add_gate(std.T, targets=1) - circuit2.add_gate(std.R([np.pi / 4, np.pi / 6]), targets=1) - circuit2.add_gate( - std.QASMU(arg_value=(np.pi / 4, np.pi / 4, np.pi / 4)), targets=0, +rand_U = qutip.rand_unitary(dimensions=[2], seed=rng) + + +class U1(Gate): + num_qubits = 1 + + @staticmethod + def get_qobj(dtype: str = "dense"): + return rand_U.to(dtype) + + +class U2(AngleParametricGate): + num_qubits = 1 + num_params = 1 + + @staticmethod + def compute_qobj(args, dtype: str = "dense"): + theta = args[0] + return qutip.Qobj( + [ + [np.exp(-1j * theta), 0], + [0, np.exp(1j * theta)], + ] + ) + + def inverse(self): + theta = self.arg_value[0] + return U2(-theta) + + +GATES = [ + gates.X, + gates.Y, + gates.Z, + gates.H, + gates.S, + gates.Sdag, + gates.T, + gates.Tdag, + gates.SWAP, + gates.ISWAP, + gates.ISWAPdag, + gates.SQRTSWAP, + gates.SQRTISWAPdag, + gates.SQRTISWAP, + gates.SQRTISWAPdag, + gates.BERKELEY, + gates.BERKELEYdag, + U1, +] + +PARAMETRIC_GATE = [ + gates.RX(0.5), + gates.RY(0.5), + gates.RZ(0.5), + gates.PHASE(0.5), + gates.R(0.5, 0.9), + gates.QASMU(0.1, 0.2, 0.3), + gates.SWAPALPHA(0.3), + gates.MS(0.47, 0.8), + gates.RZX(0.6), + U2(0.447), +] + +CONTROLLED_GATE = [ + gates.CX, + gates.CY, + gates.CZ, + gates.CH, + gates.CS, + gates.CT, + gates.CRX(0.7), + gates.CRY(0.88), + gates.CRZ(0.78), + gates.CPHASE(0.9), + gates.CQASMU(0.9, 0.22, 0.15), + gates.TOFFOLI, + gates.FREDKIN, + get_controlled_gate(U1, 1, 1), + get_controlled_gate(U2, 1, 1)(0.88), +] + + +@pytest.mark.parametrize("gate", GATES + PARAMETRIC_GATE + CONTROLLED_GATE) +def test_gate_inverse(gate: Gate | Type[Gate]): + n = 2**gate.num_qubits + inverse = gate.inverse() + print(rand_U) + np.testing.assert_allclose( + (gate.get_qobj() * inverse.get_qobj()).full(), + np.eye(n), + atol=1e-12, ) - circuit2.add_gate(std.CX, controls=0, targets=1) - circuit2.add_gate(std.CPHASE(np.pi / 4), controls=0, targets=1) - circuit2.add_gate(std.SWAP, targets=[0, 1]) - circuit2.add_gate(std.ISWAP, targets=[2, 1]) - circuit2.add_gate(std.CZ, controls=[0], targets=[2]) - circuit2.add_gate(std.SQRTSWAP, targets=[2, 0]) - circuit2.add_gate(std.SQRTISWAP, targets=[0, 1]) - circuit2.add_gate(std.SWAPALPHA(np.pi / 4), targets=[1, 2]) - circuit2.add_gate(std.MS(arg_value=(np.pi / 4, np.pi / 7)), targets=[1, 0]) - circuit2.add_gate(std.TOFFOLI, controls=[2, 0], targets=[1]) - circuit2.add_gate(std.FREDKIN, controls=[0, 1], targets=[2]) - circuit2.add_gate(std.BERKELEY, targets=[1, 0]) - circuit2.add_gate(std.RZX(arg_value=1.0), targets=[1, 0]) - result2 = circuit2.run(init_state) - - assert pytest.approx(qutip.fidelity(result1, result2), 1.0e-6) == 1 + + +def test_gate_equality(): + assert gates.CX == gates.CX + assert gates.CX != gates.CY + assert gates.CX != gates.CRX(0.5) + + assert gates.RX(0.5) == gates.RX(0.5) + assert gates.RX(0.5) != gates.RY(0.5) + assert gates.RX(0.5) != gates.RX(0.6) + + assert gates.CRX(0.5) == gates.CRX(0.5) + assert gates.CRX(0.5) != gates.CRY(0.5) + assert gates.CRX(0.5) != gates.CRX(0.6) + + +class TestGateErrors: + def test_namespace_errors(self): + with pytest.raises(ValueError): + NameSpace("bad.namespace") # namespace cannot contain dots + + ns1 = NameSpace("test_ns") + with pytest.raises(ValueError): + _ = NameSpace("test_ns") # Existing namespace + + class TmpGate(Gate): + num_qubits = 1 + + ns1.register("tmp_gate", TmpGate) + with pytest.raises(NameError, match="already exists in namespace"): + ns1.register("tmp_gate", TmpGate) + + assert ns1.get("tmp") is None + assert ns1.get("tmp_gate") is TmpGate + + ns1._remove("tmp_gate") + with pytest.raises(KeyError): + ns1._remove("tmp_gate") + + def test_gateclass_errors(self): + with pytest.raises(TypeError): + + class BadGate(Gate): + num_qubits = -1 + + def get_qobj(cls): + pass + + with pytest.raises(TypeError): + + class BadGate2(Gate): + num_qubits = 1 + is_clifford = 1 # attribute 'is_clifford' must be a bool + + def get_qobj(cls): + pass + + with pytest.raises(TypeError): + + class BadGate3(Gate): + num_qubits = 1 + self_inverse = 1 # attribute 'self_inverse' must be a bool + + def get_qobj(cls): + pass + + with pytest.raises(TypeError): + + class BadGate4(Gate): + num_qubits = 1 + self_inverse = True + + def get_qobj(cls): + pass + + def inverse(cls): + pass # Can't define inverse method is self_inverse=True + + class GoodGate(Gate): + num_qubits = 1 + + @staticmethod + def get_qobj(): + pass + + with pytest.raises(AttributeError): + # For a given gateclass, class attribute like num_qubit can't be modified + GoodGate.num_qubits = 2 + + U_rect = qutip.Qobj(np.eye(2, 3)) + with pytest.raises(ValueError): + get_unitary_gate("U_rect", U_rect) # U must be a square matrix + + U_dim = qutip.Qobj(np.eye(3)) + with pytest.raises(ValueError): + get_unitary_gate("U_dim", U_dim) # 3 != 2^n + + U_not_unitary = qutip.Qobj(np.zeros((2, 2))) + with pytest.raises(ValueError): + get_unitary_gate( + "U_not_unitary", U_not_unitary + ) # U must be unitaru + + with pytest.raises(TypeError): + + class NotGoodGate(GoodGate): + def is_controlled(args): + return args # is_controlled can't take arguments + + with pytest.raises(TypeError): + + class NotGoodGate2(GoodGate): + def is_controlled(): + return 1 # must return a bool + + with pytest.raises(TypeError): + + class NotGoodGate3(GoodGate): + def is_parametric(args): + return args # is_parametric can't take arguments + + with pytest.raises(TypeError): + + class NotGoodGate4(GoodGate): + def is_parametric(): + return 1 # must return a bool + + def test_parametric_gate_errors(self): + with pytest.raises(TypeError): + + class BadParamGate(ParametricGate): + num_params = -1 + + @staticmethod + def compute_qobj(args): + pass + + @staticmethod + def validate_params(args): + pass + + with pytest.raises(TypeError): + + class BadParamGate2(ParametricGate): + num_params = 1.5 + + @staticmethod + def compute_qobj(args): + pass + + @staticmethod + def validate_params(args): + pass + + class GoodParamGate(AngleParametricGate): + num_qubits = 1 + num_params = 2 + + @staticmethod + def compute_qobj(args, dtype): + return qutip.qeye(2) + + with pytest.raises(ValueError, match="Requires 2 parameters, got 1"): + GoodParamGate(1.0) + + with pytest.raises(TypeError): + GoodParamGate( + 1.0, "wrong" + ) # second argument is a string instead of float + + gate = GoodParamGate(1.0, 2.0) + with pytest.raises(NotImplementedError): + gate.inverse() + + with pytest.raises(SyntaxError): + + class NotGoodParamGate(GoodParamGate): + def validate_params(arg1, arg2): + pass + + with pytest.raises(SyntaxError): + + class NotGoodParamGate2(GoodParamGate): + def compute_qobj(arg1, arg2, dtype): + pass + + def test_controlled_gate_errors(self): + with pytest.raises(TypeError): + + class BadCtrlGate(ControlledGate): + target_gate = gates.RX( + 0 + ) # target_gate must be a gate subclass not an instantiated onject + num_ctrl_qubits = 1 + ctrl_value = 1 + num_qubits = 2 + + with pytest.raises(TypeError): + + class BadCtrlGate2(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = -1 # Can't be negative + ctrl_value = 1 + num_qubits = 2 + + with pytest.raises(ValueError): + + class BadCtrlGate3(ControlledGate): + target_gate = gates.X + num_qubits = 2 + num_ctrl_qubits = 2 # Must be less than num_qubit + ctrl_value = 1 + + with pytest.raises(AttributeError): + + class BadCtrlGate4(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = ( + 1 # No of control qubits must be 2, since target gate is X + ) + ctrl_value = 1 + num_qubits = 3 + + with pytest.raises(TypeError): + + class BadCtrlGate5(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = 1 + ctrl_value = 1.5 # Control value must be an int + num_qubits = 2 + + with pytest.raises(ValueError): + + class BadCtrlGate6(ControlledGate): + target_gate = gates.X + num_ctrl_qubits = 1 + ctrl_value = ( + 2 # Control value can't be greater than 1 in this case + ) + num_qubits = 2 + + def test_utils_errors(self): + from qutip_qip.operations.utils import ( + _check_oper_dims, + _targets_to_list, + expand_operator, + gate_sequence_product, + ) + + with pytest.raises(ValueError): + _check_oper_dims( + qutip.basis(2, 0) + ) # The operator is not an Qobj with the same input and output dimensions. + + op = qutip.qeye(2) + with pytest.raises(ValueError): + _check_oper_dims( + oper=op, dims=[3], targets=[0] + ) # The dims don't match + + with pytest.raises(TypeError): + _targets_to_list( + 1.5, op, 1 + ) # targets should be an integer or a list of integer + + with pytest.raises(ValueError): + expand_operator(op, 2, 0) # dims needs to be an interable + + with pytest.raises(ValueError): + gate_sequence_product([], left_to_right=True) # empty U_list + + def test_standard_gates_failures(self): + # Instantiation on non-parametric + with pytest.raises(TypeError): + gates.CX() + + # Wrong no. of parameters + with pytest.raises(TypeError): + gates.CRX(0, 1, 2) + + # Wrong no. of arguments on parametric + with pytest.raises(TypeError): + gates.CRX.get_qobj() + + # Control_value > 2^n -1 + with pytest.raises(ValueError): + get_controlled_gate(gates.X, n_ctrl_qubits=1, control_value=2) + + with pytest.raises(TypeError): + get_controlled_gate( + gates.X, n_ctrl_qubits=0 + ) # num_ctrl_qubits > 0 + + def test_class_attribute_modification(self): + with pytest.raises(AttributeError): + gates.CX.namespace = NameSpace("temp") + + with pytest.raises(AttributeError): + gates.X.num_qubits = 0 + + with pytest.raises(AttributeError): + gates.X.is_clifford = False + + with pytest.raises(AttributeError): + gates.X.self_inverse = False + + with pytest.raises(AttributeError): + gates.CX.num_ctrl_qubits = 0 + + with pytest.raises(AttributeError): + gates.CX.ctrl_value = 0 + + with pytest.raises(AttributeError): + gates.CX.target_gate = gates.Y + + with pytest.raises(AttributeError): + gates.RX.num_params = 2 + + with pytest.raises(AttributeError): + gates.RY.latex_str = "R_y" + + class H(Gate): + num_qubits = 1 + + def get_qobj(): + pass + + assert H.name == "H" + assert H.latex_str == "H" + + class H(Gate): + name = "Hadamard" + num_qubits = 1 + + def get_qobj(): + pass + + H.name = "Hadamard" diff --git a/tests/test_model.py b/tests/test_model.py index fc1efdb09..b9443cc4e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -30,11 +30,13 @@ def test_cavityqed_model(): def test_spinchain_model(model_class): model = LinearSpinChain(3, sx=[1.1, 1, 0, 0.8], sz=7.0, t1=10.0) assert model.get_all_drift() == [] + model.get_control_labels() if isinstance(model, LinearSpinChain): assert len(model.get_control_labels()) == 3 * 3 - 1 elif isinstance(model, CircularSpinChain): assert len(model.get_control_labels()) == 3 * 3 + model.get_control("g1") model.get_control("sx0") assert_array_equal(model.params["sz"], 7.0) diff --git a/tests/test_noise.py b/tests/test_noise.py index cc20c4d87..a4bee8175 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -16,7 +16,7 @@ sigmam, ) from qutip_qip.device import Processor, SCQubits, LinearSpinChain -from qutip_qip.operations import X +from qutip_qip.operations.gates import X from qutip_qip.noise import ( RelaxationNoise, DecoherenceNoise, diff --git a/tests/test_optpulseprocessor.py b/tests/test_optpulseprocessor.py index a64bf5530..1a0a408de 100644 --- a/tests/test_optpulseprocessor.py +++ b/tests/test_optpulseprocessor.py @@ -15,7 +15,7 @@ sigmay, identity, ) -from qutip_qip.operations import X, CX, H, SWAP +from qutip_qip.operations.gates import X, CX, H, SWAP class TestOptPulseProcessor: diff --git a/tests/test_phase_flip.py b/tests/test_phase_flip.py index 9b1075834..55ca02a42 100644 --- a/tests/test_phase_flip.py +++ b/tests/test_phase_flip.py @@ -2,7 +2,7 @@ import qutip from qutip_qip.algorithms import PhaseFlipCode from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import Z +from qutip_qip.operations.gates import Z @pytest.fixture @@ -60,7 +60,7 @@ def test_phaseflip_correction_simulation(code, data_qubits, syndrome_qubits): state = qc_encode.run(state) # Apply Z (phase-flip) error to qubit 1 - qc_error = QubitCircuit(num_qubits = 5) + qc_error = QubitCircuit(num_qubits=5) qc_error.add_gate(Z, targets=[1]) state = qc_error.run(state) diff --git a/tests/test_processor.py b/tests/test_processor.py index 3b2a93870..e570f7fb2 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -5,7 +5,6 @@ import pytest import qutip -from qutip_qip.device import Processor, LinearSpinChain from qutip import ( basis, sigmaz, @@ -18,10 +17,12 @@ fidelity, ) from qutip_qip.circuit import QubitCircuit -from qutip_qip.operations import hadamard_transform, ISWAP, X +from qutip_qip.device import Processor, LinearSpinChain +from qutip_qip.operations import hadamard_transform +from qutip_qip.operations.gates import ISWAP, X from qutip_qip.noise import DecoherenceNoise, RandomNoise, ControlAmpNoise -from qutip_qip.qubits import qubit_states from qutip_qip.pulse import Pulse +from qutip_qip.qubits import qubit_states class TestCircuitProcessor: @@ -152,7 +153,7 @@ def test_id_with_T1_T2(self): np.exp(-1.0 / t2 * end_time) * 0.5 + 0.5, rtol=1e-5, err_msg="Error in t1 & t2 simulation, " - "with t1={} and t2={}".format(t1, t2), + f"with t1={t1} and t2={t2}", ) def test_plot(self): diff --git a/tests/test_qasm.py b/tests/test_qasm.py index 8870cf8ce..603235850 100644 --- a/tests/test_qasm.py +++ b/tests/test_qasm.py @@ -8,7 +8,7 @@ from qutip_qip.circuit import QubitCircuit from qutip import tensor, rand_ket, basis, identity from qutip_qip.operations import Measurement -import qutip_qip.operations.std as std +import qutip_qip.operations.gates as gates @pytest.mark.parametrize( @@ -75,9 +75,7 @@ def test_qasm_addcircuit(): check_gate_instruction_defn(qc.instructions[3], "CX", (1,), (0,)) check_gate_instruction_defn(qc.instructions[4], "H", (0,)) check_gate_instruction_defn(qc.instructions[5], "H", (1,)) - check_gate_instruction_defn( - qc.instructions[6], "H", (0,), (), (0, 1), 0 - ) + check_gate_instruction_defn(qc.instructions[6], "H", (0,), (), (0, 1), 0) check_measurement_defn(qc.instructions[7], "M", (0,), (0,)) check_measurement_defn(qc.instructions[8], "M", (1,), (1,)) @@ -88,7 +86,9 @@ def test_custom_gates(): qc = read_qasm(filepath) unitaries = qc.propagators() assert (unitaries[0] - unitaries[1]).norm() < 1e-12 - ry_cx = std.CX.get_qobj() * tensor(identity(2), std.RY(np.pi / 2).get_qobj()) + ry_cx = gates.CX.get_qobj() * tensor( + identity(2), gates.RY(np.pi / 2).get_qobj() + ) assert (unitaries[2] - ry_cx).norm() < 1e-12 @@ -130,33 +130,33 @@ def test_qasm_str(): "x q[0];\nmeasure q[1] -> c[0]\n" ) simple_qc = QubitCircuit(2, num_cbits=1) - simple_qc.add_gate(std.X, targets=[0]) + simple_qc.add_gate(gates.X, targets=[0]) simple_qc.add_measurement("M", targets=[1], classical_store=0) assert circuit_to_qasm_str(simple_qc) == expected_qasm_str def test_export_import(): qc = QubitCircuit(3) - qc.add_gate(std.CRY(np.pi), targets=1, controls=0) - qc.add_gate(std.CRX(np.pi), targets=1, controls=0) - qc.add_gate(std.CRZ(np.pi), targets=1, controls=0) - qc.add_gate(std.CX, targets=1, controls=0) - qc.add_gate(std.TOFFOLI, targets=2, controls=[0, 1]) + qc.add_gate(gates.CRY(np.pi), targets=1, controls=0) + qc.add_gate(gates.CRX(np.pi), targets=1, controls=0) + qc.add_gate(gates.CRZ(np.pi), targets=1, controls=0) + qc.add_gate(gates.CX, targets=1, controls=0) + qc.add_gate(gates.TOFFOLI, targets=2, controls=[0, 1]) # qc.add_gate(SQRTX, targets=0) - qc.add_gate(std.CS, targets=1, controls=0) - qc.add_gate(std.CT, targets=1, controls=0) - qc.add_gate(std.SWAP, targets=[0, 1]) - qc.add_gate(std.QASMU(arg_value=[np.pi, np.pi, np.pi]), targets=[0]) - qc.add_gate(std.RX(np.pi), targets=[0]) - qc.add_gate(std.RY(np.pi), targets=[0]) - qc.add_gate(std.RZ(np.pi), targets=[0]) - qc.add_gate(std.H, targets=[0]) - qc.add_gate(std.X, targets=[0]) - qc.add_gate(std.Y, targets=[0]) - qc.add_gate(std.Z, targets=[0]) - qc.add_gate(std.S, targets=[0]) - qc.add_gate(std.T, targets=[0]) - # qc.add_gate(CZ, targets=[0], controls=[1]) + qc.add_gate(gates.CS, targets=1, controls=0) + qc.add_gate(gates.CT, targets=1, controls=0) + qc.add_gate(gates.SWAP, targets=[0, 1]) + qc.add_gate(gates.QASMU(np.pi, np.pi, np.pi), targets=[0]) + qc.add_gate(gates.RX(np.pi), targets=[0]) + qc.add_gate(gates.RY(np.pi), targets=[0]) + qc.add_gate(gates.RZ(np.pi), targets=[0]) + qc.add_gate(gates.H, targets=[0]) + qc.add_gate(gates.X, targets=[0]) + qc.add_gate(gates.Y, targets=[0]) + qc.add_gate(gates.Z, targets=[0]) + qc.add_gate(gates.S, targets=[0]) + qc.add_gate(gates.T, targets=[0]) + # qc.add_gate(gates.CZ, targets=[0], controls=[1]) # The generated code by default has a inclusion statement of # qelib1.inc, which will trigger a warning when read. @@ -171,15 +171,16 @@ def test_export_import(): assert (u0 - u1).norm() < 1e-12 -def test_read_qasm(): +def test_read_qasm_1(): filename = "w-state.qasm" filepath = Path(__file__).parent / "qasm_files" / filename + read_qasm(filepath) + + +def test_read_qasm_2(): filename2 = "w-state_with_comments.qasm" filepath2 = Path(__file__).parent / "qasm_files" / filename2 - - read_qasm(filepath) read_qasm(filepath2) - assert True def test_parsing_mode(tmp_path): @@ -195,22 +196,23 @@ def test_parsing_mode(tmp_path): ) assert "Unknown parsing mode" in record_warning[0].message.args[0] - mode = "predefined_only" - qasm_input_string = ( - 'OPENQASM 2.0;\ninclude "qelib1.inc"\n\ncreg c[2];' - "\nqreg q[2];swap q[0],q[1];\n" - ) - with pytest.raises(SyntaxError): - with pytest.warns(UserWarning) as record_warning: - circuit = read_qasm( - qasm_input_string, - mode=mode, - strmode=True, - ) - assert ( - "Ignoring external gate definition in the predefined_only mode." - in record_warning[0].message.args[0] - ) + # TODO fix this test, since SWAP is now a predefined gate + # mode = "predefined_only" + # qasm_input_string = ( + # 'OPENQASM 2.0;\ninclude "qelib1.inc"\n\ncreg c[2];' + # "\nqreg q[2];swap q[0],q[1];\n" + # ) + # with pytest.raises(SyntaxError): + # with pytest.warns(UserWarning) as record_warning: + # circuit = read_qasm( + # qasm_input_string, + # mode=mode, + # strmode=True, + # ) + # assert ( + # "Ignoring external gate definition in the predefined_only mode." + # in record_warning[0].message.args[0] + # ) mode = "external_only" file_path = tmp_path / "custom_swap.inc" @@ -229,7 +231,7 @@ def test_parsing_mode(tmp_path): ) propagator = circuit.compute_unitary() - fidelity = qutip.average_gate_fidelity(propagator, std.SWAP.get_qobj()) + fidelity = qutip.average_gate_fidelity(propagator, gates.SWAP.get_qobj()) pytest.approx(fidelity, 1.0) circuit = read_qasm( @@ -238,5 +240,5 @@ def test_parsing_mode(tmp_path): ) propagator = circuit.compute_unitary() - fidelity = qutip.average_gate_fidelity(propagator, std.SWAP.get_qobj()) + fidelity = qutip.average_gate_fidelity(propagator, gates.SWAP.get_qobj()) pytest.approx(fidelity, 1.0) diff --git a/tests/test_qft.py b/tests/test_qft.py index abcbe9bc3..d87750069 100644 --- a/tests/test_qft.py +++ b/tests/test_qft.py @@ -1,6 +1,8 @@ -from numpy.testing import assert_, assert_equal, assert_string_equal +from numpy.testing import assert_, assert_equal + from qutip_qip.algorithms.qft import qft, qft_steps, qft_gate_sequence from qutip_qip.operations import gate_sequence_product +from qutip_qip.operations.gates import CPHASE, H, SWAP class TestQFT: @@ -29,13 +31,11 @@ def testQFTGateSequenceNoSwapping(self): totsize = N * (N + 1) / 2 assert_equal(len(circuit.instructions), totsize) - snots = sum( - g.operation.name == "H" for g in circuit.instructions - ) + snots = sum(g.operation == H for g in circuit.instructions) assert_equal(snots, N) phases = sum( - g.operation.name == "CPHASE" for g in circuit.instructions + isinstance(g.operation, CPHASE) for g in circuit.instructions ) assert_equal(phases, N * (N - 1) / 2) @@ -52,9 +52,7 @@ def testQFTGateSequenceWithSwapping(self): assert_equal(len(circuit.instructions), phases + swaps) for i in range(phases, phases + swaps): - assert_string_equal( - circuit.instructions[i].operation.name, "SWAP" - ) + assert circuit.instructions[i].operation == SWAP def testQFTGateSequenceWithCNOT(self): """ @@ -65,5 +63,5 @@ def testQFTGateSequenceWithCNOT(self): circuit = qft_gate_sequence(N, swapping=False, to_cnot=True) assert not any( - [ins.operation.name == "CPHASE" for ins in circuit.instructions] + [isinstance(ins.operation, CPHASE) for ins in circuit.instructions] ) diff --git a/tests/test_qiskit.py b/tests/test_qiskit.py index d7dd00eb8..2287d2786 100644 --- a/tests/test_qiskit.py +++ b/tests/test_qiskit.py @@ -15,7 +15,7 @@ CircularSpinChain, DispersiveCavityQED, ) -from qutip_qip.operations import X, CX, RX +from qutip_qip.operations.gates import X, CX, RX, H from qiskit import QuantumCircuit from qiskit_aer import AerSimulator @@ -38,11 +38,11 @@ class TestConverter: def _compare_args(self, req_gate, res_gate): """Compare parameters of two gates""" res_arg = [] - if res_gate.operation.is_parametric_gate(): + if res_gate.operation.is_parametric(): res_arg = res_gate.operation.arg_value req_arg = [] - if req_gate.operation.is_parametric_gate(): + if req_gate.operation.is_parametric(): req_arg = req_gate.operation.arg_value if len(req_arg) != len(res_arg): @@ -58,23 +58,29 @@ def _compare_gate_instructions( req_gate.operation.name == res_gate.operation.name ) and ( list(req_gate.qubits) - == get_qutip_index(list(res_gate.qubits), result_circuit.num_qubits) + == get_qutip_index( + list(res_gate.qubits), result_circuit.num_qubits + ) ) if not check_condition: return False if req_gate.is_measurement_instruction(): - check_condition = list(req_gate.operation.classical_store) == get_qutip_index( + check_condition = list( + req_gate.operation.classical_store + ) == get_qutip_index( res_gate.operation.classical_store, result_circuit.num_cbits ) else: # TODO correct for float error in arg_value res_controls = None - if res_gate.operation.is_controlled_gate(): - res_controls = get_qutip_index(list(res_gate.controls), result_circuit.num_qubits) + if res_gate.operation.is_controlled(): + res_controls = get_qutip_index( + list(res_gate.controls), result_circuit.num_qubits + ) req_controls = None - if req_gate.operation.is_controlled_gate(): + if req_gate.operation.is_controlled(): req_controls = list(req_gate.controls) check_condition = ( @@ -96,8 +102,6 @@ def _compare_circuit( for i, res_ins in enumerate(result_circuit.instructions): req_ins = required_circuit.instructions[i] - print(req_ins) - print(res_ins) if not self._compare_gate_instructions( req_ins, res_ins, result_circuit @@ -134,6 +138,24 @@ def test_controlled_qubit_conversion(self): assert self._compare_circuit(result_circuit, required_circuit) def test_rotation_conversion(self): + """ + Test to check conversion of a circuit + containing a single qubit rotation gate. + """ + qiskit_circuit = QuantumCircuit(3) + qiskit_circuit.rx(np.pi / 3, 0) + qiskit_circuit.cx(0, 1) + qiskit_circuit.h(2) + result_circuit = convert_qiskit_circuit_to_qutip(qiskit_circuit) + + required_circuit = QubitCircuit(3) + required_circuit.add_gate(RX(np.pi / 3), targets=[0]) + required_circuit.add_gate(CX, targets=[1], controls=[0]) + required_circuit.add_gate(H, targets=[2]) + + assert self._compare_circuit(result_circuit, required_circuit) + + def test_multiqubit_circuit_conversion(self): """ Test to check conversion of a circuit containing a single qubit rotation gate. @@ -189,7 +211,7 @@ def test_measurements(self): obtain predetermined results. """ random.seed(1) - predefined_counts = {"0": 233, "11": 267, "10": 270, "1": 254} + predefined_counts = {"0": 233, "11": 267, "1": 270, "10": 254} circ = QuantumCircuit(2, 2) circ.h(0) diff --git a/tests/test_qpe.py b/tests/test_qpe.py index 41dd21bee..6ae1a5cbf 100644 --- a/tests/test_qpe.py +++ b/tests/test_qpe.py @@ -1,14 +1,11 @@ +import unittest import numpy as np from numpy.testing import assert_, assert_equal -import unittest from qutip import Qobj, sigmaz, tensor -from qutip_qip.operations import ( - ControlledGate, - controlled_gate_factory, - custom_gate_factory, -) from qutip_qip.algorithms.qpe import qpe +from qutip_qip.operations import get_controlled_gate, get_unitary_gate +from qutip_qip.operations import gates as std class TestQPE(unittest.TestCase): @@ -18,11 +15,11 @@ class TestQPE(unittest.TestCase): def test_custom_gate(self): """ - Test if custom_gate_factory correctly stores and returns the quantum object + Test if get_unitary_gate correctly stores and returns the quantum object """ U = Qobj([[0, 1], [1, 0]]) - custom = custom_gate_factory(gate_name="custom", U=U) + custom = get_unitary_gate(gate_name="custom", U=U) qobj = custom.get_qobj() assert_((qobj - U).norm() < 1e-12) @@ -32,15 +29,13 @@ def test_controlled_unitary(self): """ U = Qobj([[0, 1], [1, 0]]) - controlled_u = controlled_gate_factory( - gate=custom_gate_factory(gate_name="CU", U=U), - )(control_value=1) - - assert_equal(controlled_u.control_value, 1) - assert_( - (controlled_u.target_gate.get_qobj() - U).norm() < 1e-12 + controlled_u = get_controlled_gate( + gate=get_unitary_gate(gate_name="CU", U=U), ) + assert_equal(controlled_u.ctrl_value, 1) + assert_((controlled_u.target_gate.get_qobj() - U).norm() < 1e-12) + def test_qpe_validation(self): """ Test input validation in qpe function @@ -72,7 +67,7 @@ def test_qpe_circuit_structure(self): for i in range(num_counting): circ_instruction = circuit.instructions[num_counting + i] - assert_(circ_instruction.operation.is_controlled_gate) + assert_(circ_instruction.operation.is_controlled) assert_equal(circ_instruction.controls, [i]) assert_equal(circ_instruction.targets, [num_counting]) @@ -133,11 +128,9 @@ def test_qpe_to_cnot_flag(self): num_counting = 2 circuit1 = qpe(U, num_counting_qubits=num_counting, to_cnot=False) - circuit2 = qpe(U, num_counting_qubits=num_counting, to_cnot=True) - has_cnot = any( - gate.operation.name == "CX" for gate in circuit2.instructions + gate.operation == std.CX for gate in circuit2.instructions ) assert_(has_cnot) assert_(len(circuit2.instructions) > len(circuit1.instructions)) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index bb3729e4e..4c958d044 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -3,9 +3,9 @@ from unittest.mock import patch from qutip_qip.circuit import QubitCircuit from qutip_qip.circuit.draw import TextRenderer -from qutip_qip.operations import ( - ControlledGate, - IDLE, +from qutip_qip.operations import get_controlled_gate +from qutip_qip.operations.gates import ( + IDENTITY, X, H, CX, @@ -164,23 +164,11 @@ def qc3(): @pytest.fixture def qc4(): - class i(ControlledGate): - num_qubits = 2 - num_ctrl_qubits = 1 - target_gate = IDLE - - def __init__(self, control_value=1): - super().__init__(control_value=control_value) - - def get_qobj(self): - pass - - class ii(i): - num_qubits = 3 - num_ctrl_qubits = 2 - - class iii(i): - pass + i = get_controlled_gate(IDENTITY, n_ctrl_qubits=1, gate_name="i") + ii = get_controlled_gate(IDENTITY, n_ctrl_qubits=2, gate_name="ii") + iii = get_controlled_gate( + IDENTITY, n_ctrl_qubits=1, control_value=0, gate_name="iii" + ) qc = QubitCircuit(5, num_cbits=2) qc.add_gate( diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index deb7659d2..483790e3c 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -3,9 +3,8 @@ from qutip_qip.circuit import QubitCircuit from qutip_qip.compiler import PulseInstruction, Scheduler -from qutip_qip.operations import ( +from qutip_qip.operations.gates import ( GATE_CLASS_MAP, - ControlledGate, CX, X, Z, @@ -121,7 +120,7 @@ def _instructions1(): instruction_list = [] for circuit_ins in circuit3.instructions: - if circuit_ins.operation.name == "H": + if circuit_ins.operation == H: instruction_list.append(PulseInstruction(circuit_ins, duration=1)) else: instruction_list.append(PulseInstruction(circuit_ins, duration=2)) @@ -277,7 +276,7 @@ def test_scheduling_pulse( for instruction in instructions: gate_cls = GATE_CLASS_MAP[instruction.name] - if issubclass(gate_cls, ControlledGate): + if gate_cls.is_controlled(): circuit.add_gate( gate_cls, targets=instruction.targets, diff --git a/tests/decomposition_functions/test_utility.py b/tests/test_utils.py similarity index 82% rename from tests/decomposition_functions/test_utility.py rename to tests/test_utils.py index 3c392782a..5aba04d64 100644 --- a/tests/decomposition_functions/test_utility.py +++ b/tests/test_utils.py @@ -1,13 +1,10 @@ import numpy as np import pytest - from qutip import Qobj, qeye -from qutip_qip.decompose._utility import ( - check_gate, -) +from qutip_qip.utils import valid_unitary -# Tests for check_gate +# Tests for valid_unitary @pytest.mark.parametrize( "invalid_input", [ @@ -25,14 +22,14 @@ def test_check_gate_non_qobj(invalid_input): """Checks if correct value is returned or not when the input is not a Qobj .""" with pytest.raises(TypeError, match="The input matrix is not a Qobj."): - check_gate(invalid_input, num_qubits=1) + valid_unitary(invalid_input, num_qubits=1) @pytest.mark.parametrize("non_unitary", [Qobj([[1, 1], [0, 1]])]) def test_check_gate_non_unitary(non_unitary): """Checks if non-unitary input is correctly identified.""" with pytest.raises(ValueError, match="Input is not unitary."): - check_gate(non_unitary, num_qubits=1) + valid_unitary(non_unitary, num_qubits=1) @pytest.mark.parametrize("non_1qubit_unitary", [qeye(4)]) @@ -42,11 +39,11 @@ def test_check_gate_non_1qubit(non_1qubit_unitary): with pytest.raises( ValueError, match=f"Input is not a unitary on {num_qubits} qubits." ): - check_gate(non_1qubit_unitary, num_qubits) + valid_unitary(non_1qubit_unitary, num_qubits) @pytest.mark.parametrize("unitary", [Qobj([[1, 0], [0, -1]])]) def test_check_gate_unitary_input(unitary): """Checks if shape of input is correctly identified.""" # No error raised if it passes. - check_gate(unitary, num_qubits=1) + valid_unitary(unitary, num_qubits=1) diff --git a/tests/test_vqa.py b/tests/test_vqa.py index 394855fac..822b71956 100644 --- a/tests/test_vqa.py +++ b/tests/test_vqa.py @@ -1,7 +1,8 @@ import pytest import numpy as np import qutip -from qutip_qip.operations import expand_operator, H +from qutip_qip.operations import expand_operator +from qutip_qip.operations.gates import H from qutip_qip.vqa import ( VQA, VQABlock,