diff --git a/docs/source/noise.rst b/docs/source/noise.rst index 03c0c440c..3d1303710 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -1335,3 +1335,271 @@ we should always measure ``1``. [1] [0] [1]] + + +A realistic simulation of a noisy quantum computer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The module noise.py facilitates the execution of a realistic simulation of a noisy quantum computer, +providing users with an accessible and robust tool. +The core functionality emulates Rigetti's quantum computers, +utilizing current calibration data from the company. +Additionally, users have the ability to explore the effects of varying noise intensity or specifying alternative noise parameters. + +In the following sections, we will delve into the workings of the simulator, +along with a comprehensive guide on its usage, accompanied by illustrative examples. + +How does the simulator work? +============================ + +The simulator employs Kraus operators as its foundation, as described previously. However, instead of incorporating Kraus operators directly into the gate (which poses challenges for parametric gates and does not account for effects on other qubits), we apply them to unity matrices following each gate. This serves as the fundamental concept behind the simulator. + +It is important to note that the majority of gates utilized in quantum programs are not physically realized within the quantum computer. Instead, they are implemented using a limited set of native gates. For instance, in Rigetti's Aspen-M-3 computer, the native gates include RX, RZ, CZ, CPHASE, and XY. To accurately simulate the real computer, the simulator, by default, translates the program into native gates and subsequently introduces noise into the native program, unless the user specifies otherwise. + +Depolarizing +------------ + +Upon the application of a gate to a qubit, some quantum information may be lost, resulting in the density matrix representing the qubit being mapped to a linear combination of itself and a maximally mixed state. For each qubit and gate, the fidelity is known, which is the probability of the qubit remaining consistent. + +The corresponding Kraus operators (with :math:`\lambda = 1 - \text{{fidelity}}`) are as follows: + +.. math:: + + K_0 = \sqrt{1- \frac{3 \lambda}{4}}I + K_1 = \sqrt{\frac{\lambda}{4}} \sigma_X + K_2 = \sqrt{\frac{\lambda}{4}} \sigma_Y + K_3 = \sqrt{\frac{\lambda}{4}} \sigma_Z + +For further information on quantum depolarizing channels, please refer to `this `_. + +After each gate, the depolarizing channel is applied to the target qubits. Although there is a probability, dictated by the fidelity, that nothing will occur, there remains the possibility that the qubit will transition into a maximally mixed state. + +Damping and dephasing +----------- + +Each gate possesses a specific duration time. Within this module, we assume that the duration of a two-qubit gate is 176 nanoseconds, while the duration of a one-qubit gate is 32 nanoseconds. + +During the application of a two-qubit gate, other gates are typically not applied to the additional qubits involved in the program. However, these qubits do experience a waiting period and are subject to damping and dephasing noise: damping and dephasing. The Kraus operators representing damping and dephasing are determined by the times T1 and T2; longer times indicate lower noise levels for the qubit. Consequently, after each two-qubit gate in the program, we introduce noise intensity gates (for damping and dephasing) to all other qubits in the program. + +One-qubit gates are presumed to be applied simultaneously, so by default, we do not apply damping and dephasing noise (representing waiting) to qubits not participating in the gate. This default setting can be altered as needed. + +Amplitude damping channels are imperfect identity maps with Kraus operators + +.. math:: + + K_1 = \begin{pmatrix} + 1 & 0 \\ + 0 & \sqrt{1-p} + \end{pmatrix} + + K_2 = \begin{pmatrix} + 0 & \sqrt{p} \\ + 0 & 0 + \end{pmatrix} + +Here, :math:`p` represents the probability that a qubit in the :math:`|1\rangle` state decays to the :math:`|0\rangle` state, and it is given by :math:`p = 1 - \exp(-T_{\text{{gate}}}/T_1)`. + +Dephasing is usually characterized through a qubit's T2 time. +For a single qubit the dephasing Kraus operators are + +.. math:: + + K_1(p) = \sqrt{1-p} I_2 + K_2(p) = \sqrt{p} \sigma_Z + +In this case, :math:`p = 0.5 * (1 - \exp(-(\frac{{\text{{gate_time}}}}{{T_2}} - \frac{{\text{{gate_time}}}}{{2 \cdot T_1}})))` denotes the probability that the qubit is dephased over the time interval of interest, :math:`I_2` is the 2x2-identity matrix, and :math:`\sigma_Z` is the Pauli-Z operator. + +These two sets of Kraus operators are combined to simulate damping after dephasing, resulting in damping and dephasing noise. + +Readout +------- + +The final category of noise, readout noise, is distinct from the previously discussed types. For each qubit, the readout fidelity is known, which is the probability that the read value is accurate. This probability is assumed to be identical for obtaining 0 when the qubit is in state 0 and acquiring 1 when the qubit is in state 1. + +The QVM is capable of handling a Pragma that incorporates readout noise. Therefore, the Pyquil module only needs to construct the appropriate Pragma according to the corresponding fidelities. + +Usage +===== + +Basic usage +----------- + +Utilizing the simulator is straightforward: First, create a quantum computer object and a program, and then provide them as parameters to the function. The output will be a noisy version of the program, simulating the real quantum computer specified as an input. The process can be summarized as follows: + +.. code:: python + + from pyquil.quil import Program + from pyquil.api import get_qc + from pyquil.noise import add_noise_to_program + + # create a Quantum computer object: + qc = get_qc('Aspen-M-3', as_qvm=True) + # create a program: + prog = Program() + # write your quantum program... + + # get the noisy version of your program, fitted to the quantum computer you chose: + noisy_prog = add_noise_to_program(qc=qc, p=prog) + noisy_prog = add_noise_to_program(qc=qc, p=prog, depolarizing=False, damping_after_dephasing_after_2q_gate=False, damping_after_dephasing_only_on_targets=True) + +Advanced usage +-------------- + +Certain users may not only wish to evaluate the behavior of their program on a real Rigetti quantum computer, but also explore the effects of varying noise levels or the resolution of specific noise types. Some users might want to define their own fidelities and damping and dephasing times, while others may be interested in investigating different gate types with new estimated noise parameters. For these users, the basic function can be further customized and adapted to accommodate a wide range of requirements. + +convert to native gates +^^^^^^^^^^^^^^^^^^^^^^^ + +As previously discussed, the function's default behavior is to convert the program into native gates before introducing noise. However, this approach may be problematic in two scenarios: + +1. If a user has already written their program using native gates and desires them to execute exactly as written (without altering the qubits or optimizing the program), the default compiler may modify their choices. +2. If a user wishes to explore the potential of non-native gates, accompanied by their own estimated noise parameters, the default function will convert these gates into native ones, defeating the purpose. + +In such instances, the solution is to instruct the function not to convert the program into native gates. This can be achieved with the following command: + +.. code:: python + + noisy_prog = add_noise_to_program(qc=qc, p=prog, convert_to_native=False) + +intensity parameter +^^^^^^^^^^^^^^^^^^^ + +You can assess the impact of noise on your program by adjusting the noise intensity. The default value is 1, which corresponds to the noise level in a real quantum computer. This parameter can be varied from 0 (no noise) to infinity (extremely noisy). For example, to reduce the noise intensity to 0.5, proceed as follows: + +.. code:: python + + noisy_prog = add_noise_to_program(qc=qc, p=prog, noise_intensity=0.5) + +Setting noise types +^^^^^^^^^^^^^^^^^^^ + +You can also configure the types of noise you want to introduce to the program. As previously mentioned, the default assumption is that two-qubit gates operate independently, while one-qubit gates run simultaneously. This assumption may not hold true for different types of programs, so users can modify the settings accordingly. If single-qubit gates do not operate simultaneously, you can add damping and dephasing noise to all other qubits in the program while the one-qubit gate is being applied, effectively simulating the waiting process for the remaining qubits: + +.. code:: python + + noisy_prog = add_noise_to_program(qc=qc, p=prog, damping_after_dephasing_after_1q_gate=True) + +Conversely, if your two-qubit gates operate simultaneously, you can eliminate the waiting noise following the execution of two-qubit gates: + +.. code:: python + + noisy_prog = add_noise_to_program(qc=qc, p=prog, damping_after_dephasing_after_2q_gate=False) + +Additionally, you have the option to exclude readout noise, depolarizing noise, or any combination thereof: + +.. code:: python + + noisy_prog = add_noise_to_program(qc=qc, p=prog, readout_noise=False, depolarizing=False) + +When depolarizing noise is enabled, the target qubit does not receive additional damping and dephasing "waiting" noise, as it is already incorporated within the depolarizing noise. If you choose to disable depolarizing noise, the target qubit, along with all other qubits in the program, will still be subject to damping and dephasing noise based on the parameters `damping_after_dephasing_after_1q_gate` and `damping_after_dephasing_after_2q_gate`. You can also configure the target qubit to receive damping and dephasing noise according to the gate time, as demonstrated below: + +.. code:: python + + noisy_prog = add_noise_to_program(qc=qc, p=prog, depolarizing=False, damping_after_dephasing_after_2q_gate=False, damping_after_dephasing_only_on_targets=True) + +Calibrations +^^^^^^^^^^^^ + +When selecting a quantum computer object and providing it to the function, its calibrations are stored in a class called ``Calibrations``. Each noise type is stored as a dictionary that maps a qubit to its fidelity or time (depending on the noise type). The dictionaries include: ``T1``, ``T2``, ``readout_fidelity``, and ``fidelities`` - which is a nested dictionary for each gate. The class also contains a set of two-qubit gates present in the computer. + +In a basic usage of the function, the quantum computer's name is provided, and the appropriate calibrations for the specified computer are retrieved from the web. (A future improvement could involve obtaining this information directly, without relying on a specific URL and JSON format). + +Often, you may want to run a loop to test your program with various modifications on the same computer. In such cases, constructing the ``Calibrations`` instance from the web can be inefficient. A better practice would be to create the instance explicitly once and pass it as a parameter to the function: + +.. code:: python + + cal = Calibrations('Aspen-M-3') + noisy_prog = add_noise_to_program(qc, prog, calibrations=cal) + +If you want to define custom noise parameters or gate types, you should create an instance of ``Calibrations``, populate it with your own data, and then provide it as a parameter to the function: + +.. code:: python + + cal = Calibrations() + # Populate the calibrations with your own data + # ... + noisy_prog = add_noise_to_program(qc, prog, calibrations=cal) + +If you want to define custom noise parameters or gate types, you should create an instance of `Calibrations`, populate it with your own data, and then provide it as a parameter to the function: + +.. code:: python + + from pyquil.noise import Calibrations + + cal = Calibrations() + cal.T1 = {0: 1.6e-5, 1: 5.1e-6} + cal.T2 = {0: 7.6e-6, 1: 9.3e-6} + cal.two_q_gates = set(['XY']) + cal.readout_fidelity = {0: 0.994, 1: 0.984} + # attention: in "fidelities" (and only there), the qubit indexes are strings, not integers. + cal.fidelities["1Q_gate"] = {'0': 0.999, '1': 0.998} + cal.fidelities["XY"] = {'0-1': 0.977} + + noisy_prog = add_noise_to_program(qc, prog, calibrations=cal) + +Example: GHZ +------------ + +In a GHZ state, three qubits are entangled, and the measurement results should ideally be half '000' and half '111'. The Greenberger-Horne-Zeilinger (GHZ) experiment is an important demonstration of quantum entanglement involving three or more qubits. This entangled state is highly sensitive to noise and damping and dephasing, making it an ideal testbed for evaluating the performance of quantum computers and error mitigation techniques. + +The GHZ experiment can be implemented using the following sequence of quantum gates: + +1. Apply a Hadamard gate (H) to the first qubit, which puts it in an equal superposition of '0' and '1' states. +2. Apply a CNOT gate between the first and second qubits, creating entanglement between them. +3. Apply another CNOT gate between the second and third qubits, resulting in the entanglement of all three qubits. + +After performing these operations, the resulting GHZ state is :math:`(\ket{000} + \ket{111})/\sqrt{2}`. Due to noise, the actual program will yield other results as well. In this example, we examine the results for a noise-free program, as well as the outcomes for a noisy program that simulates a run on Rigetti's Aspen-M-2 computer. Furthermore, we investigate what would have occurred if all noise levels were reduced to half of their actual values. By examining the outcomes of the GHZ experiment in the presence of different noise levels, researchers can gain insights into the behavior of quantum computers under various conditions and investigate strategies for improving their performance. + +.. code:: python + + from pyquil.quil import Program + from pyquil.gates import H, MEASURE, CNOT + from pyquil.api import get_qc + from pyquil.noise import add_noise_to_program, Calibrations + from pyquil.quantum_processor.qcs import get_qcs_quantum_processor + import matplotlib.pyplot as plt + + def ghz(qvm, isa, qubits, numshots, noise=False, cal=None, intensity=1.0): + p = Program() + p.declare("ro", "BIT", 3) + p += H(qubits[0]) + p += CNOT(qubits[0], qubits[1]) + p += CNOT(qubits[1], qubits[2]) + p += MEASURE(qubits[0], ("ro", 0)) + p += MEASURE(qubits[1], ("ro", 1)) + p += MEASURE(qubits[2], ("ro", 2)) + p.wrap_in_numshots_loop(numshots) + p = qvm.compiler.quil_to_native_quil(p) + if noise: + if cal is not None: + p = add_noise_to_program(isa, p, calibrations=cal, noise_intensity=intensity) + else: + p = add_noise_to_program(isa, p, noise_intensity=intensity) + return p + + def run_experiment(qpu_name, qubits, numshots): + qvm = get_qc(qpu_name, as_qvm=True, execution_timeout=1000) + qcs_qpu = get_qcs_quantum_processor(qpu_name) + isa = qcs_qpu._isa + cal = Calibrations(qcs_qpu) + no_noise = qvm.run(ghz(qvm,isa,qubits,numshots, False)).readout_data.get("ro") + half_noise = qvm.run(ghz(qvm,isa,qubits,numshots, True, cal, intensity=0.5)).readout_data.get("ro") + noisy = qvm.run(ghz(qvm,isa,qubits,numshots, True, cal)).readout_data.get("ro") + return no_noise, half_noise, noisy + + def plot_results(results, label): + histogram={} + for i in results: + code=("00"+str(int(float(i[2])*100+float(i[1])*10+float(i[0]))))[-3:] + histogram[code]=histogram.get(code,0)+1 + histogram=dict(sorted(histogram.items())) + plt.bar(*zip(*histogram.items()), label=label) + plt.xlabel('State') + plt.ylabel('Counts') + plt.legend() + plt.show() + + no_noise, half_noise, noisy = run_experiment('Aspen-M-3',[0,1,2], 1000) + plot_results(no_noise, 'no noise') + plot_results(half_noise, 'half noise') + plot_results(noisy, 'noisy') \ No newline at end of file diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index ba22e14a8..ef924bb2f 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -52,7 +52,6 @@ def _extract_memory_regions( ro_sources: Dict[MemoryReference, str], buffers: Dict[str, np.ndarray], ) -> Dict[str, np.ndarray]: - # hack to extract num_shots indirectly from the shape of the returned data first, *rest = buffers.values() num_shots = first.shape[0] diff --git a/pyquil/experiment/_result.py b/pyquil/experiment/_result.py index ce769d64a..09eaa84a7 100644 --- a/pyquil/experiment/_result.py +++ b/pyquil/experiment/_result.py @@ -62,7 +62,6 @@ def __init__( calibration_counts: Optional[int] = None, additional_results: Optional[List["ExperimentResult"]] = None, ): - object.__setattr__(self, "setting", setting) object.__setattr__(self, "expectation", expectation) object.__setattr__(self, "total_counts", total_counts) diff --git a/pyquil/noise.py b/pyquil/noise.py index 5521214e4..1cce556d9 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -18,20 +18,23 @@ """ import sys from collections import namedtuple -from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union, cast +from typing import Dict, List, Sequence, Optional, Any, Tuple, Set, Iterable, TYPE_CHECKING, Union, cast import numpy as np from pyquil.external.rpcq import CompilerISA from pyquil.gates import I, RX, MEASURE from pyquil.noise_gates import _get_qvm_noise_supported_gates -from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator -from pyquil.quilbase import Pragma, Gate, Declare +from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit +from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits +from qcs_api_client.models import InstructionSetArchitecture + if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + INFINITY = float("inf") "Used for infinite coherence times." @@ -47,7 +50,7 @@ class KrausModel(_KrausModel): :ivar Sequence[int] targets: The target qubit ids. :ivar Sequence[np.array] kraus_ops: The Kraus operators (must be square complex numpy arrays). :ivar float fidelity: The average gate fidelity associated with the Kraus map relative to the - ideal operation. + ideal operation. """ @staticmethod @@ -56,8 +59,8 @@ def unpack_kraus_matrix(m: Union[List[Any], np.ndarray]) -> np.ndarray: Helper to optionally unpack a JSON compatible representation of a complex Kraus matrix. :param m: The representation of a Kraus operator. Either a complex - square matrix (as numpy array or nested lists) or a JSON-able pair of real matrices - (as nested lists) representing the element-wise real and imaginary part of m. + square matrix (as numpy array or nested lists) or a JSON-able pair of real matrices + (as nested lists) representing the element-wise real and imaginary part of m. :return: A complex square numpy array representing the Kraus operator. """ m = np.asarray(m, dtype=complex) @@ -75,18 +78,18 @@ def to_dict(self) -> Dict[str, Any]: For example:: - { - "gate": "RX", - "params": np.pi, - "targets": [0], - "kraus_ops": [ # In this example single Kraus op = ideal RX(pi) gate - [[[0, 0], # element-wise real part of matrix - [0, 0]], - [[0, -1], # element-wise imaginary part of matrix - [-1, 0]]] - ], - "fidelity": 1.0 - } + { + "gate": "RX", + "params": np.pi, + "targets": [0], + "kraus_ops": [ # In this example single Kraus op = ideal RX(pi) gate + [[[0, 0], # element-wise real part of matrix + [0, 0]], + [[0, -1], # element-wise imaginary part of matrix + [-1, 0]]] + ], + "fidelity": 1.0 + } :return: A JSON compatible dictionary representation. :rtype: Dict[str,Any] @@ -101,7 +104,7 @@ def from_dict(d: Dict[str, Any]) -> "KrausModel": Recreate a KrausModel from the dictionary representation. :param d: The dictionary representing the KrausModel. See `to_dict` for an - example. + example. :return: The deserialized KrausModel. """ kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] @@ -123,7 +126,7 @@ class NoiseModel(_NoiseModel): :ivar Sequence[KrausModel] gates: The tomographic estimates of all gates. :ivar Dict[int,np.array] assignment_probs: The single qubit readout assignment - probability matrices keyed by qubit id. + probability matrices keyed by qubit id. """ def to_dict(self) -> Dict[str, Any]: @@ -132,17 +135,17 @@ def to_dict(self) -> Dict[str, Any]: For example:: - { - "gates": [ - # list of embedded dictionary representations of KrausModels here [...] - ] - "assignment_probs": { - "0": [[.8, .1], - [.2, .9]], - "1": [[.9, .4], - [.1, .6]], + { + "gates": [ + # list of embedded dictionary representations of KrausModels here [...] + ] + "assignment_probs": { + "0": [[.8, .1], + [.2, .9]], + "1": [[.9, .4], + [.1, .6]], + } } - } :return: A dictionary representation of self. """ @@ -235,15 +238,15 @@ def pauli_kraus_map(probabilities: Sequence[float]) -> List[np.ndarray]: Generate the Kraus operators corresponding to a pauli channel. :params probabilities: The 4^num_qubits list of probabilities specifying the - desired pauli channel. There should be either 4 or 16 probabilities specified in the - order I, X, Y, Z for 1 qubit or II, IX, IY, IZ, XI, XX, XY, etc for 2 qubits. + desired pauli channel. There should be either 4 or 16 probabilities specified in the + order I, X, Y, Z for 1 qubit or II, IX, IY, IZ, XI, XX, XY, etc for 2 qubits. - For example:: + For example:: - The d-dimensional depolarizing channel \Delta parameterized as - \Delta(\rho) = p \rho + [(1-p)/d] I - is specified by the list of probabilities - [p + (1-p)/d, (1-p)/d, (1-p)/d), ... , (1-p)/d)] + The d-dimensional depolarizing channel \Delta parameterized as + \Delta(\rho) = p \rho + [(1-p)/d] I + is specified by the list of probabilities + [p + (1-p)/d, (1-p)/d, (1-p)/d), ... , (1-p)/d)] :return: A list of the 4^num_qubits Kraus operators that parametrize the map. """ @@ -338,14 +341,13 @@ def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.n damping = damping_kraus_map(p=1 - np.exp(-float(gate_time) / float(T1))) else: damping = [np.eye(2)] - if T2 != INFINITY: gamma_phi = float(gate_time) / float(T2) if T1 != INFINITY: if T2 > 2 * T1: - raise ValueError("T2 is upper bounded by 2 * T1") + T2 = 2 * T1 + gamma_phi = float(gate_time) / float(T2) gamma_phi -= float(gate_time) / float(2 * T1) - dephasing = dephasing_kraus_map(p=0.5 * (1 - np.exp(-gamma_phi))) else: dephasing = [np.eye(2)] @@ -370,7 +372,7 @@ def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> Tup :param gate_name: The Quil gate name :param params: The gate parameters. :return: A tuple (matrix, noisy_name) with the representation of the ideal gate matrix - and a proposed name for the noisy version. + and a proposed name for the noisy version. """ params = tuple(params) if gate_name == "I": @@ -432,15 +434,15 @@ def _decoherence_noise_model( :param gates: The gates to provide the noise model for. :param T1: The T1 amplitude damping time either globally or in a - dictionary indexed by qubit id. By default, this is 30 us. + dictionary indexed by qubit id. By default, this is 30 us. :param T2: The T2 dephasing time either globally or in a - dictionary indexed by qubit id. By default, this is also 30 us. + dictionary indexed by qubit id. By default, this is also 30 us. :param gate_time_1q: The duration of the one-qubit gates, namely RX(+pi/2) and RX(-pi/2). - By default, this is 50 ns. + By default, this is 50 ns. :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - By default, this is 150 ns. + By default, this is 150 ns. :param ro_fidelity: The readout assignment fidelity - :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. + :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. :return: A NoiseModel with the appropriate Kraus operators defined. """ all_qubits = set(sum(([t.index for t in g.qubits] for g in gates), [])) @@ -518,11 +520,11 @@ def _noise_model_program_header(noise_model: NoiseModel) -> "Program": Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates. The program header consists of 3 sections: - - The ``DEFGATE`` statements that define the meaning of the newly introduced "noisy" gate - names. - - The ``PRAGMA ADD-KRAUS`` statements to overload these noisy gates on specific qubit - targets with their noisy implementation. - - THe ``PRAGMA READOUT-POVM`` statements that define the noisy readout per qubit. + - The ``DEFGATE`` statements that define the meaning of the newly introduced "noisy" gate + names. + - The ``PRAGMA ADD-KRAUS`` statements to overload these noisy gates on specific qubit + targets with their noisy implementation. + - THe ``PRAGMA READOUT-POVM`` statements that define the noisy readout per qubit. :param noise_model: The assumed noise model. :return: A quil Program with the noise pragmas. @@ -532,7 +534,6 @@ def _noise_model_program_header(noise_model: NoiseModel) -> "Program": p = Program() defgates: Set[str] = set() for k in noise_model.gates: - # obtain ideal gate matrix and new, noisy name by looking it up in the NOISY_GATES dict try: ideal_gate, new_name = get_noisy_gate(k.gate, tuple(k.params)) @@ -563,9 +564,9 @@ def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": :param prog: A Quil Program object. :param noise_model: A NoiseModel, either generated from an ISA or - from a simple decoherence model. + from a simple decoherence model. :return: A new program translated to a noisy gateset and with noisy readout as described by the - noisemodel. + noisemodel. """ new_prog = _noise_model_program_header(noise_model) for i in prog: @@ -613,15 +614,15 @@ def add_decoherence_noise( :param prog: A pyquil program consisting of I, RZ, CZ, and RX(+-pi/2) instructions :param T1: The T1 amplitude damping time either globally or in a - dictionary indexed by qubit id. By default, this is 30 us. + dictionary indexed by qubit id. By default, this is 30 us. :param T2: The T2 dephasing time either globally or in a - dictionary indexed by qubit id. By default, this is also 30 us. + dictionary indexed by qubit id. By default, this is also 30 us. :param gate_time_1q: The duration of the one-qubit gates, namely RX(+pi/2) and RX(-pi/2). - By default, this is 50 ns. + By default, this is 50 ns. :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - By default, this is 150 ns. + By default, this is 150 ns. :param ro_fidelity: The readout assignment fidelity - :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. + :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. :return: A new program with noisy operators. """ gates = _get_program_gates(prog) @@ -644,8 +645,8 @@ def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray: This should not allocate much memory if ``p`` is already in ``C``-contiguous order (row-major). :param p: An array that enumerates bitstring probabilities. When flattened out - ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must therefore be a - power of 2. + ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must therefore be a + power of 2. :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. """ p = np.asarray(p, order="C") @@ -658,9 +659,9 @@ def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray: Given an array of single shot results estimate the probability distribution over all bitstrings. :param results: A 2d array where the outer axis iterates over shots - and the inner axis over bits. + and the inner axis over bits. :return: An array with as many axes as there are qubit and normalized such that it sums to one. - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. """ nshots, nq = np.shape(results) outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) @@ -678,15 +679,15 @@ def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarr the inner axis of results) apply local 2x2 matrices to each bit index. :param p: An array that enumerates a function indexed by - bitstrings:: + bitstrings:: - f(ijk...) = p[i,j,k,...] + f(ijk...) = p[i,j,k,...] :param ts: A sequence of 2x2 transform-matrices, one for each bit. :return: ``p_transformed`` an array with as many dimensions as there are bits with the result of - contracting p along each axis by the corresponding bit transformation:: + contracting p along each axis by the corresponding bit transformation:: - p_transformed[ijk...] = f'(ijk...) = sum_lmn... ts[0][il] ts[1][jm] ts[2][kn] f(lmn...) + p_transformed[ijk...] = f'(ijk...) = sum_lmn... ts[0][il] ts[1][jm] ts[2][kn] f(lmn...) """ p_corrected = _bitstring_probs_by_qubit(p) nq = p_corrected.ndim @@ -710,18 +711,18 @@ def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.nda ordered like the inner axis of results) compute the corrupted probabilities. :param p: An array that enumerates bitstring probabilities. When - flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must - therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must + therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: + per qubit. Each assignment probability matrix is expected to be of the form:: - [[p00 p01] - [p10 p11]] + [[p00 p01] + [p10 p11]] :return: ``p_corrected`` an array with as many dimensions as there are qubits that contains - the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. """ return _apply_local_transforms(p, assignment_probabilities) @@ -733,18 +734,18 @@ def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.nda compute the corrected probabilities. :param p: An array that enumerates bitstring probabilities. When - flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must - therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must + therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: + per qubit. Each assignment probability matrix is expected to be of the form:: - [[p00 p01] - [p10 p11]] + [[p00 p01] + [p10 p11]] :return: ``p_corrected`` an array with as many dimensions as there are qubits that contains - the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. """ return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) # type: ignore @@ -754,14 +755,14 @@ def bitstring_probs_to_z_moments(p: np.ndarray) -> np.ndarray: Convert between bitstring probabilities and joint Z moment expectations. :param p: An array that enumerates bitstring probabilities. When - flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must - therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must + therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. :return: ``z_moments``, an np.array with one length-2 axis per qubit which contains the - expectations of all monomials in ``{I, Z_0, Z_1, ..., Z_{n-1}}``. The expectations of each - monomial can be accessed via:: + expectations of all monomials in ``{I, Z_0, Z_1, ..., Z_{n-1}}``. The expectations of each + monomial can be accessed via:: - = z_moments[j_0,j_1,...,j_m] + = z_moments[j_0,j_1,...,j_m] """ zmat = np.array([[1, 1], [1, -1]]) return _apply_local_transforms(p, (zmat for _ in range(p.ndim))) @@ -777,14 +778,14 @@ def estimate_assignment_probs( Estimate the readout assignment probabilities for a given qubit ``q``. The returned matrix is of the form:: - [[p00 p01] - [p10 p11]] + [[p00 p01] + [p10 p11]] :param q: The index of the qubit. :param trials: The number of samples for each state preparation. :param qc: The quantum computer to sample from. :param p0: A header program to prepend to the state preparation programs. Will not be compiled by quilc, so it must - be native Quil. + be native Quil. :return: The assignment probability matrix """ from pyquil.quil import Program @@ -822,3 +823,553 @@ def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> List[List[int]]: bitstrings = result.readout_data.get("ro") assert bitstrings is not None return cast(List[List[int]], bitstrings.tolist()) + + +single_qubit_noise_1Q_gate_name = "Damping_after_dephasing_for_1Q_gate" +single_qubit_noise_2Q_gate_name = "Damping_after_dephasing_for_2Q_gate" +Depolarizing_1Q_gate = "1Q_gate" +Depolarizing_pre_name = "Depolarizing_" +No_Depolarizing = "No_Depolarizing" + + +def change_times_by_ratio(times: Dict[Any, float], ratio: float): + """ + change the times in the dict `times` by a given `ratio`. + larger ratio will make the times shorter (i.e. more noise) + """ + for key in times.keys(): + times[key] = times[key] / (ratio + 1e-10) # no zerp devision + return times + + +def get_t1s(isa: InstructionSetArchitecture) -> Dict[int, float]: + benchmarks = isa.benchmarks + operations = next(operation for operation in benchmarks if operation.name == "FreeInversionRecovery") + t1s = {site.node_ids[0]: site.characteristics[0].value for site in operations.sites} + return t1s + + +def get_t2s(isa: InstructionSetArchitecture) -> Dict[int, float]: + benchmarks = isa.benchmarks + operations = next(operation for operation in benchmarks if operation.name == "FreeInductionDecay") + t2s = {site.node_ids[0]: site.characteristics[0].value for site in operations.sites} + return t2s + + +def get_1q_fidelities(isa: InstructionSetArchitecture) -> Dict[int, float]: + benchmarks = isa.benchmarks + operations = next(operation for operation in benchmarks if operation.name == "randomized_benchmark_1q") + fidelities = {site.node_ids[0]: site.characteristics[0].value for site in operations.sites} + return fidelities + + +def get_readout_fidelities(isa: InstructionSetArchitecture) -> Dict[int, float]: + instructions = isa.instructions + sites = [operation for operation in instructions if operation.name == "MEASURE"][0].sites + fro = {site.node_ids[0]: site.characteristics[0].value for site in sites} + return fro + + +class Calibrations: + """ + encapsulate the calibration data for Aspen-M-2 or Aspen-M-3 machine. + + contains: + T1 + T2 + fidelities: 1Q_gate, and a dict for each 2Q gate + readout + + args: qc (QuantumComputer, optional): a Quantum Computer (Aspen-M-2 or Aspen-M-3). + Defaults to None, where the user can define their own calibration data. + + Notice: this class heavily relies on the specific way on which the lattices are written. + this may change in time, and require changes in the class. + """ + + def __init__(self, isa: Optional[InstructionSetArchitecture] = None, calibrations=None) -> None: + self.fidelities = {} + self.readout_fidelity = {} + self.T2 = {} + self.T1 = {} + self.two_q_gates = set() + + if calibrations is not None: + self.T1 = calibrations.T1.copy() + self.T2 = calibrations.T2.copy() + self.readout_fidelity = calibrations.readout_fidelity.copy() + self.fidelities = {} + for key, val in calibrations.fidelities.items(): + self.fidelities[key] = val.copy() + self.two_q_gates = calibrations.two_q_gates + return + + if isa is None: + return # user can set their own values + + else: + self.T1 = get_t1s(isa) + self.T2 = get_t2s(isa) + self.fidelities[Depolarizing_1Q_gate] = get_1q_fidelities(isa) + self.readout_fidelity = get_readout_fidelities(isa) + self._create_2q_dicts(isa) + + def _create_2q_dicts(self, isa: InstructionSetArchitecture) -> None: + instructions = isa.instructions + self.two_q_gates = { + operation.name + for operation in instructions + if (operation.sites.__len__() > 0 and operation.sites[0].node_ids.__len__() == 2) + } + for gate in self.two_q_gates: + operations = [operation.sites for operation in instructions if operation.name == gate][0] + self.fidelities[gate] = { + str(str(operation.node_ids[0]) + "-" + str(operation.node_ids[1])): operation.characteristics[0].value + for operation in operations + } + + def change_noise_intensity(self, intensity: float) -> None: + self.T1 = change_times_by_ratio(self.T1, intensity) + self.T2 = change_times_by_ratio(self.T2, intensity) + self.readout_fidelity = self._change_fidelity_by_noise_intensity(self.readout_fidelity, intensity) + for name, dic in self.fidelities.items(): + self.fidelities[name] = self._change_fidelity_by_noise_intensity(dic, intensity) + + def _get_qc_2q_gates(self, isa_2q: Dict[str, Dict[str, List]]) -> Set[str]: + gates = set() + for pair_val in isa_2q.values(): + if "type" in pair_val.keys(): + for gate in pair_val["type"]: + gates.add(gate) + return gates + + def _change_fidelity_by_noise_intensity(self, fidelity: Dict[Any, float], intensity: float) -> Dict[Any, float]: + """ + change the fidelities in the dict `fidelity` + so that the noise (1-fidelity) will change by a given `intensity`. + the fidelity will always stay in range [0.0, 1.0]. + """ + for key in fidelity.keys(): + fidelity[key] = max(0.0, min(1.0, 1 - ((1 - fidelity[key]) * intensity))) + return fidelity + + +def create_damping_after_dephasing_kraus_maps( + gates: Sequence[Gate], + T1: Dict[int, float], + T2: Dict[int, float], + gate_time: float, +) -> List[Dict[str, Any]]: + """ + Create A List with the appropriate Kraus operators defined. + + :param gates: The gates to provide the noise model for. + :param T1: The T1 amplitude damping time dictionary indexed by qubit id. + :param T2: The T2 dephasing time dictionary indexed by qubit id. + :param gate_time: The duration of a gate + :return: A List with the appropriate Kraus operators defined. + """ + all_qubits = set(sum(([t.index for t in g.qubits] for g in gates), [])) + + matrices = {q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time) for q in all_qubits} + kraus_maps = [] + for g in gates: + targets = tuple(t.index for t in g.qubits) + kraus_ops = matrices[targets[0]] + + kraus_maps.append({"gate": g.name, "params": tuple(g.params), "targets": targets, "kraus_ops": kraus_ops}) + return kraus_maps + + +def create_kraus_maps(prog: "Program", gate_name: str, gate_time: float, T1: Dict[int, float], T2: Dict[int, float]): + gates = [i for i in _get_program_gates(prog) if i.name == gate_name] + kraus_maps = create_damping_after_dephasing_kraus_maps( + gates, + T1=T1, + T2=T2, + # for future improvement: use the specific gate time if known + gate_time=gate_time, + ) + # add Kraus definition pragmas + for k in kraus_maps: + prog.define_noisy_gate(k["gate"], k["targets"], k["kraus_ops"]) + return prog + + +def add_single_qubit_noise( + prog: "Program", + T1: Dict[int, float], + T2: Dict[int, float], + gate_time_1q: float = 32e-9, + gate_time_2q: float = 176e-09, +) -> "Program": + """ + Applies the model on the different kinds of I. + :param prog: The program including I's that are not noisy yet. + :param T1: The T1 amplitude damping time dictionary indexed by qubit id. + :param T2: The T2 dephasing time dictionary indexed by qubit id. + :param gate_time_1q: The duration of the one-qubit gates. By default, this is 32 ns. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. By default, this is 176 ns. + :return: A new program with noisy operators. + """ + prog = create_kraus_maps(prog, single_qubit_noise_2Q_gate_name, gate_time_2q, T1, T2) + prog = create_kraus_maps(prog, single_qubit_noise_1Q_gate_name, gate_time_1q, T1, T2) + return prog + + +def add_readout_noise( + prog: "Program", + ro_fidelity: Dict[int, float], +) -> "Program": + """ + adds readout noise to the program. + :param prog: The program without readout noise yet. + :param ro_fidelity: The readout assignment fidelity dictionary indexed by qubit id. + """ + # define readout fidelity dict for all qubits in the program: + ro_fidelity_prog_qubits = {q: ro_fidelity[q] for q in prog.get_qubits()} + # define a noise model with readout noise + assignment_probs = {} + for q, f_ro in ro_fidelity_prog_qubits.items(): + assignment_probs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) + # add readout noise pragmas + for q, ap in assignment_probs.items(): + prog.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) + return prog + + +def create_depolarizing_kraus_maps( + gates: Sequence[Gate], + fidelity: Dict[str, float], +) -> List[Dict[str, Any]]: + """ + creates a noise model for depolarizing. + + :param gates: depolarizing gates of a certain type. + :param fidelity: a mapping between qubits (one or a pair) to the fidelity. + """ + + num_qubits = 1 + all_qubits = [] + for g in gates: + qubits = [t.index for t in g.qubits] + if len(qubits) == 1: + all_qubits.append(str(qubits[0])) + elif len(qubits) == 2: + num_qubits = 2 + qubits.sort(key=lambda x: int(x)) + all_qubits.append(str(qubits[0]) + "-" + str(qubits[1])) + all_qubits = set(all_qubits) + + kraus_matrices = {q: depolarizing_kraus(num_qubits, fidelity.get(q, 1.0)) for q in all_qubits} + kraus_maps = [] + for g in gates: + targets = tuple(t.index for t in g.qubits) + qubits = targets + if num_qubits == 1: + qubits = str(qubits[0]) + if num_qubits > 1: + qubits = sorted(list(targets)) + qubits = str(qubits[0]) + "-" + str(qubits[1]) + kraus_ops = kraus_matrices[qubits] + + kraus_maps.append({"gate": g.name, "params": tuple(g.params), "targets": targets, "kraus_ops": kraus_ops}) + return kraus_maps + + +def depolarizing_kraus(num_qubits: int, p: float = 0.95) -> List[np.ndarray]: + """ + Generate the Kraus operators corresponding to a given unitary + single qubit gate followed by a depolarizing noise channel. + + :params float num_qubits: either 1 or 2 qubit channel supported + :params float p: parameter in depolarizing channel as defined by: p $\rho$ + (1-p)/d I + :return: A list, eg. [k0, k1, k2, k3], of the Kraus operators that parametrize the map. + :rtype: list + """ + num_of_operators = 4**num_qubits + probabilities = [p + (1.0 - p) / num_of_operators] + probabilities += [(1.0 - p) / num_of_operators] * (num_of_operators - 1) + return pauli_kraus_map(probabilities) + + +def add_depolarizing_noise(prog: "Program", fidelities: Dict[str, Dict[str, float]]) -> "Program": + """ + add depolarizing noise to the program. + + :param prog: the program. + :param fidelities: dictionary of fidelities by name. each fidelity is a dictionary + mapping a qubit or a pair of qubits to their fidelity. + :return: the changed program + """ + + for name in fidelities.keys(): + gates = [i for i in _get_program_gates(prog) if i.name == Depolarizing_pre_name + name] + kraus_maps = create_depolarizing_kraus_maps(gates, fidelities[name]) + for k in kraus_maps: + prog.define_noisy_gate(k["gate"], k["targets"], k["kraus_ops"]) + return prog + + +def add_delay_maps( + prog: "Program", delay_gates: Dict[str, float], T1: Dict[int, float], T2: Dict[int, float] +) -> "Program": + """ + Add kraus maps for a `DELAY` instruction, + that was converted already into `noisy-I` gate. + + :param prog: the program to add the maps to. + :param delay_gates: a Dictionary with the gates name and duration. + :param T1: Dictionary with T1 times. + :param T2: Dictionary with T2 times. + """ + if len(delay_gates.items()) > 0: + for name, duration in delay_gates.items(): + prog = create_kraus_maps(prog, name, duration, T1, T2) + return prog + + +def def_gate_to_prog(name: str, dim: int, new_p: "Program"): + """ + defines a gate wit name `name` for `new_p`, and returns the gate. + the gate is an identity matrix, in dimension `dim`. + + :param name: gate name. + :param dim: matrix dimension. + :param new_p: the program to add the definition to. + :return: the new gate. + """ + dg = DefGate(name, np.eye(dim)) + new_p += dg + return dg.get_constructor() + + +def define_noisy_gates_on_new_program( + new_p: "Program", + prog: "Program", + two_q_gates: Set, + depolarizing: bool, + damping_after_dephasing_after_1q_gate: bool, + damping_after_dephasing_after_2q_gate: bool, +) -> Tuple["Program", Dict]: + """ + defines noisy gates for the new program `new_p`, + and returns a Dictionary with the new noise gates. + the function defines noise gates only for types of noises that are given as parameters, + and only for gates that appear in the program `prog`. + + :param new_p: new program, to add definitions on. + :param prog: old program, to find which noise gates we need. + :param two_q_gates: Set of 2-Q gates that exist in the QC calibrations. + :param depolarizing: add depolarizing noise. + :param damping_after_dephasing_after_1q_gate: add damping after dephasing to all qubits after every one-qubit gate. + :param damping_after_dephasing_after_2q_gate: add damping after dephasing to all qubits after every two-qubit gate. + + :return: `noise_gates`, a Dictionary with the new noise gates. + """ + + # check which noise types are needed the program: + depolarizing_1q = no_depolarizing = dec_1q = dec_2q = False + depolarizing_2q = {gate: False for gate in two_q_gates} + noise_gates = {} + for i in prog: + if (damping_after_dephasing_after_1q_gate or damping_after_dephasing_after_2q_gate) and ( + (isinstance(i, Pragma) and i.command == "DELAY") or isinstance(i, DelayQubits) + ): + duration = i.duration if isinstance(i, DelayQubits) else i.freeform_string + name = "Noisy-DELAY-" + duration + if name not in noise_gates.keys(): + noise_gates[name] = def_gate_to_prog(name, 2, new_p) + if isinstance(i, Gate): + if len(i.qubits) == 1: + if depolarizing: + depolarizing_1q = True + if damping_after_dephasing_after_1q_gate: + dec_1q = True + elif len(i.qubits) == 2: + if damping_after_dephasing_after_2q_gate: + dec_2q = True + if depolarizing: + if i.name in two_q_gates: + depolarizing_2q[i.name] = True + else: + no_depolarizing = True + + # add relevant definitions and noise gates: + if depolarizing_1q: + noise_gates[Depolarizing_1Q_gate] = def_gate_to_prog(Depolarizing_pre_name + Depolarizing_1Q_gate, 2, new_p) + for gate, val in depolarizing_2q.items(): + if val: + noise_gates[gate] = def_gate_to_prog(Depolarizing_pre_name + gate, 4, new_p) + if dec_2q: + noise_gates[single_qubit_noise_2Q_gate_name] = def_gate_to_prog(single_qubit_noise_2Q_gate_name, 2, new_p) + if dec_1q: + noise_gates[single_qubit_noise_1Q_gate_name] = def_gate_to_prog(single_qubit_noise_1Q_gate_name, 2, new_p) + if no_depolarizing: + noise_gates[No_Depolarizing] = def_gate_to_prog(No_Depolarizing, 4, new_p) + return new_p, noise_gates + + +def add_noisy_gates_to_program( + new_p: "Program", + prog: "Program", + noise_gates: Dict, + damping_after_dephasing_after_2q_gate: bool, + damping_after_dephasing_after_1q_gate: bool, + depolarizing: bool, + damping_after_dephasing_only_on_targets: bool, +): + """ + :param new_p: new program to add noisy gates on + :param prog: old program, from which we build the new one + :param noise_gates: a Dictionary with the new noise gates. + :param damping_after_dephasing_after_2q_gate: add damping after dephasing to all qubits after every two-qubit gate. + :param damping_after_dephasing_after_1q_gate: add damping after dephasing to all qubits after every one-qubit gate. + :param depolarizing: add depolarizing noise. + :param damping_after_dephasing_only_on_targets: add damping after dephasing only on gate targets. + + :return: new_p + :return: delay_gates: noisy Delay gates (if given in `prog`) + """ + qubits = prog.get_qubits() + delay_gates = {} + for i in prog: + if (damping_after_dephasing_after_1q_gate or damping_after_dephasing_after_2q_gate) and ( + (isinstance(i, Pragma) and i.command == "DELAY") or isinstance(i, DelayQubits) + ): + duration = i.duration if isinstance(i, DelayQubits) else i.freeform_string + name = "Noisy-DELAY-" + duration + targets = i.qubits if isinstance(i, DelayQubits) else i.args + for q in targets: + new_p += noise_gates[name](Qubit(q)) + if name not in delay_gates.keys(): + delay_gates[name] = float(duration) + + else: + new_p += i + if isinstance(i, Gate): + targets = tuple(t.index for t in i.qubits) + + if len(targets) == 2: + if depolarizing: + name = i.name if i.name in noise_gates.keys() else No_Depolarizing + new_p += noise_gates[name](targets[0], targets[1]) + if damping_after_dephasing_after_2q_gate: + for q in qubits: + if (q not in targets or not depolarizing) or ( + q in targets and damping_after_dephasing_only_on_targets + ): + new_p += noise_gates[single_qubit_noise_2Q_gate_name](q) + + elif len(targets) == 1: + if depolarizing: + new_p += noise_gates[Depolarizing_1Q_gate](targets[0]) + if damping_after_dephasing_after_1q_gate: + for q in qubits: + if (q not in targets or not depolarizing) or ( + q in targets and damping_after_dephasing_only_on_targets + ): + new_p += noise_gates[single_qubit_noise_1Q_gate_name](q) + + return new_p, delay_gates + + +def add_kraus_maps_to_program( + new_p: "Program", + calibrations: Calibrations, + delay_gates: Dict, + depolarizing: bool, + damping_after_dephasing_after_1q_gate: bool, + damping_after_dephasing: bool, + readout_noise: bool, +): + if depolarizing: + new_p = add_depolarizing_noise(prog=new_p, fidelities=calibrations.fidelities) + + if damping_after_dephasing_after_1q_gate or damping_after_dephasing: + new_p = add_single_qubit_noise(prog=new_p, T1=calibrations.T1, T2=calibrations.T2) + + if readout_noise: + new_p = add_readout_noise(prog=new_p, ro_fidelity=calibrations.readout_fidelity) + + new_p = add_delay_maps(new_p, delay_gates, calibrations.T1, calibrations.T2) + return new_p + + +def add_noise_to_program( + isa: InstructionSetArchitecture, + p: "Program", + convert_to_native: bool = True, + calibrations: Optional[Calibrations] = None, + depolarizing: bool = True, + damping_after_dephasing_after_1q_gate: bool = False, + damping_after_dephasing_after_2q_gate: bool = True, + damping_after_dephasing_only_on_targets: bool = False, + readout_noise: bool = True, + noise_intensity: float = 1.0, +) -> "Program": + """ + Add generic damping and dephasing noise to a program. + Noise is added to all qubits, after a 2-qubit gate operation. + This function will define new "I" gates and add Kraus noise to these gates. + :param damping_after_dephasing_only_on_targets: add damping after dephasing only on the target qubits of the gate. + :param noise_intensity: one parameter to control the noise intensity. + :param qcs_quantum_processor: A InstructionSetArchitecture object. + :param p: A pyquil program + :param calibrations: optional, can get the calibrations in advance, + instead of producing them from the URL. + :param depolarizing: add depolarizing noise, default is True. + :param damping_after_dephasing_after_1q_gate: add damping after dephasing to all qubits after every one-qubit gate. + default is False. + :param damping_after_dephasing_after_2q_gate: add damping after dephasing to all qubits after every two-qubit gate. + default is True. + :param readout_noise: add readout noise. default is True. + :param noise_intensity: the noise intensity. receives non-negative values. + default is 1.0, the noise as in the real QC. 0 is no noise. + + :return: A new program with noisy operators. + """ + from pyquil.quil import Program + + if calibrations is None: + calibrations = Calibrations(isa=InstructionSetArchitecture) + + new_p = Program() + + new_p, noise_gates = define_noisy_gates_on_new_program( + new_p=new_p, + prog=p, + two_q_gates=calibrations.two_q_gates, + depolarizing=depolarizing, + damping_after_dephasing_after_2q_gate=damping_after_dephasing_after_2q_gate, + damping_after_dephasing_after_1q_gate=damping_after_dephasing_after_1q_gate, + ) + + new_p, delay_gates = add_noisy_gates_to_program( + new_p=new_p, + prog=p, + noise_gates=noise_gates, + damping_after_dephasing_after_2q_gate=damping_after_dephasing_after_2q_gate, + damping_after_dephasing_after_1q_gate=damping_after_dephasing_after_1q_gate, + depolarizing=depolarizing, + damping_after_dephasing_only_on_targets=damping_after_dephasing_only_on_targets, + ) + + if noise_intensity != 1.0: + new_calibrations = Calibrations(calibrations=calibrations) + new_calibrations.change_noise_intensity(noise_intensity) + calibrations = new_calibrations + + new_p = add_kraus_maps_to_program( + new_p, + calibrations, + delay_gates, + depolarizing, + damping_after_dephasing_after_1q_gate, + damping_after_dephasing_after_2q_gate, + readout_noise, + ) + + new_p.wrap_in_numshots_loop(p.num_shots) + + return new_p diff --git a/pyquil/operator_estimation.py b/pyquil/operator_estimation.py index bfa46a8a8..1ec408c58 100644 --- a/pyquil/operator_estimation.py +++ b/pyquil/operator_estimation.py @@ -151,7 +151,6 @@ def _generate_experiment_programs( programs = [] meas_qubits = [] for settings in tomo_experiment: - # Prepare a state according to the amalgam of all setting.in_state total_prog = Program() if active_reset: diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py index 86108bb46..1fb2e685f 100644 --- a/pyquil/quilbase.py +++ b/pyquil/quilbase.py @@ -1233,7 +1233,6 @@ def get_qubits(self, indices: bool = True) -> Set[QubitDesignator]: class DelayFrames(AbstractInstruction): def __init__(self, frames: List[Frame], duration: float): - # all frames should be on the same qubits if len(frames) == 0: raise ValueError("DELAY expected nonempty list of frames.") diff --git a/pyquil/quiltwaveforms.py b/pyquil/quiltwaveforms.py index b4a6ad3c6..343f96997 100644 --- a/pyquil/quiltwaveforms.py +++ b/pyquil/quiltwaveforms.py @@ -318,7 +318,6 @@ def samples(self, rate: float) -> np.ndarray: @waveform("boxcar_kernel") class BoxcarAveragerKernel(TemplateWaveform): - scale: Optional[float] = None """ An optional global scaling factor. """ diff --git a/test/unit/test_noise.py b/test/unit/test_noise.py index f7b17b690..aa0b5add2 100644 --- a/test/unit/test_noise.py +++ b/test/unit/test_noise.py @@ -7,26 +7,26 @@ from pyquil.api._qam import QAMExecutionResult from pyquil.gates import RZ, RX, I, CZ from pyquil.noise import ( - pauli_kraus_map, - damping_kraus_map, - dephasing_kraus_map, - tensor_kraus_maps, - _get_program_gates, - _decoherence_noise_model, - add_decoherence_noise, - combine_kraus_maps, - damping_after_dephasing, - INFINITY, - apply_noise_model, - _noise_model_program_header, - KrausModel, - NoiseModel, - corrupt_bitstring_probs, - correct_bitstring_probs, - estimate_bitstring_probs, - bitstring_probs_to_z_moments, - estimate_assignment_probs, - NO_NOISE, + pauli_kraus_map, + damping_kraus_map, + dephasing_kraus_map, + tensor_kraus_maps, + _get_program_gates, + _decoherence_noise_model, + add_single_qubit_noise, + combine_kraus_maps, + damping_after_dephasing, + INFINITY, + apply_noise_model, + _noise_model_program_header, + KrausModel, + NoiseModel, + corrupt_bitstring_probs, + correct_bitstring_probs, + estimate_bitstring_probs, + bitstring_probs_to_z_moments, + estimate_assignment_probs, + NO_NOISE, ) from pyquil.quil import Pragma, Program from pyquil.quilbase import DefGate, Gate @@ -157,8 +157,8 @@ def test_decoherence_noise(): ) assert headers.out() in new_prog.out() - # verify that high-level add_decoherence_noise reproduces new_prog - new_prog2 = add_decoherence_noise(prog, T1={0: 30e-6}, T2={0: 30e-6}) + # verify that high-level add_single_qubit_noise reproduces new_prog + new_prog2 = add_single_qubit_noise(prog, T1={0: 30e-6}, T2={0: 30e-6}) assert new_prog == new_prog2