Source code for mitiq.zne.scaling.parameter
# Copyright (C) Unitary Fund
#
# This source code is licensed under the GPL license (v3) found in the
# LICENSE file in the root directory of this source tree.
import copy
from typing import Callable, List, Optional, cast
import numpy as np
from cirq import (
Circuit,
CXPowGate,
CZPowGate,
EigenGate,
HPowGate,
MeasurementGate,
Moment,
Qid,
XPowGate,
YPowGate,
ZPowGate,
)
from mitiq import QPROGRAM
from mitiq.interface import accept_qprogram_and_validate
[docs]
class GateTypeException(Exception):
pass
def _get_base_gate(gate: EigenGate) -> EigenGate:
BASE_GATES = [ZPowGate, HPowGate, XPowGate, YPowGate, CXPowGate, CZPowGate]
for base_gate in BASE_GATES:
if isinstance(gate, base_gate):
return cast(EigenGate, base_gate)
raise GateTypeException(
"Must have circuit be made of rotation gates. "
"Your gate {} may not be supported".format(gate)
)
[docs]
class CircuitMismatchException(Exception):
pass
def _generate_parameter_calibration_circuit(
qubits: List[Qid], depth: int, gate: EigenGate
) -> Circuit:
"""Generates a circuit which should be the identity. Given a rotation
gate R(param), it applies R(2 * pi / depth) depth times, resulting
in R(2*pi). Requires that the gate is periodic in 2*pi.
Args:
qubits: A list of qubits.
depth: The length of the circuit to create.
gate: The base gate to apply several times, must be periodic
in 2*pi.
Returns:
A parameter calibration circuit that can be used for estimating
the parameter noise of the input gate.
"""
num_qubits = gate().num_qubits()
if num_qubits != len(qubits):
raise CircuitMismatchException(
"Number of qubits does not match domain size of gate."
)
return Circuit(
gate(exponent=2 * np.pi / depth).on(*qubits) for _ in range(depth)
)
[docs]
def compute_parameter_variance(
executor: Callable[..., float],
gate: EigenGate,
qubit: Qid,
depth: int = 100,
) -> float:
"""Given an executor and a gate, determines the effective variance in the
control parameter that can be used as the ``base_variance`` argument in
``mitiq.zne.scaling.scale_parameters``.
Note: Only works for one qubit gates for now.
Args:
executor: A function that takes in a quantum circuit and returns
an expectation value.
gate: The quantum gate that you wish to profile.
qubit: The index of the qubit you wish to profile.
depth: The number of operations you would like to use to profile
your gate.
Returns:
The estimated variance of the control parameter.
"""
base_gate = _get_base_gate(gate)
circuit = _generate_parameter_calibration_circuit(
[qubit], depth, base_gate
)
expectation = executor(circuit)
error_prob = (1 - np.power(2 * expectation - 1, 1 / depth)) / 2
variance = -0.5 * np.log(1 - 2 * error_prob)
return variance
[docs]
@accept_qprogram_and_validate
def scale_parameters(
circuit: QPROGRAM,
scale_factor: float,
base_variance: float,
seed: Optional[int] = None,
) -> Circuit:
"""Applies parameter-noise scaling to the input circuit,
assuming that each gate has the same base level of noise.
Args:
circuit: The circuit to scale as a QPROGRAM. All measurements
should be in the last moment of the circuit.
scale_factor: The amount to scale the base noise level by.
base_variance: The base level (variance) of parameter noise,
assumed to be the same for each gate of the circuit.
seed: Optional seed for random number generator.
Returns:
The parameter noise scaled circuit.
"""
final_moments = []
noise = (scale_factor - 1) * base_variance
rng = np.random.RandomState(seed)
for moment in circuit:
curr_moment = []
for op in moment.operations: # type: ignore
gate = copy.deepcopy(op.gate)
qubits = op.qubits
if isinstance(gate, MeasurementGate):
curr_moment.append(gate(*qubits))
else:
assert isinstance(gate, EigenGate)
base_gate = _get_base_gate(gate)
param = cast(float, gate.exponent) * np.pi
error = rng.normal(loc=0.0, scale=np.sqrt(noise))
new_param = param + error
curr_moment.append(
base_gate(exponent=new_param / np.pi)(*qubits)
)
final_moments.append(Moment(curr_moment))
return Circuit(final_moments)