diff --git a/quantum_image_processing/models/tensor_network_circuits/mera.py b/quantum_image_processing/models/tensor_network_circuits/mera.py index a6af2a2..26d23af 100644 --- a/quantum_image_processing/models/tensor_network_circuits/mera.py +++ b/quantum_image_processing/models/tensor_network_circuits/mera.py @@ -1,19 +1,18 @@ """Multiscale Entanglement Renormalization Ansatz (MERA) Tensor Network""" from __future__ import annotations import uuid -from typing import Callable +from typing import Callable, Optional import math import numpy as np from qiskit.circuit import ( QuantumCircuit, - QuantumRegister, - ClassicalRegister, ParameterVector, ) from quantum_image_processing.gates.two_qubit_unitary import TwoQubitUnitary +from quantum_image_processing.models.tensor_network_circuits.ttn import TTN -class MERA(TwoQubitUnitary): +class MERA(TTN): """ Implements a Multiscale Entanglement Renormalization Ansatz (MERA) tensor network structure as given by [2]. @@ -29,14 +28,39 @@ class MERA(TwoQubitUnitary): doi: https://doi.org/10.1103/physrevlett.101.110501. """ - def __init__(self, img_dims: tuple[int, int], layer_depth: type(None) = None): - self.img_dims = img_dims - self.num_qubits = int(math.prod(img_dims)) + def __init__( + self, img_dims: tuple[int, int], layer_depth: Optional[int] | None = None + ): + """ + Initializes the MERA class with given input variables. + + Args: + img_dims (int): dimensions of the input image data. + layer_depth (int): number of MERA layers to be built in a circuit. + """ + TTN.__init__(self, img_dims) + self.num_qubits = int(math.prod(self.img_dims)) + + if not isinstance(layer_depth, int) and layer_depth is not None: + raise TypeError("The input layer_depth must be of the type int or None.") + + # Should there be a max cap on the value of layer_depth? + if isinstance(layer_depth, int) and layer_depth < 1: + raise ValueError("The input layer_depth must be at least 1.") + if layer_depth is None: self.layer_depth = int(np.ceil(np.sqrt(self.num_qubits))) else: self.layer_depth = layer_depth + self._circuit = QuantumCircuit(self.num_qubits) + self.mera_qr = self._circuit.qubits + + @property + def circuit(self): + """Returns the MERA circuit.""" + return self._circuit + def mera_simple(self, complex_structure: bool = True) -> QuantumCircuit: """ Builds a MERA circuit with a simple unitary gate @@ -50,13 +74,14 @@ def mera_simple(self, complex_structure: bool = True) -> QuantumCircuit: circuit (QuantumCircuit): Returns the MERA circuit generated with the help of the input arguments. """ + # Check params here. param_vector = ParameterVector( f"theta_{str(uuid.uuid4())[:5]}", - int(self.num_qubits / 2 * (self.num_qubits / 2 + 1)) + 3, + 10 * self.num_qubits - 1, ) param_vector_copy = param_vector return self.mera_backbone( - self.simple_parameterization, + TwoQubitUnitary().simple_parameterization, param_vector_copy, complex_structure, ) @@ -86,18 +111,18 @@ def mera_general(self, complex_structure: bool = True) -> QuantumCircuit: ) param_vector_copy = param_vector return self.mera_backbone( - self.general_parameterization, + TwoQubitUnitary().general_parameterization, param_vector_copy, complex_structure, ) - # pylint: disable=too-many-branches def mera_backbone( self, gate_structure: Callable, param_vector_copy: ParameterVector, complex_structure: bool = True, ) -> QuantumCircuit: + # pylint: disable=duplicate-code """ Lays out the backbone structure of a MERA circuit onto which the unitary gates are applied. @@ -116,72 +141,73 @@ def mera_backbone( circuit (QuantumCircuit): Returns the MERA circuit generated with the help of the input arguments. """ - mera_qr = QuantumRegister(size=self.num_qubits) - mera_cr = ClassicalRegister(size=self.num_qubits) - mera_circ = QuantumCircuit(mera_qr, mera_cr) - - # Make recursive layer structure using a staticmethod i.e. convert code to staticmethod. - # This should solve R0912: Too many branches (13/12) (too-many-branches) qubit_list = [] - for layer in range(self.layer_depth): - if layer == 0: - # D unitary blocks - for index in range(1, self.num_qubits, 2): - if index == self.num_qubits - 1: - break - - _, param_vector_copy = gate_structure( - circuit=mera_circ, - qubits=[mera_qr[index], mera_qr[index + 1]], - parameter_vector=param_vector_copy, - complex_structure=complex_structure, - ) - - # U unitary blocks - mera_circ.barrier() - for index in range(0, self.num_qubits, 2): - if index == self.num_qubits - 1: - qubit_list.append(mera_qr[index]) - else: - qubit_list.append(mera_qr[index + 1]) - _, param_vector_copy = gate_structure( - circuit=mera_circ, - qubits=[mera_qr[index], mera_qr[index + 1]], - parameter_vector=param_vector_copy, - complex_structure=complex_structure, - ) + # Layer = 0 + for index in range(1, self.num_qubits, 2): + if index == self.num_qubits - 1: + break + + # D unitary blocks + unitary_block, param_vector_copy = gate_structure( + parameter_vector=param_vector_copy, + complex_structure=complex_structure, + ) + self.circuit.compose( + unitary_block, + qubits=[self.mera_qr[index], self.mera_qr[index + 1]], + inplace=True, + ) + # U unitary blocks + for index in range(0, self.num_qubits, 2): + if index == self.num_qubits - 1: + qubit_list.append(self.mera_qr[index]) else: - temp_list = [] + qubit_list.append(self.mera_qr[index + 1]) + unitary_block, param_vector_copy = gate_structure( + parameter_vector=param_vector_copy, + complex_structure=complex_structure, + ) + self.circuit.compose( + unitary_block, + qubits=[self.mera_qr[index], self.mera_qr[index + 1]], + inplace=True, + ) + + # Rest of the layers. + temp_list = qubit_list.copy() + if self.layer_depth > 1: + while len(temp_list) > 1: # D unitary blocks - mera_circ.barrier() for index in range(1, len(qubit_list), 2): - if len(qubit_list) == 2 or index == len(qubit_list) - 1: + if len(temp_list) == 2 or index == len(temp_list) - 1: break - _, param_vector_copy = gate_structure( - circuit=mera_circ, - qubits=[qubit_list[index], qubit_list[index + 1]], + unitary_block, param_vector_copy = gate_structure( parameter_vector=param_vector_copy, complex_structure=complex_structure, ) + self.circuit.compose( + unitary_block, + qubits=[qubit_list[index], qubit_list[index + 1]], + inplace=True, + ) # U unitary blocks - mera_circ.barrier() for index in range(0, len(qubit_list) - 1, 2): - _, param_vector_copy = gate_structure( - circuit=mera_circ, - qubits=[qubit_list[index], qubit_list[index + 1]], + unitary_block, param_vector_copy = gate_structure( parameter_vector=param_vector_copy, complex_structure=complex_structure, ) - temp_list.append(qubit_list[index + 1]) - - if len(qubit_list) % 2 != 0: - temp_list.append(qubit_list[-1]) + self.circuit.compose( + unitary_block, + qubits=[qubit_list[index], qubit_list[index + 1]], + inplace=True, + ) + temp_list.pop(0) qubit_list = temp_list - if len(qubit_list) == 1: - mera_circ.ry(param_vector_copy[0], mera_qr[-1]) + if len(qubit_list) == 1: + self.circuit.ry(param_vector_copy[0], self.mera_qr[-1]) - return mera_circ + return self.circuit diff --git a/quantum_image_processing/models/tensor_network_circuits/ttn.py b/quantum_image_processing/models/tensor_network_circuits/ttn.py index 293cce8..332934a 100644 --- a/quantum_image_processing/models/tensor_network_circuits/ttn.py +++ b/quantum_image_processing/models/tensor_network_circuits/ttn.py @@ -34,7 +34,7 @@ def __init__(self, img_dims: tuple[int, int]): raise ValueError("Image dimensions cannot be zero or negative.") self.img_dims = img_dims - self.num_qubits = int(math.prod(img_dims)) + self.num_qubits = int(math.prod(self.img_dims)) self._circuit = QuantumCircuit(self.num_qubits) self.q_reg = self._circuit.qubits @@ -94,7 +94,6 @@ def ttn_general(self, complex_structure: bool = True) -> QuantumCircuit: def ttn_with_aux(self, complex_structure: bool = True): """ - TODO: Find the implementation procedure for this. Implements a TTN network with alternative parameterization that requires an auxiliary qubit, as given in [1]. @@ -132,6 +131,7 @@ def ttn_backbone( QuantumCircuit: quantum circuit with unitary gates represented by general parameterization. """ + # Layer = 0 qubit_list = [] for index in range(0, self.num_qubits, 2): if index == self.num_qubits - 1: @@ -148,6 +148,7 @@ def ttn_backbone( inplace=True, ) + # Rest of the layers. for _ in range(int(np.sqrt(self.num_qubits))): temp_list = [] for index in range(0, len(qubit_list) - 1, 2): diff --git a/tests/models/tensor_networks/test_mera.py b/tests/models/tensor_networks/test_mera.py index e69de29..03176c2 100644 --- a/tests/models/tensor_networks/test_mera.py +++ b/tests/models/tensor_networks/test_mera.py @@ -0,0 +1,239 @@ +"""Unit test for MERA class""" +from __future__ import annotations +import math +from unittest import mock +import pytest +from pytest import raises +from qiskit.circuit import QuantumCircuit, ParameterVector +from quantum_image_processing.models.tensor_network_circuits.mera import MERA +from quantum_image_processing.gates.two_qubit_unitary import TwoQubitUnitary + + +@pytest.fixture(name="mera_circuit") +def mera_circuit_fixture(): + """Fixture to replicate a real simple two-qubit unitary block.""" + + # pylint: disable=duplicate-code + def _mera_circuit(img_dims, parameter_vector, parameterization): + test_circuit = QuantumCircuit(int(math.prod(img_dims))) + parameterization_callable = { + "real_simple": [TwoQubitUnitary().real_simple_block, 2], + "real_general": [TwoQubitUnitary().real_general_block, 6], + "complex_general": [TwoQubitUnitary().complex_general_block, 15], + } + + mapper = parameterization_callable[parameterization] + if math.prod(img_dims) == 2: + # D-block doesn't exist + # U-block implementation + test_circuit.compose( + mapper[0](parameter_vector[: mapper[1]])[0], + qubits=[0, 1], + inplace=True, + ) + test_circuit.ry(parameter_vector[mapper[1]], len(test_circuit.qubits) - 1) + elif math.prod(img_dims) == 3: + # D-block + test_circuit.compose( + mapper[0](parameter_vector[: mapper[1]])[0], + qubits=[1, 2], + inplace=True, + ) + # U-block + test_circuit.compose( + mapper[0](parameter_vector[mapper[1] : 2 * mapper[1]])[0], + qubits=[0, 1], + inplace=True, + ) + # No D-block now. Skip to U-block + test_circuit.compose( + mapper[0](parameter_vector[2 * mapper[1] : 3 * mapper[1]])[0], + qubits=[1, 2], + inplace=True, + ) + test_circuit.ry( + parameter_vector[3 * mapper[1]], len(test_circuit.qubits) - 1 + ) + elif math.prod(img_dims) == 4: + # D-block + test_circuit.compose( + mapper[0](parameter_vector[: mapper[1]])[0], + qubits=[1, 2], + inplace=True, + ) + # U-block + test_circuit.compose( + mapper[0](parameter_vector[mapper[1] : 2 * mapper[1]])[0], + qubits=[0, 1], + inplace=True, + ) + test_circuit.compose( + mapper[0](parameter_vector[2 * mapper[1] : 3 * mapper[1]])[0], + qubits=[2, 3], + inplace=True, + ) + # U-block again. + test_circuit.compose( + mapper[0](parameter_vector[3 * mapper[1] : 4 * mapper[1]])[0], + qubits=[1, 3], + inplace=True, + ) + test_circuit.ry( + parameter_vector[4 * mapper[1]], len(test_circuit.qubits) - 1 + ) + elif math.prod(img_dims) == 5: + # D-block + test_circuit.compose( + mapper[0](parameter_vector[: mapper[1]])[0], + qubits=[1, 2], + inplace=True, + ) + test_circuit.compose( + mapper[0](parameter_vector[mapper[1] : 2 * mapper[1]])[0], + qubits=[3, 4], + inplace=True, + ) + # U-block + test_circuit.compose( + mapper[0](parameter_vector[2 * mapper[1] : 3 * mapper[1]])[0], + qubits=[0, 1], + inplace=True, + ) + test_circuit.compose( + mapper[0](parameter_vector[3 * mapper[1] : 4 * mapper[1]])[0], + qubits=[2, 3], + inplace=True, + ) + # D-block + test_circuit.compose( + mapper[0](parameter_vector[4 * mapper[1] : 5 * mapper[1]])[0], + qubits=[3, 4], + inplace=True, + ) + # U-block + test_circuit.compose( + mapper[0](parameter_vector[5 * mapper[1] : 6 * mapper[1]])[0], + qubits=[1, 3], + inplace=True, + ) + # U-block again. + test_circuit.compose( + mapper[0](parameter_vector[6 * mapper[1] : 7 * mapper[1]])[0], + qubits=[3, 4], + inplace=True, + ) + test_circuit.ry( + parameter_vector[7 * mapper[1]], len(test_circuit.qubits) - 1 + ) + return test_circuit + + return _mera_circuit + + +class TestMERA: + """Tests for MERA class""" + + @pytest.mark.parametrize("img_dims, layer_depth", [((3, 1), -0.9), ((2, 1), "abc")]) + def test_layer_depth(self, img_dims, layer_depth): + """Tests the type of layer_depth input.""" + with raises( + TypeError, match="The input layer_depth must be of the type int or None." + ): + _ = MERA(img_dims, layer_depth) + + @pytest.mark.parametrize("img_dims, layer_depth", [((3, 1), 0)]) + def test_layer_depth_value(self, img_dims, layer_depth): + """Tests the value of layer_depth input.""" + with raises(ValueError, match="The input layer_depth must be at least 1."): + _ = MERA(img_dims, layer_depth) + + @pytest.mark.parametrize("img_dims", [(2, 4)]) + def test_circuit_property(self, img_dims): + """Tests the MERA circuit initialization.""" + test_circuit = QuantumCircuit(math.prod(img_dims)) + assert test_circuit.data == MERA(img_dims).circuit.data + + @pytest.mark.parametrize( + "img_dims, layer_depth, complex_structure", + [((2, 2), 1, False), ((4, 5), None, True)], + ) + def test_mera_simple(self, img_dims, layer_depth, complex_structure): + # pylint: disable=line-too-long + """Tests the mera_backbone method call via the mera_simple function.""" + with mock.patch( + "quantum_image_processing.models.tensor_network_circuits.mera.MERA.mera_backbone" + ) as mock_mera_simple: + with mock.patch( + "quantum_image_processing.gates.two_qubit_unitary.TwoQubitUnitary.simple_parameterization" + ) as simple_parameterization: + _ = MERA(img_dims, layer_depth).mera_simple(complex_structure) + mock_mera_simple.assert_called_once_with( + simple_parameterization, mock.ANY, complex_structure + ) + + @pytest.mark.parametrize( + "img_dims, layer_depth, complex_structure", + [((2, 2), 1, False), ((4, 5), None, True)], + ) + def test_mera_general(self, img_dims, layer_depth, complex_structure): + # pylint: disable=line-too-long + """Tests the mera_backbone method call via the mera_general function.""" + with mock.patch( + "quantum_image_processing.models.tensor_network_circuits.mera.MERA.mera_backbone" + ) as mock_mera_general: + with mock.patch( + "quantum_image_processing.gates.two_qubit_unitary.TwoQubitUnitary.general_parameterization" + ) as general_parameterization: + _ = MERA(img_dims, layer_depth).mera_general(complex_structure) + mock_mera_general.assert_called_once_with( + general_parameterization, mock.ANY, complex_structure + ) + + @pytest.mark.parametrize( + "img_dims, layer_depth, complex_structure, parameterization", + [ + ((1, 2), None, False, "real_general"), + ((1, 3), None, False, "real_general"), + ((1, 4), None, False, "real_general"), + ((2, 1), None, False, "real_simple"), + ((1, 3), None, False, "real_simple"), + ((2, 2), None, False, "real_simple"), + ((1, 2), None, True, "complex_general"), + ((1, 3), None, True, "complex_general"), + ((1, 4), None, True, "complex_general"), + ((1, 5), None, False, "real_general"), + ((5, 1), None, False, "real_simple"), + ((1, 5), None, True, "complex_general"), + ], + ) + def test_mera_backbone( + self, img_dims, layer_depth, complex_structure, parameterization, mera_circuit + ): + # pylint: disable=too-many-arguments + """Tests the mera_backbone circuit with real and complex parameterization.""" + # Add test cases when layer_depth is not None. + num_qubits = int(math.prod(img_dims)) + parameterization_mapper = { + "real_simple": [ + ParameterVector("test", 10 * num_qubits - 1), + TwoQubitUnitary().simple_parameterization, + ], + "real_general": [ + ParameterVector("test", 20 * num_qubits - 1), + TwoQubitUnitary().general_parameterization, + ], + "complex_general": [ + ParameterVector("test", 15 * 10 * num_qubits - 1), + TwoQubitUnitary().general_parameterization, + ], + } + + test_circuit = mera_circuit( + img_dims, parameterization_mapper[parameterization][0], parameterization + ) + circuit = MERA(img_dims, layer_depth).mera_backbone( + parameterization_mapper[parameterization][1], + parameterization_mapper[parameterization][0], + complex_structure, + ) + assert circuit.data == test_circuit.data diff --git a/tests/models/tensor_networks/test_ttn.py b/tests/models/tensor_networks/test_ttn.py index d6e7f55..e2bd8f0 100644 --- a/tests/models/tensor_networks/test_ttn.py +++ b/tests/models/tensor_networks/test_ttn.py @@ -11,11 +11,11 @@ from quantum_image_processing.gates.two_qubit_unitary import TwoQubitUnitary -@pytest.fixture(name="ttn_simple_circuit") -def ttn_simple_circuit_fixture(): +@pytest.fixture(name="ttn_circuit") +def ttn_circuit_fixture(): """Fixture to replicate a real simple two-qubit unitary block.""" - def _ttn_simple_circuit(img_dims, parameter_vector, parameterization): + def _ttn_circuit(img_dims, parameter_vector, parameterization): test_circuit = QuantumCircuit(int(math.prod(img_dims))) parameterization_callable = { @@ -67,7 +67,7 @@ def _ttn_simple_circuit(img_dims, parameter_vector, parameterization): ) return test_circuit - return _ttn_simple_circuit + return _ttn_circuit class TestTTN: @@ -119,25 +119,31 @@ def test_ttn_general(self, img_dims, complex_structure): """Tests the ttn_backbone method call via the ttn_general function.""" with mock.patch( "quantum_image_processing.models.tensor_network_circuits.ttn.TTN.ttn_backbone" - ) as mock_ttn_simple: + ) as mock_ttn_general: with mock.patch( "quantum_image_processing.gates.two_qubit_unitary.TwoQubitUnitary.general_parameterization" ) as general_parameterization: _ = TTN(img_dims).ttn_general(complex_structure) - mock_ttn_simple.assert_called_once_with( + mock_ttn_general.assert_called_once_with( general_parameterization, mock.ANY, complex_structure ) @pytest.mark.parametrize( "img_dims, complex_structure, parameterization", [ + ((1, 2), False, "real_general"), ((1, 3), False, "real_general"), + ((1, 4), False, "real_general"), + ((2, 1), False, "real_simple"), + ((1, 3), False, "real_simple"), ((2, 2), False, "real_simple"), ((1, 2), True, "complex_general"), + ((1, 3), True, "complex_general"), + ((1, 4), True, "complex_general"), ], ) def test_ttn_backbone( - self, img_dims, complex_structure, parameterization, ttn_simple_circuit + self, img_dims, complex_structure, parameterization, ttn_circuit ): """Tests the ttn_backbone circuit with real and complex parameterization.""" parameterization_mapper = { @@ -155,7 +161,7 @@ def test_ttn_backbone( ], } - test_circuit = ttn_simple_circuit( + test_circuit = ttn_circuit( img_dims, parameterization_mapper[parameterization][0], parameterization ) circuit = TTN(img_dims).ttn_backbone(