from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Sequence
import numpy as np
import numpy.typing as npt
from numpy.random import Generator
from mpqp.core.circuit import QCircuit
from mpqp.core.instruction.gates.gate import Gate, SingleQubitGate
from mpqp.core.instruction.gates.native_gates import (
NATIVE_GATES,
TOF,
CRk,
P,
Rk,
RotationGate,
Rx,
Ry,
Rz,
U,
)
from mpqp.core.instruction.gates.parametrized_gate import ParametrizedGate
from mpqp.noise.noise_model import (
NOISE_MODELS,
AmplitudeDamping,
BitFlip,
Dephasing,
Depolarizing,
NoiseModel,
PhaseDamping,
)
from mpqp.tools.maths import closest_unitary
if TYPE_CHECKING:
from qiskit import QuantumCircuit
from qiskit._accelerate.circuit import CircuitInstruction
[docs]
def random_circuit(
gate_classes: Optional[Sequence[type[Gate]]] = None,
nb_qubits: int = 5,
nb_gates: Optional[int] = None,
seed: Optional[int] = None,
):
"""This function creates a QCircuit with a specified number of qubits and gates.
The gates are chosen randomly from the provided list of native gate classes.
args:
nb_qubits : Number of qubits in the circuit.
gate_classes : List of native gate classes to use in the circuit.
nb_gates : Number of gates to add to the circuit.
seed: Seed used to initialize the random number generation.
Returns:
A quantum circuit with the specified number of qubits and randomly chosen gates.
Raises:
ValueError: If the number of qubits is too low for the specified gates.
Examples:
>>> print(random_circuit([U, TOF], 3)) # doctest: +NORMALIZE_WHITESPACE
┌───┐┌───┐ ┌───┐ ┌───┐┌───┐
q_0: ┤ X ├┤ X ├──■──┤ X ├────────────────────────────┤ X ├┤ X ├
└─┬─┘└─┬─┘┌─┴─┐└─┬─┘ └─┬─┘└─┬─┘
q_1: ──■────■──┤ X ├──■────────────────────────────────■────■──
│ │ └─┬─┘ │ ┌──────────────────────────┐ │ │
q_2: ──■────■────■────■──┤ U(0.31076,5.5951,6.2613) ├──■────■──
└──────────────────────────┘
>>> print(random_circuit([U, TOF], 3, seed=123)) # doctest: +NORMALIZE_WHITESPACE
┌───┐ ┌───┐
q_0: ──■──┤ X ├───────────────────────────┤ X ├
┌─┴─┐└─┬─┘┌─────────────────────────┐└─┬─┘
q_1: ┤ X ├──■──┤ U(5.1025,5.8015,1.7378) ├──■──
└─┬─┘ │ ├─────────────────────────┤ │
q_2: ──■────■──┤ U(5.5914,3.2231,1.5392) ├──■──
└─────────────────────────┘
"""
rng = np.random.default_rng(seed)
if nb_gates is None:
nb_gates = int(rng.integers(5, 10))
qcircuit = QCircuit(nb_qubits)
for _ in range(nb_gates):
qcircuit.add(random_gate(gate_classes, nb_qubits, rng))
return qcircuit
[docs]
def statevector_from_random_circuit(
nb_qubits: int = 5,
seed: Optional[int] = None,
) -> npt.NDArray[np.complex128]:
"""
This function creates a statevector with a specified number of qubits,
generated from a random circuit executed on IBM AER Simulator.
The QCircuit is generated randomly and his statevector is calculated.
args:
nb_qubits : Number of qubits in the circuit.
seed: Seed used to initialize the random number generation.
Returns:
The statevector with the specified number of qubits
Examples:
>>> print(statevector_from_random_circuit(2, seed=123)) # doctest: +NORMALIZE_WHITESPACE
[0.70710678+0.j 0. -0.j 0.26893257-0.65396886j 0. -0.j ]
"""
from mpqp.execution import IBMDevice, Result, run
mpqp_circ = random_circuit(None, nb_qubits, None, seed)
res = run(mpqp_circ, IBMDevice.AER_SIMULATOR_STATEVECTOR)
if TYPE_CHECKING:
assert isinstance(res, Result)
return res.state_vector.vector
[docs]
def random_gate(
gate_classes: Optional[Sequence[type[Gate]]] = None,
nb_qubits: int = 5,
seed: Optional[int | Generator] = None,
) -> Gate:
"""This function creates a gate with a specified number of qubits.
The gate are chosen randomly from the provided list of native gate classes.
args:
nb_qubits : Number of qubits in the circuit.
gate_classes : List of native gate classes to use in the circuit.
Returns:
A quantum circuit with the specified number of qubits and randomly chosen gates.
Raises:
ValueError: If the number of qubits is too low for the specified gates.
Examples:
>>> random_gate([U, TOF], 3) # doctest: +SKIP
U(2.067365317109373, 0.18652872274018245, 0.443968374745352, 0)
>>> random_gate(nb_qubits=4) # doctest: +SKIP
SWAP(3, 1)
"""
rng = np.random.default_rng(seed)
if gate_classes is None:
gate_classes = []
for gate in NATIVE_GATES:
if TYPE_CHECKING:
assert isinstance(gate.nb_qubits, int)
if gate.nb_qubits <= nb_qubits:
gate_classes.append(gate)
qubits = list(range(nb_qubits))
if any(
not issubclass(gate, SingleQubitGate)
and ((gate == TOF and nb_qubits <= 2) or nb_qubits <= 1)
for gate in gate_classes
):
raise ValueError("number of qubits too low for this gates")
gate_class = rng.choice(np.array(gate_classes))
target = rng.choice(qubits).item()
if issubclass(gate_class, SingleQubitGate):
if issubclass(gate_class, ParametrizedGate):
if issubclass(gate_class, U):
return U(
np.round(rng.uniform(0, 2 * np.pi), 5),
np.round(rng.uniform(0, 2 * np.pi), 5),
np.round(rng.uniform(0, 2 * np.pi), 5),
target,
)
elif issubclass(gate_class, Rk):
return Rk(int(rng.integers(1, 10)), target)
elif issubclass(gate_class, RotationGate):
if TYPE_CHECKING:
assert issubclass(gate_class, (Rx, Ry, Rz, P))
return gate_class(np.round(rng.uniform(0, 2 * np.pi), 5), target)
else:
raise ValueError
else:
return gate_class(target)
else:
control = rng.choice(list(set(qubits) - {target})).item()
if issubclass(gate_class, ParametrizedGate):
if TYPE_CHECKING:
assert issubclass(gate_class, CRk)
return gate_class(
int(rng.integers(1, 10)),
control,
target,
)
elif issubclass(gate_class, TOF):
control2 = rng.choice(list(set(qubits) - {target, control})).item()
return TOF([control, control2], target)
else:
return gate_class(control, target)
[docs]
def random_noise(
noise_model: Optional[Sequence[type[NoiseModel]]] = None,
seed: Optional[int | Generator] = None,
) -> NoiseModel:
"""This function creates a noise model.
The noise are chosen randomly from the provided list of noise model.
args:
noise_model : List of noise model.
Returns:
A quantum circuit with the specified number of qubits and randomly chosen gates.
Raises:
ValueError: If the number of qubits is too low for the specified gates.
Examples:
>>> random_noise() # doctest: +SKIP
Depolarizing(0.37785041428875576)
"""
rng = np.random.default_rng(seed)
if noise_model is None:
noise_model = NOISE_MODELS
noise = rng.choice(np.array(noise_model))
if issubclass(noise, AmplitudeDamping):
prob = rng.uniform(0, 1)
return AmplitudeDamping(prob)
elif issubclass(noise, BitFlip):
prob = rng.uniform(0, 0.5)
return BitFlip(prob)
elif issubclass(noise, Dephasing):
prob = rng.uniform(0, 1)
return Depolarizing(prob)
elif issubclass(noise, Depolarizing):
prob = rng.uniform(0, 0.75)
return Depolarizing(prob)
elif issubclass(noise, PhaseDamping):
gamma = rng.uniform(0, 1)
return PhaseDamping(gamma)
else:
raise NotImplementedError(f"{noise} model not implemented")
[docs]
def compute_expected_matrix(qcircuit: QCircuit):
"""
Computes the expected matrix resulting from applying single-qubit gates
in reverse order on a quantum circuit.
args:
qcircuit : The quantum circuit object containing instructions.
returns:
Expected matrix resulting from applying the gates.
raises:
ValueError: If any gate in the circuit is not a SingleQubitGate.
"""
from sympy import N
from mpqp.core.instruction.gates.gate import Gate, SingleQubitGate
gates = [
instruction
for instruction in qcircuit.instructions
if isinstance(instruction, Gate)
]
nb_qubits = qcircuit.nb_qubits
result_matrix = np.eye(2**nb_qubits, dtype=complex)
for gate in reversed(gates):
if not isinstance(gate, SingleQubitGate):
raise ValueError(
f"Unsupported gate: {type(gate)} only SingleQubitGate can be computed for now"
)
matrix = np.eye(2**nb_qubits, dtype=complex)
gate_matrix = gate.to_matrix()
index = gate.targets[0]
matrix = np.kron(
np.eye(2**index, dtype=complex),
np.kron(gate_matrix, np.eye(2 ** (nb_qubits - index - 1), dtype=complex)),
)
result_matrix = np.dot(result_matrix, matrix)
return np.vectorize(N)(result_matrix).astype(complex)
[docs]
def replace_custom_gate(
custom_unitary: "CircuitInstruction", nb_qubits: int, targets: list[int]
) -> "tuple[QuantumCircuit, float]":
"""Decompose and replace the (custom) qiskit unitary given in parameter by a
qiskit `QuantumCircuit` composed of ``U`` and ``CX`` gates.
Note:
When using Qiskit, a global phase is introduced (related to usage of
``u`` in OpenQASM2). This may be problematic in some cases, so this
function also returns the global phase introduced so it can be corrected
later on.
Args:
custom_unitary: instruction containing the custom unitary operator.
nb_qubits: Number of qubits of the circuit from which the unitary
instruction was taken.
Returns:
A circuit containing the decomposition of the unitary in terms
of gates ``U`` and ``CX``, and the global phase used to
correct the statevector if need be.
"""
from qiskit import QuantumCircuit, transpile
from qiskit.exceptions import QiskitError
from qiskit.circuit.library import UnitaryGate
transpilation_circuit = QuantumCircuit(nb_qubits)
transpilation_circuit.append(custom_unitary)
try:
transpiled = transpile(
transpilation_circuit, basis_gates=['u3', 'cx'], optimization_level=0
)
except QiskitError as e:
# if the error is arising from TwoQubitWeylDecomposition, we replace the
# matrix by the closest unitary
if "TwoQubitWeylDecomposition" in str(e):
custom_closest_unitary = UnitaryGate(closest_unitary(custom_unitary.matrix))
transpilation_circuit = QuantumCircuit(nb_qubits)
transpilation_circuit.unitary(
custom_closest_unitary, list(reversed(targets))
)
transpiled = transpile(
transpilation_circuit,
basis_gates=['u1', 'u2', 'u3', 'cx'],
optimization_level=0,
)
else:
raise e
return transpiled, transpiled.global_phase