from copy import deepcopy
from typing import Optional
import numpy as np
from qibo import Circuit, gates
from qibo.backends import _check_backend
from qibo.config import raise_error
from qibo.models.encodings import entangling_layer
from qibo.quantum_info.random_ensembles import uniform_sampling_U3
from scipy.special import binom
[docs]def hardware_efficient(
nqubits: int,
qubits: Optional[tuple[int]] = None,
nlayers: int = 1,
single_block: Optional[Circuit] = None,
entangling_block: Optional[Circuit] = None,
entangling_gate: str = "CNOT",
architecture: str = "diagonal",
closed_boundary: bool = True,
seed: Optional[int | np.random.Generator] = None,
backend=None,
**kwargs,
) -> Circuit:
"""
Create a hardware-efficient ansatz with custom single-qubit layers and entangling blocks.
Args:
nqubits (int): Number of qubits :math:`n` in the ansatz.
qubits (tuple[int], optional): Qubit indexes to apply the ansatz to. If ``None``,
the ansatz is applied to all qubits from :math:`0` to :math:`nqubits-1`.
Defaults to ``None``.
nlayers (int, optional): Number of layers (single-qubit + entangling per layer). Defaults to :math:`1`.
single_block (Circuit, optional): :math:`1`-qubit circuit applied to each qubit.
If ``None``, defaults to a block with :class:`qibo.gates.RY` and
:class:`qibo.gates.RZ` gates with Haar-random sampled phases. Defaults to ``None``.
entangling_block (Circuit, optional): :math:`n`-qubit entangling circuit. Defaults to ``None``.
entangling_gate (str or :class:`qibo.gates.Gate`, optional): Only used if ``entangling_block``
is ``None``. Two-qubit gate to be used in the entangling layer if ``entangling_block`` is not
provided. If ``entangling_gate`` is a parametrized gate, all phases are initialized as
:math:`0.0`. Defaults to ``"CNOT"``.
architecture (str, optional): Only used if ``entangling_block`` is ``None``.
Architecture of the entangling layer. In alphabetical order, options are:
``"diagonal"``, ``"even_layer"``, ``"next_nearest"``, ``"odd_layer"``,
``"pyramid"``, ``"shifted"``, ``"v"``, and ``"x"``. The ``"x"`` architecture
is only defined for an even number of qubits. Defaults to ``"diagonal"``.
closed_boundary (bool, optional): Only used if ``entangling_block`` is ``None``.
If ``True`` and ``architecture not in ["pyramid", "v", "x"]``, adds a
closed-boundary condition to the entangling layer. Defaults to ``True``.
seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
numbers or a fixed seed to initialize a generator. If ``None``, initializes
a generator with a random seed. Default: ``None``.
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
in the execution. If ``None``, it uses the current backend.
Defaults to ``None``.
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`: Constructed hardware-efficient ansatz.
"""
circ = Circuit(nqubits, **kwargs)
if qubits is None:
qubits = list(range(nqubits))
elif len(qubits) > nqubits:
raise_error(
ValueError,
f"Number of specified qubits ({len(qubits)}) cannot exceed the total number of qubits in the circuit ({nqubits}).",
)
if single_block is None:
backend = _check_backend(backend)
phases = uniform_sampling_U3(1, seed, backend=backend)[0]
phases = backend.to_numpy(phases)
single_block = Circuit(1)
single_block.add(gates.RY(0, theta=phases[0], trainable=True))
single_block.add(gates.RZ(0, theta=phases[1], trainable=True))
for _ in range(nlayers):
for q in qubits:
circ.add(single_block.on_qubits(q))
if len(qubits) != 1:
if entangling_block is None:
entangling_block = entangling_layer(
nqubits=len(qubits),
architecture=architecture,
entangling_gate=entangling_gate,
closed_boundary=closed_boundary,
)
elif entangling_block.nqubits != len(qubits):
raise_error(
ValueError,
f"Entangling layer circuit must have {len(qubits)} qubits.",
)
circ.add(entangling_block.on_qubits(*qubits))
return circ
HardwareEfficient = hardware_efficient
[docs]def brickwork_givens(nqubits: int, weight: int, full_hwp: bool = False, **kwargs):
"""Create a Hamming-weight-preserving circuit based on brickwork layers of two-qubit Givens rotations.
Args:
nqubits (int): Total number of qubits.
weight (int): Hamming weight to be encoded.
full_hwp (bool, optional): If ``False``, returns circuit with the necessary
:class:`qibo.gates.X` gates included to generate the initial Hamming weight
to be preserved. If ``True``, circuit does not include the :class:`qibo.gates.X`
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`: Hamming-weight-preserving brickwork circuit.
References:
1. B. T. Gard, L. Zhu1, G. S. Barron, N. J. Mayhall, S. E. Economou, and Edwin Barnes,
*Efficient symmetry-preserving state preparation circuits for the variational quantum
eigensolver algorithm*, `npj Quantum Information (2020) 6:10
<https://doi.org/10.1038/s41534-019-0240-1>`_.
"""
n_choose_k = int(binom(nqubits, weight))
_weight = weight if not full_hwp else 0
half_filling = nqubits // 2
circuit = Circuit(nqubits, **kwargs)
if not full_hwp:
if weight > half_filling:
circuit.add(gates.X(2 * qubit) for qubit in range(half_filling))
circuit.add(
gates.X(2 * qubit + 1) for qubit in range(weight - half_filling)
)
else:
circuit.add(gates.X(2 * qubit) for qubit in range(weight))
for _ in range(n_choose_k // (nqubits - 1) - 1):
circuit += entangling_layer(
nqubits,
architecture="shifted",
entangling_gate=gates.GIVENS,
closed_boundary=False,
**kwargs,
)
ngates = len(circuit.gates_of_type(gates.GIVENS))
nmissing = (n_choose_k - 1) - ngates
queue = entangling_layer(
nqubits,
architecture="shifted",
entangling_gate=gates.GIVENS,
closed_boundary=False,
**kwargs,
).queue
circuit.add(deepcopy(queue[elem % len(queue)]) for elem in range(nmissing))
return circuit