# This code is part of Qiskit.
#
# (C) Copyright IBM 2021
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Driver for a synthesis routine which emits optimal XX-based circuits.
"""
from __future__ import annotations
import heapq
import math
from operator import itemgetter
from typing import Callable
import numpy as np
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.library.standard_gates import RXXGate, RZXGate
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators import Operator
from qiskit.quantum_info.synthesis.one_qubit_decompose import ONE_QUBIT_EULER_BASIS_GATES
from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitWeylDecomposition
from .circuits import apply_reflection, apply_shift, canonical_xx_circuit
from .utilities import EPSILON
from .polytopes import XXPolytope
def _average_infidelity(p, q):
"""
Computes the infidelity distance between two points p, q expressed in positive canonical
coordinates.
"""
a0, b0, c0 = p
a1, b1, c1 = q
return 1 - 1 / 20 * (
4
+ 16
* (
math.cos(a0 - a1) ** 2 * math.cos(b0 - b1) ** 2 * math.cos(c0 - c1) ** 2
+ math.sin(a0 - a1) ** 2 * math.sin(b0 - b1) ** 2 * math.sin(c0 - c1) ** 2
)
)
[documentos]class XXDecomposer:
"""
A class for optimal decomposition of 2-qubit unitaries into 2-qubit basis gates of XX type
(i.e., each locally equivalent to CAN(alpha, 0, 0) for a possibly varying alpha).
Args:
basis_fidelity: available strengths and fidelity of each.
Can be either (1) a dictionary mapping XX angle values to fidelity at that angle; or
(2) a single float f, interpreted as {pi: f, pi/2: f/2, pi/3: f/3}.
euler_basis: Basis string provided to OneQubitEulerDecomposer for 1Q synthesis.
Defaults to "U".
embodiments: A dictionary mapping interaction strengths alpha to native circuits which
embody the gate CAN(alpha, 0, 0). Strengths are taken so that pi/2 represents the class
of a full CX.
backup_optimizer: If supplied, defers synthesis to this callable when XXDecomposer
has no efficient decomposition of its own. Useful for special cases involving 2 or 3
applications of XX(pi/2), in which case standard synthesis methods provide lower
1Q gate count.
.. note::
If ``embodiments`` is not passed, or if an entry is missing, it will be populated as needed
using the method ``_default_embodiment``.
"""
def __init__(
self,
basis_fidelity: dict | float = 1.0,
euler_basis: str = "U",
embodiments: dict[float, QuantumCircuit] | None = None,
backup_optimizer: Callable[..., QuantumCircuit] | None = None,
):
from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import (
Optimize1qGatesDecomposition, # pylint: disable=cyclic-import
)
self._decomposer1q = Optimize1qGatesDecomposition(ONE_QUBIT_EULER_BASIS_GATES[euler_basis])
self.embodiments = embodiments if embodiments is not None else {}
self.backup_optimizer = backup_optimizer
self.basis_fidelity = basis_fidelity
# expose one of the basis gates so others can know what this decomposer targets
embodiment_circuit = next(iter(self.embodiments.values()), QuantumCircuit())
for instruction in embodiment_circuit:
if len(instruction.qubits) == 2:
self.gate = instruction.operation
break
else:
self.gate = RZXGate(np.pi / 2)
self._check_embodiments()
@staticmethod
def _default_embodiment(strength):
"""
If the user does not provide a custom implementation of XX(strength), then this routine
defines a default implementation using RZX.
"""
xx_circuit = QuantumCircuit(2)
# NOTE: One could branch here on `strength == np.pi / 2` and decide to use a CX-based
# circuit in this one case where it's available.
xx_circuit.h(0)
xx_circuit.rzx(strength, 0, 1)
xx_circuit.h(0)
return xx_circuit
def _check_embodiments(self):
"""
Checks that `self.embodiments` is populated with legal circuit embodiments: the key-value
pair (angle, circuit) satisfies Operator(circuit) approx RXX(angle).to_matrix().
"""
# pylint: disable=cyclic-import
from qiskit.quantum_info.operators.measures import average_gate_fidelity
for angle, embodiment in self.embodiments.items():
actual = Operator(RXXGate(angle))
purported = Operator(embodiment)
if average_gate_fidelity(actual, purported) < 1 - EPSILON:
raise QiskitError(
f"RXX embodiment provided for angle {angle} disagrees with RXXGate({angle})"
)
@staticmethod
def _best_decomposition(canonical_coordinate, available_strengths):
"""
Finds the cheapest sequence of `available_strengths` which supports the best approximation
to `canonical_coordinate`. Returns a dictionary with keys "cost", "point", and "operations".
NOTE: `canonical_coordinate` is a positive canonical coordinate. `strengths` is a dictionary
mapping the available strengths to their (infidelity) costs, with the strengths
themselves normalized so that pi/2 represents CX = RZX(pi/2).
"""
best_point, best_cost, best_sequence = [0, 0, 0], 1.0, []
priority_queue = []
heapq.heappush(priority_queue, (0, []))
canonical_coordinate = np.array(canonical_coordinate)
while True:
if len(priority_queue) == 0:
if len(available_strengths) == 0:
raise QiskitError(
"Attempting to synthesize entangling gate with no controlled gates in basis set."
)
raise QiskitError("Unable to synthesize a 2q unitary with the supplied basis set.")
sequence_cost, sequence = heapq.heappop(priority_queue)
strength_polytope = XXPolytope.from_strengths(*[x / 2 for x in sequence])
candidate_point = strength_polytope.nearest(canonical_coordinate)
candidate_cost = sequence_cost + _average_infidelity(
canonical_coordinate, candidate_point
)
if candidate_cost < best_cost:
best_point, best_cost, best_sequence = candidate_point, candidate_cost, sequence
if strength_polytope.member(canonical_coordinate):
break
for strength, extra_cost in available_strengths.items():
if len(sequence) == 0 or strength <= sequence[-1]:
heapq.heappush(
priority_queue, (sequence_cost + extra_cost, sequence + [strength])
)
return {"point": best_point, "cost": best_cost, "sequence": best_sequence}
[documentos] def num_basis_gates(self, unitary: Operator | np.ndarray):
"""
Counts the number of gates that would be emitted during re-synthesis.
NOTE: Used by ConsolidateBlocks.
"""
strengths = self._strength_to_infidelity(1.0)
# get the associated _positive_ canonical coordinate
weyl_decomposition = TwoQubitWeylDecomposition(unitary)
target = [getattr(weyl_decomposition, x) for x in ("a", "b", "c")]
if target[-1] < -EPSILON:
target = [np.pi / 2 - target[0], target[1], -target[2]]
best_sequence = self._best_decomposition(target, strengths)["sequence"]
return len(best_sequence)
@staticmethod
def _strength_to_infidelity(basis_fidelity, approximate=False):
"""
Converts a dictionary mapping XX strengths to fidelities to a dictionary mapping XX
strengths to infidelities. Also supports one of the other formats Qiskit uses: if only a
lone float is supplied, it extends it from CX over CX/2 and CX/3 by linear decay.
"""
if isinstance(basis_fidelity, float):
if not approximate:
slope, offset = 1e-10, 1e-12
else:
slope, offset = (1 - basis_fidelity) / 2, (1 - basis_fidelity) / 2
return {
strength: slope * strength / (np.pi / 2) + offset
for strength in [np.pi / 2, np.pi / 4, np.pi / 6]
}
elif isinstance(basis_fidelity, dict):
return {
strength: (1 - fidelity if approximate else 1e-12 + 1e-10 * strength / (np.pi / 2))
for (strength, fidelity) in basis_fidelity.items()
}
raise TypeError("Unknown basis_fidelity payload.")
def __call__(
self,
unitary: Operator | np.ndarray,
basis_fidelity: dict | float | None = None,
approximate: bool = True,
) -> QuantumCircuit:
"""
Fashions a circuit which (perhaps `approximate`ly) models the special unitary operation
`unitary`, using the circuit templates supplied at initialization as `embodiments`. The
routine uses `basis_fidelity` to select the optimal circuit template, including when
performing exact synthesis; the contents of `basis_fidelity` is a dictionary mapping
interaction strengths (scaled so that CX = RZX(pi/2) corresponds to pi/2) to circuit
fidelities.
Args:
unitary (Operator or ndarray): 4x4 unitary to synthesize.
basis_fidelity (dict or float): Fidelity of basis gates. Can be either (1) a dictionary
mapping XX angle values to fidelity at that angle; or (2) a single float f,
interpreted as {pi: f, pi/2: f/2, pi/3: f/3}.
If given, overrides the basis_fidelity given at init.
approximate (bool): Approximates if basis fidelities are less than 1.0 .
Returns:
QuantumCircuit: Synthesized circuit.
"""
basis_fidelity = basis_fidelity or self.basis_fidelity
strength_to_infidelity = self._strength_to_infidelity(
basis_fidelity, approximate=approximate
)
from qiskit.extensions import UnitaryGate # pylint: disable=cyclic-import
# get the associated _positive_ canonical coordinate
weyl_decomposition = TwoQubitWeylDecomposition(unitary)
target = [getattr(weyl_decomposition, x) for x in ("a", "b", "c")]
if target[-1] < -EPSILON:
target = [np.pi / 2 - target[0], target[1], -target[2]]
# scan for the best point
best_point, best_sequence = itemgetter("point", "sequence")(
self._best_decomposition(target, strength_to_infidelity)
)
# build the circuit building this canonical gate
embodiments = {
k: self.embodiments.get(k, self._default_embodiment(k))
for k, v in strength_to_infidelity.items()
}
circuit = canonical_xx_circuit(best_point, best_sequence, embodiments)
if (
best_sequence in ([np.pi / 2, np.pi / 2, np.pi / 2], [np.pi / 2, np.pi / 2])
and self.backup_optimizer is not None
):
pi2_fidelity = 1 - strength_to_infidelity[np.pi / 2]
return self.backup_optimizer(unitary, basis_fidelity=pi2_fidelity)
# change to positive canonical coordinates
if weyl_decomposition.c >= -EPSILON:
# if they're the same...
corrected_circuit = QuantumCircuit(2)
corrected_circuit.rz(np.pi, [0])
corrected_circuit.compose(circuit, [0, 1], inplace=True)
corrected_circuit.rz(-np.pi, [0])
circuit = corrected_circuit
else:
# else they're in the "positive" scissors part...
corrected_circuit = QuantumCircuit(2)
_, source_reflection, _ = apply_reflection("reflect XX, ZZ", [0, 0, 0])
_, source_shift, _ = apply_shift("X shift", [0, 0, 0])
corrected_circuit.compose(source_reflection.inverse(), inplace=True)
corrected_circuit.rz(np.pi, [0])
corrected_circuit.compose(circuit, [0, 1], inplace=True)
corrected_circuit.rz(-np.pi, [0])
corrected_circuit.compose(source_shift.inverse(), inplace=True)
corrected_circuit.compose(source_reflection, inplace=True)
corrected_circuit.global_phase += np.pi / 2
circuit = corrected_circuit
circ = QuantumCircuit(2, global_phase=weyl_decomposition.global_phase)
circ.append(UnitaryGate(weyl_decomposition.K2r), [0])
circ.append(UnitaryGate(weyl_decomposition.K2l), [1])
circ.compose(circuit, [0, 1], inplace=True)
circ.append(UnitaryGate(weyl_decomposition.K1r), [0])
circ.append(UnitaryGate(weyl_decomposition.K1l), [1])
circ = self._decomposer1q(circ)
return circ