From ebc98699c6f6c17a222028395b7ca819b7bf2c7d Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 22 Jan 2025 13:20:27 +0400 Subject: [PATCH 01/13] hamming weight encoder --- src/qibo/models/encodings.py | 338 ++++++++++++++++++++++++++++++++++- 1 file changed, 337 insertions(+), 1 deletion(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 3a8d3e9d72..76a28024e7 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -2,9 +2,10 @@ import math from inspect import signature -from typing import Optional, Union +from typing import List, Optional, Union import numpy as np +from scipy.special import binom from qibo import gates from qibo.config import raise_error @@ -323,6 +324,75 @@ def unary_encoder_random_gaussian( return circuit +def hamming_weight_encoder( + data, + nqubits: int, + weight: int, + full_hwp: bool = False, + phase_correction: bool = True, + optimize_controls: bool = True, + decompose: bool = False, + **kwargs, +): + complex_data = bool(data.dtype in [complex, np.dtype("complex128")]) + + initial_string = np.array([1] * weight + [0] * (nqubits - weight)) + bitstrings, targets_and_controls = _ehrlich_algorithm(initial_string) + + """Calculate all gate phases necessary to encode the amplitudes.""" + _data = np.abs(data) if complex_data else data + thetas = _generate_rbs_angles(data, nqubits, architecture="diagonal") + thetas = np.asarray(thetas, dtype=type(thetas[0])) + phis = np.zeros(len(thetas) + 1) + if complex_data: + phis[0] = _angle_mod_two_pi(-np.angle(data[0])) + for k in range(1, len(phis)): + phis[k] = _angle_mod_two_pi(-np.angle(data[k]) + np.sum(phis[:k])) + + last_qubit = nqubits - 1 + + circuit = Circuit(nqubits, **kwargs) + if not full_hwp: + circuit.add( + gates.X(qubit) for qubit in range(last_qubit, last_qubit - weight, -1) + ) + + if optimize_controls: + controls_to_remove = list(range(last_qubit, last_qubit - weight, -1)) + indices = [ + int(binom(nqubits - j, weight - j)) - 1 for j in range(weight - 1, 0, -1) + ] + + for k, ((targets, controls), theta, phi) in enumerate( + zip(targets_and_controls, thetas, phis) + ): + targets = list(last_qubit - np.asarray(targets)) + controls = list(last_qubit - np.asarray(controls)) + controls.sort() + + if optimize_controls: + comparison = k < np.asarray(indices) + controls_to_remove = list(np.argwhere(comparison == True).flatten()) + for control_index in controls_to_remove[::-1]: + controls.pop(control_index) + + gate = _get_gate( + [targets[0]], + [targets[1]], + controls, + theta, + phi, + decompose, + complex_data, + ) + circuit.add(gate) + + if complex_data and phase_correction: + circuit.add(_get_phase_gate_correction(bitstrings[-1], phis[-1])) + + return circuit + + def entangling_layer( nqubits: int, architecture: str = "diagonal", @@ -558,3 +628,269 @@ def _parametrized_two_qubit_gate(gate, q0, q1, params=None): return gate(q0, q1, *params) return gate(q0, q1) + + +def _angle_mod_two_pi(angle): + return angle % (2 * np.pi) + + +def _get_markers(bitstring, last_run: bool = False): + if not isinstance(bitstring, np.ndarray): + bitstring = np.asarray(bitstring) + + nqubits = len(bitstring) + + markers = [len(bitstring) - 1] + for ind, value in zip(range(nqubits - 2, -1, -1), bitstring[::-1][1:]): + if value == bitstring[-1]: + markers.append(ind) + else: + break + + markers = set(markers) + + if not last_run: + markers = set(range(nqubits)) - markers + + return markers + + +def _get_next_bistring(bitstring, markers, hamming_weight): + if len(markers) == 0: + return bitstring + + new_bitstring = np.copy(bitstring) + + nqubits = len(new_bitstring) + + indexes = np.argsort(bitstring) + zeros, ones = np.sort(indexes[:-hamming_weight]), np.sort(indexes[-hamming_weight:]) + + max_index = max(markers) + nearest_one = ones[ones > max_index] + nearest_one = None if len(nearest_one) == 0 else nearest_one[0] + if new_bitstring[max_index] == 0 and nearest_one is not None: + new_bitstring[max_index] = 1 + new_bitstring[nearest_one] = 0 + else: + farthest_zero = zeros[zeros > max_index] + if nearest_one is not None: + farthest_zero = farthest_zero[farthest_zero < nearest_one] + farthest_zero = farthest_zero[-1] + new_bitstring[max_index] = 0 + new_bitstring[farthest_zero] = 1 + + markers.remove(max_index) + last_run = _get_markers(new_bitstring, last_run=True) + markers = markers | (set(range(max_index + 1, nqubits)) - last_run) + + new_ones = np.argsort(new_bitstring)[-hamming_weight:] + controls = list(set(ones) & set(new_ones)) + difference = new_bitstring - bitstring + qubits = [np.where(difference == -1)[0][0], np.where(difference == 1)[0][0]] + + return new_bitstring, markers, [qubits, controls] + + +def _ehrlich_algorithm(initial_string, return_indices: bool = True): + k = np.unique(initial_string, return_counts=True) + if len(k[1]) == 1: + return ["".join([str(item) for item in initial_string])] + + k = k[1][1] + n = len(initial_string) + n_choose_k = int(binom(n, k)) + + markers = _get_markers(initial_string, last_run=False) + string = initial_string + strings = ["".join(string[::-1].astype(str))] + controls_and_targets = [] + for _ in range(n_choose_k - 1): + string, markers, c_and_t = _get_next_bistring(string, markers, k) + strings.append("".join(string[::-1].astype(str))) + controls_and_targets.append(c_and_t) + + if return_indices: + return strings, controls_and_targets + + return strings + + +def _get_gate( + qubits_in: List[int], + qubits_out: List[int], + controls: List[int], + theta: float, + phi: float, + decompose: bool = False, + complex_data: bool = False, +): + """Return gate(s) necessary to encode a complex amplitude in a given computational basis state, + given the computational basis state used to encode the previous amplitude. + + Information about computational baiss states in question are contained + in the indexing of ``qubits_in``, ``qubits_out``, and ``controls``. + + Args: + qubits_in (list): list of qubits with ``in`` label. + qubits_out (list): list of qubits with ``out`` label. + controls (list): list of qubits that control the resulting gate. + theta (float): first phase, used to encode the ``abs`` of amplitude. + phi (float): second phase, used to encode the complex phase of amplitude. + decompose (bool): if ``True``, all two-qubit gates are decomposed in terms of CNOTs. + If ``False``, original gates are left uncompiled. Defaults to ``False``. + complex_data (bool): if ``True``, uses :class:`qibo.gates.U3` to as basis gate. + If ``False``, uses :class:`qibo.gates.RY` as basis gate. Defaults to ``False``. + + Returns: + List[:class:`qibo.gates.Gate`]: gate(s) to be added to circuit. + + References: + 1. R. M. S. Farias, T. O. Maciel, G. Camilo, R. Lin, S. Ramos-Calderer, and L. Aolita, + *Quantum encoder for fixed Hamming-weight subspaces* + `arXiv:2405.20408 [quant-ph] `_. + """ + if len(qubits_in) == 0 and len(qubits_out) == 1: + gate_list = ( + gates.U3(*qubits_out, 2 * theta, 2 * phi, 0.0).controlled_by(*controls) + if complex_data + else gates.RY(*qubits_out, 2 * theta).controlled_by(*controls) + ) + gate_list = [gate_list] + elif len(qubits_in) == 1 and len(qubits_out) == 1: + ## chooses best combilation of complex RBS gate + ## given number of controls and if data is real or complex + gate_list = _compiled_RBS( + qubits_in, + qubits_out, + controls, + theta, + phi, + decompose, + complex_data, + ) + else: + # gate_list = gRBS(list(qubits_out), list(qubits_in), theta, phi, controls) + gate_list = [ + gates.GeneralizedRBS( + list(qubits_in), list(qubits_out, theta, phi) + ).controlled_by(*controls) + ] + + return gate_list + + +def _compiled_RBS( + qubits_in: List[int], + qubits_out: List[int], + controls: List[int], + theta: float, + phi: float, + decompose: bool = False, + complex_data: bool = False, +) -> List: + """Returns (un)compiled version of complex RBS gate depending on number of controls and data type. + + Qubit labelling has to be flipped from paper below due to difference between + big-endinan and little-endian notations (see e.g. + `Wikipedia page on Endianness `_). + + Args: + qubits_in (List[int]): qubits with ``in`` label. + qubits_out (List[int]): qubits with ``out`` label. + controls (List[int]): qubits that control the RBS gate. + theta (float): first phase, used to encode the ``abs`` of amplitude. + phi (float): second phase, used to encode the complex phase of amplitude. + decompose (bool): if ``True``, all two-qubit gates are decomposed in terms of CNOTs. + If ``False``, original gates are left uncompiled. Defaults to ``False``. + complex_data (bool): if ``True``, uses :class:`qibo.gates.U3` to as basis gate. + If ``False``, uses :class:`qibo.gates.RY` as basis gate. Defaults to ``False``. + + Returns: + List[:class:`qibo.gates.Gate`]: compilation (or not) of (controlled) RBS gate. + + References: + 1. R. M. S. Farias, T. O. Maciel, G. Camilo, R. Lin, S. Ramos-Calderer, and L. Aolita, + *Quantum encoder for fixed Hamming-weight subspaces* + `arXiv:2405.20408 [quant-ph] `_. + """ + gate_list = [] + + if len(controls) == 0: + if decompose: + # Without controls, the best decomposition uses two CNOTs + gate_list.extend( + [ + gates.H(*qubits_in), + gates.CNOT(*qubits_in, *qubits_out), + gates.RY(*qubits_in, theta), + gates.RY(*qubits_out, theta), + gates.CNOT(*qubits_in, *qubits_out), + gates.H(*qubits_in), + ] + ) + else: + gate_list.append(gates.RBS(*qubits_in, *qubits_out, theta)) + + if complex_data: + gate_list.append(gates.RZ(*qubits_in, -phi)) + gate_list.append(gates.RZ(*qubits_out, phi)) + + elif len(controls) == 1 and not complex_data: + if decompose: + # Best decomposition for 1 control and real data + gate_list.extend( + [ + gates.H(*qubits_out), + gates.CNOT(*qubits_out, *qubits_in), + gates.RY(*qubits_out, 2 * theta).controlled_by(*controls), + gates.RY(*qubits_in, 2 * theta).controlled_by(*controls), + gates.CNOT(*qubits_out, *qubits_in), + gates.H(*qubits_out), + ] + ) + else: + gate_list.append( + gates.RBS(*qubits_in, *qubits_out, theta).controlled_by(*controls) + ) + + else: + if decompose: + # Best decompositon in any other case + controls.extend(qubits_in) + gate_list.append(gates.CNOT(*qubits_out, *qubits_in)) + + if complex_data: + gate_list.append( + gates.U3(*qubits_out, 2 * theta, 2 * phi, 0.0).controlled_by( + *controls + ) + ) + else: + gate_list.append( + gates.RY(*qubits_out, 2 * theta).controlled_by(*controls) + ) + + gate_list.append(gates.CNOT(*qubits_out, *qubits_in)) + else: + gate_list.append( + gates.RBS(*qubits_in, *qubits_out, theta).controlled_by(*controls) + ) + + return gate_list + + +def _get_phase_gate_correction(last_string, phase: float): + # to avoid circular import error + from qibo.quantum_info.utils import hamming_weight + + if isinstance(last_string, str): + last_string = np.asarray(list(last_string), dtype=int) + + last_weight = hamming_weight(last_string) + last_ones = np.argsort(last_string) + last_zero = last_ones[0] + last_controls = last_ones[-last_weight:] + + # adding an RZ gate to correct the phase of the last amplitude encoded + return gates.RZ(last_zero, 2 * phase).controlled_by(*last_controls) From 48f5975c8c26c5711dd827480fc3d1ec98c50c9b Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 22 Jan 2025 13:59:33 +0400 Subject: [PATCH 02/13] docstring and api ref --- doc/source/api-reference/qibo.rst | 6 +++++ src/qibo/models/encodings.py | 43 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 1d8ce5ca66..75241ccb91 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -362,6 +362,12 @@ of the :math:`d`-dimensional array is sampled from a Gaussian distribution .. autofunction:: qibo.models.encodings.unary_encoder_random_gaussian +Fixed Hamming-weight Encoder +"""""""""""""""""""""""""""" + +.. autofunction:: qibo.models.encodings.hamming_weight_encoder + + Entangling layer """""""""""""""" diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 76a28024e7..dcf273ebad 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -334,6 +334,49 @@ def hamming_weight_encoder( decompose: bool = False, **kwargs, ): + """Create circuit that encodes ``data`` in the Hamming-weight-:math:`k` basis of ``nqubits``. + + Let :math:`\\mathbf{x}` be a :math:`1`-dimensional array of size :math:`d = \\binom{n}{k}` and + :math:`B_{k} \\equiv \\{ \\ket{b_{j}} : b_{j} \\in \\{0, 1\\}^{\\otimes n} \\,\\, \\text{and} + \\,\\, |b_{j}| = k \\}` be a set of :math:`d` computational basis states of :math:`n` qubits + that are represented by bitstrings of Hamming weight :math:`k`. Then, an amplitude encoder + in the basis :math:`B_{k}` is an :math:`n`-qubit parameterized quantum circuit + :math:`\\operatorname{Load}_{B_{k}}` such that + + .. math:: + \\operatorname{Load}(\\mathbf{x}) \\, \\ket{0}^{\\otimes n} = \\frac{1}{\\|\\mathbf{x}\\|} + \\, \\sum_{j = 1}^{d} \\, x_{j} \\, \\ket{b_{j}} + + Args: + data (ndarray): :math:`1`-dimensional array of data to be loaded. + nqubits (int): number of qubits. + weight (int): Hamming weight that defines the subspace in which ``data`` will be encoded. + full_hwp (bool, optional): if ``False``, includes Pauli-:math:`X` gates that prepare the + first bitstring of Hamming weight ``k = weight``. If ``True``, circuit is full + Hamming weight preserving. Defaults to ```False``. + phase_correction (bool, optional): to be used when ``data`` is complex. If ``True``, + a :math:`(k - 1)`-controlled :class:`qibo.gates.RZ` rotation is added to the end + of the circuit and the complex ``data`` array is encoded exactly. If ``False``, + the aforementioned gate is not added to the circuit, and ``data`` is encoded + up to a global phase. Defaults to ``True``. + optimize_controls (bool, optional): if ``True``, removes unnecessary controlled operations. + Defaults to ``True``. + decompose (bool, optional): if ``True``, decomposes the (possibly multi-controlled) + :class:`qibo.gates.RBS` gates into :class:`qibo.gates.CNOT` gates and single-qubit + rotations. If ``False``, returns circuit as composition of (multi-)controlled + :class:`qibo.gates.RBS` gates. Defaults to ``False``. + kwargs (dict, optional): Additional arguments used to initialize a Circuit object. + For details, see the documentation of :class:`qibo.models.circuit.Circuit`. + + Returns: + :class:`qibo.models.circuit.Circuit`: Circuit that loads ``data``in + Hamming-weight-:math:`k` representation. + + References: + 1. R. M. S. Farias, T. O. Maciel, G. Camilo, R. Lin, S. Ramos-Calderer, and L. Aolita, + *Quantum encoder for fixed Hamming-weight subspaces* + `arXiv:2405.20408 [quant-ph] `_. + """ complex_data = bool(data.dtype in [complex, np.dtype("complex128")]) initial_string = np.array([1] * weight + [0] * (nqubits - weight)) From 0da76510a67ecd1a0ba72d7a1e1b5c202a3f1acd Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 22 Jan 2025 15:08:14 +0400 Subject: [PATCH 03/13] fix bug --- src/qibo/models/encodings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index dcf273ebad..3793393ad0 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -816,7 +816,7 @@ def _get_gate( # gate_list = gRBS(list(qubits_out), list(qubits_in), theta, phi, controls) gate_list = [ gates.GeneralizedRBS( - list(qubits_in), list(qubits_out, theta, phi) + list(qubits_in), list(qubits_out), theta, phi ).controlled_by(*controls) ] From 6e6928536019e6e7b9d2afcf422362dc40a8f1b8 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 22 Jan 2025 15:22:56 +0400 Subject: [PATCH 04/13] increase tol --- tests/test_models_encodings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index 33c201871a..d671c5d261 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -121,7 +121,7 @@ def test_binary_encoder(backend, nqubits): circuit = binary_encoder(target) state = backend.execute_circuit(circuit).state() - backend.assert_allclose(state, target) + backend.assert_allclose(state, target, atol=1e-10, rtol=1e-4) @pytest.mark.parametrize("kind", [None, list]) From bb13fccf71fa8166db14e04ad7e9212103d1c0d7 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 23 Jan 2025 10:47:57 +0400 Subject: [PATCH 05/13] update `models.__init__` --- src/qibo/models/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/qibo/models/__init__.py b/src/qibo/models/__init__.py index e9c2abb92c..62a843c4cb 100644 --- a/src/qibo/models/__init__.py +++ b/src/qibo/models/__init__.py @@ -1,8 +1,11 @@ from qibo.models import hep, tsp from qibo.models.circuit import Circuit from qibo.models.encodings import ( + binary_encoder, comp_basis_encoder, entangling_layer, + ghz_state, + hamming_weight_encoder, phase_encoder, unary_encoder, unary_encoder_random_gaussian, From 84ec853e78cf87c01d72305a27ff836b201096d6 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 23 Jan 2025 10:48:08 +0400 Subject: [PATCH 06/13] fix bug --- src/qibo/models/encodings.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 3793393ad0..6f8c169763 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -329,7 +329,6 @@ def hamming_weight_encoder( nqubits: int, weight: int, full_hwp: bool = False, - phase_correction: bool = True, optimize_controls: bool = True, decompose: bool = False, **kwargs, @@ -354,11 +353,6 @@ def hamming_weight_encoder( full_hwp (bool, optional): if ``False``, includes Pauli-:math:`X` gates that prepare the first bitstring of Hamming weight ``k = weight``. If ``True``, circuit is full Hamming weight preserving. Defaults to ```False``. - phase_correction (bool, optional): to be used when ``data`` is complex. If ``True``, - a :math:`(k - 1)`-controlled :class:`qibo.gates.RZ` rotation is added to the end - of the circuit and the complex ``data`` array is encoded exactly. If ``False``, - the aforementioned gate is not added to the circuit, and ``data`` is encoded - up to a global phase. Defaults to ``True``. optimize_controls (bool, optional): if ``True``, removes unnecessary controlled operations. Defaults to ``True``. decompose (bool, optional): if ``True``, decomposes the (possibly multi-controlled) @@ -384,7 +378,7 @@ def hamming_weight_encoder( """Calculate all gate phases necessary to encode the amplitudes.""" _data = np.abs(data) if complex_data else data - thetas = _generate_rbs_angles(data, nqubits, architecture="diagonal") + thetas = _generate_rbs_angles(_data, nqubits, architecture="diagonal") thetas = np.asarray(thetas, dtype=type(thetas[0])) phis = np.zeros(len(thetas) + 1) if complex_data: @@ -430,7 +424,7 @@ def hamming_weight_encoder( ) circuit.add(gate) - if complex_data and phase_correction: + if complex_data: circuit.add(_get_phase_gate_correction(bitstrings[-1], phis[-1])) return circuit @@ -813,7 +807,6 @@ def _get_gate( complex_data, ) else: - # gate_list = gRBS(list(qubits_out), list(qubits_in), theta, phi, controls) gate_list = [ gates.GeneralizedRBS( list(qubits_in), list(qubits_out), theta, phi @@ -919,6 +912,9 @@ def _compiled_RBS( gate_list.append( gates.RBS(*qubits_in, *qubits_out, theta).controlled_by(*controls) ) + if complex_data: + gate_list.append(gates.RZ(*qubits_in, -phi).controlled_by(*controls)) + gate_list.append(gates.RZ(*qubits_out, phi).controlled_by(*controls)) return gate_list From 88fabbfa57d485cd2c80200ad647ef2bf91946a9 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 23 Jan 2025 10:48:16 +0400 Subject: [PATCH 07/13] tests --- tests/test_models_encodings.py | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index d671c5d261..1016cc3768 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -6,13 +6,16 @@ import numpy as np import pytest from scipy.optimize import curve_fit +from scipy.special import binom from qibo import Circuit, gates from qibo.models.encodings import ( + _ehrlich_algorithm, binary_encoder, comp_basis_encoder, entangling_layer, ghz_state, + hamming_weight_encoder, phase_encoder, unary_encoder, unary_encoder_random_gaussian, @@ -213,6 +216,53 @@ def test_unary_encoder_random_gaussian(backend, nqubits, seed): backend.assert_allclose(stddev, theoretical_norm, atol=1e-1) +@pytest.mark.parametrize("optimize_controls", [False, True]) +@pytest.mark.parametrize("complex_data", [False, True]) +@pytest.mark.parametrize("full_hwp", [False, True]) +@pytest.mark.parametrize("weight", [1, 2, 3]) +@pytest.mark.parametrize("nqubits", [5, 6]) +def test_hamming_weight_encoder( + backend, + nqubits, + weight, + full_hwp, + complex_data, + optimize_controls, +): + n_choose_k = int(binom(nqubits, weight)) + dims = 2**nqubits + dtype = complex if complex_data else float + + initial_string = np.array([1] * weight + [0] * (nqubits - weight)) + indices = _ehrlich_algorithm(initial_string, False) + indices = [int(string, 2) for string in indices] + + rng = np.random.default_rng(10) + data = rng.random(n_choose_k) + if complex_data: + data = data.astype(complex) + 1j * rng.random(n_choose_k) + data /= np.linalg.norm(data) + + target = np.zeros(dims, dtype=dtype) + target[indices] = data + target = backend.cast(target, dtype=target.dtype) + + circuit = hamming_weight_encoder( + data, + nqubits=nqubits, + weight=weight, + full_hwp=full_hwp, + optimize_controls=optimize_controls, + ) + if full_hwp: + circuit.queue = [ + gates.X(nqubits - 1 - qubit) for qubit in range(weight) + ] + circuit.queue + state = backend.execute_circuit(circuit).state() + + backend.assert_allclose(state, target) + + def test_entangling_layer_errors(): with pytest.raises(TypeError): entangling_layer(10.5) @@ -269,6 +319,10 @@ def test_entangling_layer(nqubits, architecture, entangling_gate, closed_boundar circuit = entangling_layer(nqubits, architecture, entangling_gate, closed_boundary) for gate, target in zip(circuit.queue, target_circuit.queue): assert gate.__class__.__name__ == target.__class__.__name__ + assert gate.qubits == target.qubits + assert gate.target_qubits == target.target_qubits + assert gate.control_qubits == target.control_qubits + assert gate.parameters == target.parameters def _helper_entangling_test(gate, qubit_0, qubit_1=None): From 917fe0856af9ea46cdcd3d11943e01bfd16d907a Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 23 Jan 2025 11:28:04 +0400 Subject: [PATCH 08/13] remove `decompose` --- src/qibo/models/encodings.py | 137 ++++----------------------------- tests/test_models_encodings.py | 4 +- 2 files changed, 16 insertions(+), 125 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 6f8c169763..a4e18cbe92 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -330,7 +330,6 @@ def hamming_weight_encoder( weight: int, full_hwp: bool = False, optimize_controls: bool = True, - decompose: bool = False, **kwargs, ): """Create circuit that encodes ``data`` in the Hamming-weight-:math:`k` basis of ``nqubits``. @@ -355,10 +354,6 @@ def hamming_weight_encoder( Hamming weight preserving. Defaults to ```False``. optimize_controls (bool, optional): if ``True``, removes unnecessary controlled operations. Defaults to ``True``. - decompose (bool, optional): if ``True``, decomposes the (possibly multi-controlled) - :class:`qibo.gates.RBS` gates into :class:`qibo.gates.CNOT` gates and single-qubit - rotations. If ``False``, returns circuit as composition of (multi-)controlled - :class:`qibo.gates.RBS` gates. Defaults to ``False``. kwargs (dict, optional): Additional arguments used to initialize a Circuit object. For details, see the documentation of :class:`qibo.models.circuit.Circuit`. @@ -419,7 +414,6 @@ def hamming_weight_encoder( controls, theta, phi, - decompose, complex_data, ) circuit.add(gate) @@ -759,13 +753,12 @@ def _get_gate( controls: List[int], theta: float, phi: float, - decompose: bool = False, complex_data: bool = False, ): """Return gate(s) necessary to encode a complex amplitude in a given computational basis state, given the computational basis state used to encode the previous amplitude. - Information about computational baiss states in question are contained + Information about computational basis states in question are contained in the indexing of ``qubits_in``, ``qubits_out``, and ``controls``. Args: @@ -774,8 +767,6 @@ def _get_gate( controls (list): list of qubits that control the resulting gate. theta (float): first phase, used to encode the ``abs`` of amplitude. phi (float): second phase, used to encode the complex phase of amplitude. - decompose (bool): if ``True``, all two-qubit gates are decomposed in terms of CNOTs. - If ``False``, original gates are left uncompiled. Defaults to ``False``. complex_data (bool): if ``True``, uses :class:`qibo.gates.U3` to as basis gate. If ``False``, uses :class:`qibo.gates.RY` as basis gate. Defaults to ``False``. @@ -795,17 +786,20 @@ def _get_gate( ) gate_list = [gate_list] elif len(qubits_in) == 1 and len(qubits_out) == 1: - ## chooses best combilation of complex RBS gate + ## chooses best combination of complex RBS gate ## given number of controls and if data is real or complex - gate_list = _compiled_RBS( - qubits_in, - qubits_out, - controls, - theta, - phi, - decompose, - complex_data, - ) + gate_list = [] + gate = gates.RBS(*qubits_in, *qubits_out, theta) + if len(controls) > 0: + gate = gate.controlled_by(*controls) + gate_list.append(gate) + + if complex_data: + gate = [gates.RZ(*qubits_in, -phi), gates.RZ(*qubits_out, phi)] + if len(controls) > 0: + gate = [g.controlled_by(*controls) for g in gate] + gate_list.extend(gate) + else: gate_list = [ gates.GeneralizedRBS( @@ -816,109 +810,6 @@ def _get_gate( return gate_list -def _compiled_RBS( - qubits_in: List[int], - qubits_out: List[int], - controls: List[int], - theta: float, - phi: float, - decompose: bool = False, - complex_data: bool = False, -) -> List: - """Returns (un)compiled version of complex RBS gate depending on number of controls and data type. - - Qubit labelling has to be flipped from paper below due to difference between - big-endinan and little-endian notations (see e.g. - `Wikipedia page on Endianness `_). - - Args: - qubits_in (List[int]): qubits with ``in`` label. - qubits_out (List[int]): qubits with ``out`` label. - controls (List[int]): qubits that control the RBS gate. - theta (float): first phase, used to encode the ``abs`` of amplitude. - phi (float): second phase, used to encode the complex phase of amplitude. - decompose (bool): if ``True``, all two-qubit gates are decomposed in terms of CNOTs. - If ``False``, original gates are left uncompiled. Defaults to ``False``. - complex_data (bool): if ``True``, uses :class:`qibo.gates.U3` to as basis gate. - If ``False``, uses :class:`qibo.gates.RY` as basis gate. Defaults to ``False``. - - Returns: - List[:class:`qibo.gates.Gate`]: compilation (or not) of (controlled) RBS gate. - - References: - 1. R. M. S. Farias, T. O. Maciel, G. Camilo, R. Lin, S. Ramos-Calderer, and L. Aolita, - *Quantum encoder for fixed Hamming-weight subspaces* - `arXiv:2405.20408 [quant-ph] `_. - """ - gate_list = [] - - if len(controls) == 0: - if decompose: - # Without controls, the best decomposition uses two CNOTs - gate_list.extend( - [ - gates.H(*qubits_in), - gates.CNOT(*qubits_in, *qubits_out), - gates.RY(*qubits_in, theta), - gates.RY(*qubits_out, theta), - gates.CNOT(*qubits_in, *qubits_out), - gates.H(*qubits_in), - ] - ) - else: - gate_list.append(gates.RBS(*qubits_in, *qubits_out, theta)) - - if complex_data: - gate_list.append(gates.RZ(*qubits_in, -phi)) - gate_list.append(gates.RZ(*qubits_out, phi)) - - elif len(controls) == 1 and not complex_data: - if decompose: - # Best decomposition for 1 control and real data - gate_list.extend( - [ - gates.H(*qubits_out), - gates.CNOT(*qubits_out, *qubits_in), - gates.RY(*qubits_out, 2 * theta).controlled_by(*controls), - gates.RY(*qubits_in, 2 * theta).controlled_by(*controls), - gates.CNOT(*qubits_out, *qubits_in), - gates.H(*qubits_out), - ] - ) - else: - gate_list.append( - gates.RBS(*qubits_in, *qubits_out, theta).controlled_by(*controls) - ) - - else: - if decompose: - # Best decompositon in any other case - controls.extend(qubits_in) - gate_list.append(gates.CNOT(*qubits_out, *qubits_in)) - - if complex_data: - gate_list.append( - gates.U3(*qubits_out, 2 * theta, 2 * phi, 0.0).controlled_by( - *controls - ) - ) - else: - gate_list.append( - gates.RY(*qubits_out, 2 * theta).controlled_by(*controls) - ) - - gate_list.append(gates.CNOT(*qubits_out, *qubits_in)) - else: - gate_list.append( - gates.RBS(*qubits_in, *qubits_out, theta).controlled_by(*controls) - ) - if complex_data: - gate_list.append(gates.RZ(*qubits_in, -phi).controlled_by(*controls)) - gate_list.append(gates.RZ(*qubits_out, phi).controlled_by(*controls)) - - return gate_list - - def _get_phase_gate_correction(last_string, phase: float): # to avoid circular import error from qibo.quantum_info.utils import hamming_weight diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index 1016cc3768..703b6e3b53 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -220,7 +220,7 @@ def test_unary_encoder_random_gaussian(backend, nqubits, seed): @pytest.mark.parametrize("complex_data", [False, True]) @pytest.mark.parametrize("full_hwp", [False, True]) @pytest.mark.parametrize("weight", [1, 2, 3]) -@pytest.mark.parametrize("nqubits", [5, 6]) +@pytest.mark.parametrize("nqubits", [4, 5, 6]) def test_hamming_weight_encoder( backend, nqubits, @@ -260,7 +260,7 @@ def test_hamming_weight_encoder( ] + circuit.queue state = backend.execute_circuit(circuit).state() - backend.assert_allclose(state, target) + backend.assert_allclose(state, target, atol=1e-7) def test_entangling_layer_errors(): From 5d1dc8cf7d5b61a43d8eb6ef87935874799c5c5c Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 23 Jan 2025 12:52:42 +0400 Subject: [PATCH 09/13] fix coverage --- src/qibo/models/encodings.py | 10 ++++------ tests/test_models_encodings.py | 3 +++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index a4e18cbe92..7f1cd8625f 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -666,11 +666,7 @@ def _angle_mod_two_pi(angle): def _get_markers(bitstring, last_run: bool = False): - if not isinstance(bitstring, np.ndarray): - bitstring = np.asarray(bitstring) - nqubits = len(bitstring) - markers = [len(bitstring) - 1] for ind, value in zip(range(nqubits - 2, -1, -1), bitstring[::-1][1:]): if value == bitstring[-1]: @@ -778,7 +774,8 @@ def _get_gate( *Quantum encoder for fixed Hamming-weight subspaces* `arXiv:2405.20408 [quant-ph] `_. """ - if len(qubits_in) == 0 and len(qubits_out) == 1: + if len(qubits_in) == 0 and len(qubits_out) == 1: # pragma: no cover + # Important for future binary encoder gate_list = ( gates.U3(*qubits_out, 2 * theta, 2 * phi, 0.0).controlled_by(*controls) if complex_data @@ -800,7 +797,8 @@ def _get_gate( gate = [g.controlled_by(*controls) for g in gate] gate_list.extend(gate) - else: + else: # pragma: no cover + # Important for future sparse encoder gate_list = [ gates.GeneralizedRBS( list(qubits_in), list(qubits_out), theta, phi diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index 703b6e3b53..59088aa9c1 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -262,6 +262,9 @@ def test_hamming_weight_encoder( backend.assert_allclose(state, target, atol=1e-7) + # edge case for coverage + _ehrlich_algorithm(np.array([0] * nqubits)) + def test_entangling_layer_errors(): with pytest.raises(TypeError): From d7214985d6b96a87ce4d61fce2c20055baaac429 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 23 Jan 2025 13:57:53 +0400 Subject: [PATCH 10/13] fix coverage --- tests/test_models_encodings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index 59088aa9c1..f553ddcd1b 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -11,6 +11,7 @@ from qibo import Circuit, gates from qibo.models.encodings import ( _ehrlich_algorithm, + _get_next_bistring, binary_encoder, comp_basis_encoder, entangling_layer, @@ -264,6 +265,7 @@ def test_hamming_weight_encoder( # edge case for coverage _ehrlich_algorithm(np.array([0] * nqubits)) + _get_next_bistring(np.array([0] * nqubits), [], 2) def test_entangling_layer_errors(): From 8d26bb9dd3b61bd5cb0f0d65a1810afdc476a91b Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Fri, 24 Jan 2025 09:37:28 +0000 Subject: [PATCH 11/13] Update src/qibo/models/encodings.py Co-authored-by: BrunoLiegiBastonLiegi <45011234+BrunoLiegiBastonLiegi@users.noreply.github.com> --- src/qibo/models/encodings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 7f1cd8625f..d9d1ab4922 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -371,7 +371,7 @@ def hamming_weight_encoder( initial_string = np.array([1] * weight + [0] * (nqubits - weight)) bitstrings, targets_and_controls = _ehrlich_algorithm(initial_string) - """Calculate all gate phases necessary to encode the amplitudes.""" + # Calculate all gate phases necessary to encode the amplitudes. _data = np.abs(data) if complex_data else data thetas = _generate_rbs_angles(_data, nqubits, architecture="diagonal") thetas = np.asarray(thetas, dtype=type(thetas[0])) From 0e1146d0327196865ee4e934cf57d8cdb92f15c4 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 25 Jan 2025 05:09:37 +0000 Subject: [PATCH 12/13] Update src/qibo/models/encodings.py Co-authored-by: BrunoLiegiBastonLiegi <45011234+BrunoLiegiBastonLiegi@users.noreply.github.com> --- src/qibo/models/encodings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index d9d1ab4922..6130cf1bc0 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -403,10 +403,7 @@ def hamming_weight_encoder( controls.sort() if optimize_controls: - comparison = k < np.asarray(indices) - controls_to_remove = list(np.argwhere(comparison == True).flatten()) - for control_index in controls_to_remove[::-1]: - controls.pop(control_index) + controls = list(np.asarray(controls)[k >= np.asarray(indices)]) gate = _get_gate( [targets[0]], From 2874b04b61ae59714e891fe9caa84ac7ed199ace Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 25 Jan 2025 09:14:52 +0400 Subject: [PATCH 13/13] andrea's suggestions --- src/qibo/models/encodings.py | 5 ++--- tests/test_models_encodings.py | 4 ---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 6130cf1bc0..95b597d4b5 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -390,7 +390,6 @@ def hamming_weight_encoder( ) if optimize_controls: - controls_to_remove = list(range(last_qubit, last_qubit - weight, -1)) indices = [ int(binom(nqubits - j, weight - j)) - 1 for j in range(weight - 1, 0, -1) ] @@ -680,7 +679,7 @@ def _get_markers(bitstring, last_run: bool = False): def _get_next_bistring(bitstring, markers, hamming_weight): - if len(markers) == 0: + if len(markers) == 0: # pragma: no cover return bitstring new_bitstring = np.copy(bitstring) @@ -718,7 +717,7 @@ def _get_next_bistring(bitstring, markers, hamming_weight): def _ehrlich_algorithm(initial_string, return_indices: bool = True): k = np.unique(initial_string, return_counts=True) - if len(k[1]) == 1: + if len(k[1]) == 1: # pragma: no cover return ["".join([str(item) for item in initial_string])] k = k[1][1] diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index f553ddcd1b..900af85f44 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -263,10 +263,6 @@ def test_hamming_weight_encoder( backend.assert_allclose(state, target, atol=1e-7) - # edge case for coverage - _ehrlich_algorithm(np.array([0] * nqubits)) - _get_next_bistring(np.array([0] * nqubits), [], 2) - def test_entangling_layer_errors(): with pytest.raises(TypeError):