# Source code for mitiq.pec.representations.biased_noise

# Copyright (C) 2022 Unitary Fund
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""Function to generate representations with biased noise."""
import copy
from typing import List

from cirq import (
Circuit,
Operation,
X,
Y,
Z,
)

from mitiq import QPROGRAM
from mitiq.pec import OperationRepresentation, NoisyOperation
from mitiq.interface.conversions import (
convert_to_mitiq,
append_cirq_circuit_to_qprogram,
)

[docs]def represent_operation_with_local_biased_noise(
ideal_operation: QPROGRAM,
epsilon: float,
eta: float,
) -> OperationRepresentation:
r"""This function maps an
ideal_operation :math:\mathcal{U} into its quasi-probability
representation, which is a linear combination of noisy implementable
operations :math:\sum_\alpha \eta_{\alpha} \mathcal{O}_{\alpha}.

This function assumes a combined depolarizing and dephasing noise model
with a bias factor :math:\eta (see :cite:Strikis_2021_PRXQuantum)
and that the following noisy operations are implementable
:math:\mathcal{O}_{\alpha} = \mathcal{D} \circ \mathcal P_\alpha
where :math:\mathcal{U} is the unitary associated
to the input ideal_operation,
:math:\mathcal{P}_\alpha is a Pauli operation and

.. math::
\mathcal{D}(\epsilon) = (1 - \epsilon)[\mathbb{1}] +
\epsilon(\frac{\eta}{\eta + 1} \mathcal{Z}
+ \frac{1}{3}\frac{1}{\eta + 1}(\mathcal{X} + \mathcal{Y}
+ \mathcal{Z}))

is the combined (biased) dephasing and depolarizing channel acting on a
single qubit. For multi-qubit operations, we use a noise channel that is
the tensor product of the local single-qubit channels.

Args:
ideal_operation: The ideal operation (as a QPROGRAM) to represent.
epsilon: The local noise severity (as a float) of the combined channel.
eta: The noise bias between combined dephasing and depolarizing
channels with :math:\eta = 0 describing a fully depolarizing
channel and :math:\eta = \inf describing a fully dephasing
channel.

Returns:
The quasi-probability representation of the ideal_operation.

.. note::
This representation is based on the ideal assumption that one
can append Pauli gates to a noisy operation without introducing
additional noise. For a backend which violates this assumption,
it remains a good approximation for small values of epsilon.

.. note::
The input ideal_operation is typically a QPROGRAM with a single
gate but could also correspond to a sequence of more gates.
This is possible as long as the unitary associated to the input
QPROGRAM, followed by a single final biased noise channel, is
physically implementable.
"""
circuit_copy = copy.deepcopy(ideal_operation)
converted_circ, _ = convert_to_mitiq(circuit_copy)
post_ops: List[List[Operation]]
qubits = converted_circ.all_qubits()

# Calculate coefficients in basis expansion in terms of eta and epsilon
a = 1 - epsilon
b = epsilon * (3 * eta + 1) / (3 * (eta + 1))
c = epsilon / (3 * (eta + 1))
alpha = (a**2 + a * b - 2 * c**2) / (
a**3
+ a**2 * b
- a * b**2
- 4 * a * c**2
- b**3
+ 4 * b * c**2
)
beta = (-a * b - b**2 + 2 * c**2) / (
a**3
+ a**2 * b
- a * b**2
- 4 * a * c**2
- b**3
+ 4 * b * c**2
)
gamma = -c / (a**2 + 2 * a * b + b**2 - 4 * c**2)
if len(qubits) == 1:
q = tuple(qubits)
alphas = [alpha] + 2 * [gamma] + [beta]
post_ops = [[]]  # for eta_1, we do nothing, rather than I
post_ops += [[P(q)] for P in [X, Y, Z]]  # 1Q Paulis

# The two-qubit case: linear combination of 2Q Paulis
elif len(qubits) == 2:
q0, q1 = qubits

alphas = (
[alpha**2]
+ 2 * [alpha * gamma]
+ [alpha * beta]
+ 2 * [alpha * gamma]
+ [alpha * beta]
+ 2 * [gamma**2]
+ [beta * gamma]
+ 2 * [gamma**2]
+ 3 * [beta * gamma]
+ [beta**2]
)
post_ops = [[]]  # for eta_1, we do nothing, rather than I x I
post_ops += [[P(q0)] for P in [X, Y, Z]]  # 1Q Paulis for q0
post_ops += [[P(q1)] for P in [X, Y, Z]]  # 1Q Paulis for q1
post_ops += [
[Pi(q0), Pj(q1)] for Pi in [X, Y, Z] for Pj in [X, Y, Z]
]  # 2Q Paulis

else:
raise ValueError(
"Can only represent single- and two-qubit gates."
"Consider pre-compiling your circuit."
)
# Basis of implementable operations as circuits.
imp_op_circuits = [
append_cirq_circuit_to_qprogram(ideal_operation, Circuit(op))
for op in post_ops
]

# Build basis expansion.
expansion = {NoisyOperation(c): a for c, a in zip(imp_op_circuits, alphas)}
return OperationRepresentation(ideal_operation, expansion)