From a41e260c87e2c493125d90907201e59c7f4f1752 Mon Sep 17 00:00:00 2001 From: shrapp Date: Fri, 19 May 2023 00:59:16 +0300 Subject: [PATCH 01/19] =?UTF-8?q?A=20new=20realistic=20noise=20simulation?= =?UTF-8?q?=20of=20Rigetti=E2=80=99s=20QPU=20implemented=20in=20addition?= =?UTF-8?q?=20to=20former=20features=20in=20noise.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/noise.rst | 270 +++++- pyquil/noise.py | 1840 +++++++++++++++++++++++++-------------- test/unit/test_noise.py | 44 +- 3 files changed, 1471 insertions(+), 683 deletions(-) diff --git a/docs/source/noise.rst b/docs/source/noise.rst index 03c0c440c..6c1a53223 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -586,7 +586,7 @@ Adding Decoherence Noise In this example, we investigate how a program might behave on a near-term device that is subject to *T1*- and *T2*-type noise using the convenience function -:py:func:`pyquil.noise.add_decoherence_noise`. The same module also contains some other useful +:py:func:`pyquil.noise.add_single_qubit_noise`. The same module also contains some other useful functions to define your own types of noise models, e.g., :py:func:`pyquil.noise.tensor_kraus_maps` for generating multi-qubit noise processes, :py:func:`pyquil.noise.combine_kraus_maps` for describing the succession of two noise processes and @@ -700,12 +700,12 @@ gate noise, respectively. .. code:: python - from pyquil.noise import add_decoherence_noise + from pyquil.noise import add_single_qubit_noise records = [] for theta in thetas: for t1 in t1s: prog = get_compiled_prog(theta) - noisy = add_decoherence_noise(prog, T1=t1).inst([ + noisy = add_single_qubit_noise(prog, T1=t1).inst([ Declare("ro", "BIT", 2), MEASURE(0, ("ro", 0)), MEASURE(1, ("ro", 1)), @@ -1335,3 +1335,267 @@ 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 + import matplotlib.pyplot as plt + + def ghz(qc, 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) + if noise: + if cal is not None: + p = add_noise_to_program(qc, p, calibrations=cal, noise_intensity=intensity) + else: + p = add_noise_to_program(qc, p, noise_intensity=intensity) + return p + + def run_experiment(qpu, qubits, numshots): + qvm = get_qc(qpu, as_qvm=True, execution_timeout=1000) + cal = Calibrations(qvm) + no_noise = qvm.run(ghz(qvm,qubits,numshots, False)).readout_data.get("ro") + half_noise = qvm.run(ghz(qvm,qubits,numshots, True, cal, intensity=0.5)).readout_data.get("ro") + noisy = qvm.run(ghz(qvm,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/noise.py b/pyquil/noise.py index 5521214e4..4f9e48b13 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -16,21 +16,24 @@ """ Module for creating and verifying noisy gate and readout definitions. """ +import json 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 +import requests +from pyquil.api import QuantumComputer 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 if TYPE_CHECKING: - from pyquil.quil import Program - from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + from pyquil.quil import Program + from pyquil.api import QuantumComputer as PyquilApiQuantumComputer INFINITY = float("inf") "Used for infinite coherence times." @@ -39,290 +42,290 @@ class KrausModel(_KrausModel): - """ - Encapsulate a single gate's noise model. - - :ivar str gate: The name of the gate. - :ivar Sequence[float] params: Optional parameters for the gate. - :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. - """ - - @staticmethod - 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. - :return: A complex square numpy array representing the Kraus operator. - """ - m = np.asarray(m, dtype=complex) - if m.ndim == 3: - m = m[0] + 1j * m[1] - if not m.ndim == 2: # pragma no coverage - raise ValueError("Need 2d array.") - if not m.shape[0] == m.shape[1]: # pragma no coverage - raise ValueError("Need square matrix.") - return m - - def to_dict(self) -> Dict[str, Any]: - """ - Create a dictionary representation of a KrausModel. - - 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 - } - - :return: A JSON compatible dictionary representation. - :rtype: Dict[str,Any] - """ - res = self._asdict() - res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops] - return res - - @staticmethod - 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. - :return: The deserialized KrausModel. - """ - kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] - return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"]) - - def __eq__(self, other: object) -> bool: - return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() - - def __neq__(self, other: object) -> bool: - return not self.__eq__(other) + """ + Encapsulate a single gate's noise model. + + :ivar str gate: The name of the gate. + :ivar Sequence[float] params: Optional parameters for the gate. + :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. + """ + + @staticmethod + 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. + :return: A complex square numpy array representing the Kraus operator. + """ + m = np.asarray(m, dtype=complex) + if m.ndim == 3: + m = m[0] + 1j * m[1] + if not m.ndim == 2: # pragma no coverage + raise ValueError("Need 2d array.") + if not m.shape[0] == m.shape[1]: # pragma no coverage + raise ValueError("Need square matrix.") + return m + + def to_dict(self) -> Dict[str, Any]: + """ + Create a dictionary representation of a KrausModel. + + 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 + } + + :return: A JSON compatible dictionary representation. + :rtype: Dict[str,Any] + """ + res = self._asdict() + res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops] + return res + + @staticmethod + 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. + :return: The deserialized KrausModel. + """ + kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] + return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"]) + + def __eq__(self, other: object) -> bool: + return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() + + def __neq__(self, other: object) -> bool: + return not self.__eq__(other) _NoiseModel = namedtuple("_NoiseModel", ["gates", "assignment_probs"]) class NoiseModel(_NoiseModel): - """ - Encapsulate the QPU noise model containing information about the noisy gates. - - :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. - """ - - def to_dict(self) -> Dict[str, Any]: - """ - Create a JSON serializable representation of the noise model. - - For example:: - - { - "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. - """ - return { - "gates": [km.to_dict() for km in self.gates], - "assignment_probs": {str(qid): a.tolist() for qid, a in self.assignment_probs.items()}, - } - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "NoiseModel": - """ - Re-create the noise model from a dictionary representation. - - :param d: The dictionary representation. - :return: The restored noise model. - """ - return NoiseModel( - gates=[KrausModel.from_dict(t) for t in d["gates"]], - assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, - ) - - def gates_by_name(self, name: str) -> List[KrausModel]: - """ - Return all defined noisy gates of a particular gate name. - - :param str name: The gate name. - :return: A list of noise models representing that gate. - """ - return [g for g in self.gates if g.gate == name] - - def __eq__(self, other: object) -> bool: - return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict() - - def __neq__(self, other: object) -> bool: - return not self.__eq__(other) + """ + Encapsulate the QPU noise model containing information about the noisy gates. + + :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. + """ + + def to_dict(self) -> Dict[str, Any]: + """ + Create a JSON serializable representation of the noise model. + + For example:: + + { + "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. + """ + return { + "gates": [km.to_dict() for km in self.gates], + "assignment_probs": {str(qid): a.tolist() for qid, a in self.assignment_probs.items()}, + } + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "NoiseModel": + """ + Re-create the noise model from a dictionary representation. + + :param d: The dictionary representation. + :return: The restored noise model. + """ + return NoiseModel( + gates=[KrausModel.from_dict(t) for t in d["gates"]], + assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, + ) + + def gates_by_name(self, name: str) -> List[KrausModel]: + """ + Return all defined noisy gates of a particular gate name. + + :param str name: The gate name. + :return: A list of noise models representing that gate. + """ + return [g for g in self.gates if g.gate == name] + + def __eq__(self, other: object) -> bool: + return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict() + + def __neq__(self, other: object) -> bool: + return not self.__eq__(other) def _check_kraus_ops(n: int, kraus_ops: Sequence[np.ndarray]) -> None: - """ - Verify that the Kraus operators are of the correct shape and satisfy the correct normalization. + """ + Verify that the Kraus operators are of the correct shape and satisfy the correct normalization. - :param n: Number of qubits - :param kraus_ops: The Kraus operators as numpy.ndarrays. - """ - for k in kraus_ops: - if not np.shape(k) == (2**n, 2**n): - raise ValueError("Kraus operators for {0} qubits must have shape {1}x{1}: {2}".format(n, 2**n, k)) + :param n: Number of qubits + :param kraus_ops: The Kraus operators as numpy.ndarrays. + """ + for k in kraus_ops: + if not np.shape(k) == (2 ** n, 2 ** n): + raise ValueError("Kraus operators for {0} qubits must have shape {1}x{1}: {2}".format(n, 2 ** n, k)) - kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops) - if not np.allclose(kdk_sum, np.eye(2**n), atol=1e-3): - raise ValueError("Kraus operator not correctly normalized: sum_j K_j^*K_j == {}".format(kdk_sum)) + kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops) + if not np.allclose(kdk_sum, np.eye(2 ** n), atol=1e-3): + raise ValueError("Kraus operator not correctly normalized: sum_j K_j^*K_j == {}".format(kdk_sum)) def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[np.ndarray]) -> List[Pragma]: - """ - Generate the pragmas to define a Kraus map for a specific gate on some qubits. - - :param name: The name of the gate. - :param qubit_indices: The qubits - :param kraus_ops: The Kraus operators as matrices. - :return: A QUIL string with PRAGMA ADD-KRAUS ... statements. - """ - - pragmas = [ - Pragma( - "ADD-KRAUS", - (name,) + tuple(qubit_indices), - "({})".format(" ".join(map(format_parameter, np.ravel(k)))), - ) - for k in kraus_ops - ] - return pragmas + """ + Generate the pragmas to define a Kraus map for a specific gate on some qubits. + + :param name: The name of the gate. + :param qubit_indices: The qubits + :param kraus_ops: The Kraus operators as matrices. + :return: A QUIL string with PRAGMA ADD-KRAUS ... statements. + """ + + pragmas = [ + Pragma( + "ADD-KRAUS", + (name,) + tuple(qubit_indices), + "({})".format(" ".join(map(format_parameter, np.ravel(k)))), + ) + for k in kraus_ops + ] + return pragmas def append_kraus_to_gate( - kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray + kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray ) -> List[Union[np.number, np.ndarray]]: - """ - Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. + """ + Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. - :param kraus_ops: The Kraus operators. - :param gate_matrix: The unitary gate. - :return: A list of transformed Kraus operators. - """ - return [kj.dot(gate_matrix) for kj in kraus_ops] + :param kraus_ops: The Kraus operators. + :param gate_matrix: The unitary gate. + :return: A list of transformed Kraus operators. + """ + return [kj.dot(gate_matrix) for kj in kraus_ops] def pauli_kraus_map(probabilities: Sequence[float]) -> List[np.ndarray]: - r""" - Generate the Kraus operators corresponding to a pauli channel. + r""" + 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. + :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. - 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. - """ - if len(probabilities) not in [4, 16]: - raise ValueError( - "Currently we only support one or two qubits, " - "so the provided list of probabilities must have length 4 or 16." - ) - if not np.allclose(sum(probabilities), 1.0, atol=1e-3): - raise ValueError("Probabilities must sum to one.") + :return: A list of the 4^num_qubits Kraus operators that parametrize the map. + """ + if len(probabilities) not in [4, 16]: + raise ValueError( + "Currently we only support one or two qubits, " + "so the provided list of probabilities must have length 4 or 16." + ) + if not np.allclose(sum(probabilities), 1.0, atol=1e-3): + raise ValueError("Probabilities must sum to one.") - paulis = [ - np.eye(2), - np.array([[0, 1], [1, 0]]), - np.array([[0, -1j], [1j, 0]]), - np.array([[1, 0], [0, -1]]), - ] + paulis = [ + np.eye(2), + np.array([[0, 1], [1, 0]]), + np.array([[0, -1j], [1j, 0]]), + np.array([[1, 0], [0, -1]]), + ] - if len(probabilities) == 4: - operators = paulis - else: - operators = np.kron(paulis, paulis) # type: ignore + if len(probabilities) == 4: + operators = paulis + else: + operators = np.kron(paulis, paulis) # type: ignore - return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] + return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] def damping_kraus_map(p: float = 0.10) -> List[np.ndarray]: - """ - Generate the Kraus operators corresponding to an amplitude damping - noise channel. + """ + Generate the Kraus operators corresponding to an amplitude damping + noise channel. - :param p: The one-step damping probability. - :return: A list [k1, k2] of the Kraus operators that parametrize the map. - :rtype: list - """ - damping_op = np.sqrt(p) * np.array([[0, 1], [0, 0]]) + :param p: The one-step damping probability. + :return: A list [k1, k2] of the Kraus operators that parametrize the map. + :rtype: list + """ + damping_op = np.sqrt(p) * np.array([[0, 1], [0, 0]]) - residual_kraus = np.diag([1, np.sqrt(1 - p)]) # type: ignore - return [residual_kraus, damping_op] + residual_kraus = np.diag([1, np.sqrt(1 - p)]) # type: ignore + return [residual_kraus, damping_op] def dephasing_kraus_map(p: float = 0.10) -> List[np.ndarray]: - """ - Generate the Kraus operators corresponding to a dephasing channel. + """ + Generate the Kraus operators corresponding to a dephasing channel. - :params float p: The one-step dephasing probability. - :return: A list [k1, k2] of the Kraus operators that parametrize the map. - :rtype: list - """ - return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])] # type: ignore + :params float p: The one-step dephasing probability. + :return: A list [k1, k2] of the Kraus operators that parametrize the map. + :rtype: list + """ + return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])] # type: ignore def tensor_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.ndarray]: - """ - Generate the Kraus map corresponding to the composition - of two maps on different qubits. + """ + Generate the Kraus map corresponding to the composition + of two maps on different qubits. - :param k1: The Kraus operators for the first qubit. - :param k2: The Kraus operators for the second qubit. - :return: A list of tensored Kraus operators. - """ - return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore + :param k1: The Kraus operators for the first qubit. + :param k2: The Kraus operators for the second qubit. + :return: A list of tensored Kraus operators. + """ + return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore def combine_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.ndarray]: - """ - Generate the Kraus map corresponding to the composition - of two maps on the same qubits with k1 being applied to the state - after k2. + """ + Generate the Kraus map corresponding to the composition + of two maps on the same qubits with k1 being applied to the state + after k2. - :param k1: The list of Kraus operators that are applied second. - :param k2: The list of Kraus operators that are applied first. - :return: A combinatorially generated list of composed Kraus operators. - """ - return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore + :param k1: The list of Kraus operators that are applied second. + :param k2: The list of Kraus operators that are applied first. + :return: A combinatorially generated list of composed Kraus operators. + """ + return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.ndarray]: - """ + """ Generate the Kraus map corresponding to the composition of a dephasing channel followed by an amplitude damping channel. @@ -331,25 +334,24 @@ def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.n :param gate_time: The gate duration. :return: A list of Kraus operators. """ - assert T1 >= 0 - assert T2 >= 0 - - if T1 != INFINITY: - 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") - 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)] - return combine_kraus_maps(damping, dephasing) + assert T1 >= 0 + assert T2 >= 0 + + if T1 != INFINITY: + 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: + 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)] + return combine_kraus_maps(damping, dephasing) # You can only apply gate-noise to non-parametrized gates or parametrized gates at fixed parameters. @@ -358,467 +360,989 @@ def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.n class NoisyGateUndefined(Exception): - """Raise when user attempts to use noisy gate outside of currently supported set.""" + """Raise when user attempts to use noisy gate outside of currently supported set.""" - pass + pass def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> Tuple[np.ndarray, str]: - """ - Look up the numerical gate representation and a proposed 'noisy' name. - - :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. - """ - params = tuple(params) - if gate_name == "I": - assert params == () - return np.eye(2), "NOISY-I" - if gate_name == "RX": - (angle,) = params - if not isinstance(angle, (int, float, complex)): - raise TypeError(f"Cannot produce noisy gate for parameter of type {type(angle)}") - - if np.isclose(angle, np.pi / 2, atol=ANGLE_TOLERANCE): - return np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), "NOISY-RX-PLUS-90" - elif np.isclose(angle, -np.pi / 2, atol=ANGLE_TOLERANCE): - return np.array([[1, 1j], [1j, 1]]) / np.sqrt(2), "NOISY-RX-MINUS-90" - elif np.isclose(angle, np.pi, atol=ANGLE_TOLERANCE): - return np.array([[0, -1j], [-1j, 0]]), "NOISY-RX-PLUS-180" - elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE): - return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180" - elif gate_name == "CZ": - assert params == () - return np.diag([1, 1, 1, -1]), "NOISY-CZ" # type: ignore - - raise NoisyGateUndefined( - "Undefined gate and params: {}{}\n" - "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ".format(gate_name, params) - ) + """ + Look up the numerical gate representation and a proposed 'noisy' name. + + :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. + """ + params = tuple(params) + if gate_name == "I": + assert params == () + return np.eye(2), "NOISY-I" + if gate_name == "RX": + (angle,) = params + if not isinstance(angle, (int, float, complex)): + raise TypeError(f"Cannot produce noisy gate for parameter of type {type(angle)}") + + if np.isclose(angle, np.pi / 2, atol=ANGLE_TOLERANCE): + return np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), "NOISY-RX-PLUS-90" + elif np.isclose(angle, -np.pi / 2, atol=ANGLE_TOLERANCE): + return np.array([[1, 1j], [1j, 1]]) / np.sqrt(2), "NOISY-RX-MINUS-90" + elif np.isclose(angle, np.pi, atol=ANGLE_TOLERANCE): + return np.array([[0, -1j], [-1j, 0]]), "NOISY-RX-PLUS-180" + elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE): + return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180" + elif gate_name == "CZ": + assert params == () + return np.diag([1, 1, 1, -1]), "NOISY-CZ" # type: ignore + + raise NoisyGateUndefined( + "Undefined gate and params: {}{}\n" + "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ".format(gate_name, params) + ) def _get_program_gates(prog: "Program") -> List[Gate]: - """ - Get all gate applications appearing in prog. + """ + Get all gate applications appearing in prog. - :param prog: The program - :return: A list of all Gates in prog (without duplicates). - """ - return sorted({i for i in prog if isinstance(i, Gate)}, key=lambda g: g.out()) + :param prog: The program + :return: A list of all Gates in prog (without duplicates). + """ + return sorted({i for i in prog if isinstance(i, Gate)}, key=lambda g: g.out()) def _decoherence_noise_model( - gates: Sequence[Gate], - T1: Union[Dict[int, float], float] = 30e-6, - T2: Union[Dict[int, float], float] = 30e-6, - gate_time_1q: float = 50e-9, - gate_time_2q: float = 150e-09, - ro_fidelity: Union[Dict[int, float], float] = 0.95, + gates: Sequence[Gate], + T1: Union[Dict[int, float], float] = 30e-6, + T2: Union[Dict[int, float], float] = 30e-6, + gate_time_1q: float = 50e-9, + gate_time_2q: float = 150e-09, + ro_fidelity: Union[Dict[int, float], float] = 0.95, ) -> NoiseModel: - """ - The default noise parameters - - - T1 = 30 us - - T2 = 30 us - - 1q gate time = 50 ns - - 2q gate time = 150 ns - - are currently typical for near-term devices. - - This function will define new gates and add Kraus noise to these gates. It will translate - the input program to use the noisy version of the gates. - - :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. - :param T2: The T2 dephasing time either globally or in a - 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. - :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - 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. - :return: A NoiseModel with the appropriate Kraus operators defined. - """ - all_qubits = set(sum(([t.index for t in g.qubits] for g in gates), [])) - if isinstance(T1, dict): - all_qubits.update(T1.keys()) - if isinstance(T2, dict): - all_qubits.update(T2.keys()) - if isinstance(ro_fidelity, dict): - all_qubits.update(ro_fidelity.keys()) - - if not isinstance(T1, dict): - T1 = {q: T1 for q in all_qubits} - - if not isinstance(T2, dict): - T2 = {q: T2 for q in all_qubits} - - if not isinstance(ro_fidelity, dict): - ro_fidelity = {q: ro_fidelity for q in all_qubits} - - noisy_identities_1q = { - q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_1q) for q in all_qubits - } - noisy_identities_2q = { - q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_2q) for q in all_qubits - } - kraus_maps = [] - for g in gates: - targets = tuple(t.index for t in g.qubits) - if g.name in NO_NOISE: - continue - matrix, _ = get_noisy_gate(g.name, g.params) - - if len(targets) == 1: - noisy_I = noisy_identities_1q[targets[0]] - else: - if len(targets) != 2: - raise ValueError("Noisy gates on more than 2Q not currently supported") - - # note this ordering of the tensor factors is necessary due to how the QVM orders - # the wavefunction basis - noisy_I = tensor_kraus_maps(noisy_identities_2q[targets[1]], noisy_identities_2q[targets[0]]) - kraus_maps.append( - KrausModel( - g.name, - tuple(g.params), - targets, - combine_kraus_maps(noisy_I, [matrix]), - # FIXME (Nik): compute actual avg gate fidelity for this simple - # noise model - 1.0, - ) - ) - aprobs = {} - for q, f_ro in ro_fidelity.items(): - aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) - - return NoiseModel(kraus_maps, aprobs) + """ + The default noise parameters + + - T1 = 30 us + - T2 = 30 us + - 1q gate time = 50 ns + - 2q gate time = 150 ns + + are currently typical for near-term devices. + + This function will define new gates and add Kraus noise to these gates. It will translate + the input program to use the noisy version of the gates. + + :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. + :param T2: The T2 dephasing time either globally or in a + 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. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. + 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. + :return: A NoiseModel with the appropriate Kraus operators defined. + """ + all_qubits = set(sum(([t.index for t in g.qubits] for g in gates), [])) + if isinstance(T1, dict): + all_qubits.update(T1.keys()) + if isinstance(T2, dict): + all_qubits.update(T2.keys()) + if isinstance(ro_fidelity, dict): + all_qubits.update(ro_fidelity.keys()) + + if not isinstance(T1, dict): + T1 = {q: T1 for q in all_qubits} + + if not isinstance(T2, dict): + T2 = {q: T2 for q in all_qubits} + + if not isinstance(ro_fidelity, dict): + ro_fidelity = {q: ro_fidelity for q in all_qubits} + + noisy_identities_1q = { + q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_1q) for q in all_qubits + } + noisy_identities_2q = { + q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_2q) for q in all_qubits + } + kraus_maps = [] + for g in gates: + targets = tuple(t.index for t in g.qubits) + if g.name in NO_NOISE: + continue + matrix, _ = get_noisy_gate(g.name, g.params) + + if len(targets) == 1: + noisy_I = noisy_identities_1q[targets[0]] + else: + if len(targets) != 2: + raise ValueError("Noisy gates on more than 2Q not currently supported") + + # note this ordering of the tensor factors is necessary due to how the QVM orders + # the wavefunction basis + noisy_I = tensor_kraus_maps(noisy_identities_2q[targets[1]], noisy_identities_2q[targets[0]]) + kraus_maps.append( + KrausModel( + g.name, + tuple(g.params), + targets, + combine_kraus_maps(noisy_I, [matrix]), + # FIXME (Nik): compute actual avg gate fidelity for this simple + # noise model + 1.0, + ) + ) + aprobs = {} + for q, f_ro in ro_fidelity.items(): + aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) + + return NoiseModel(kraus_maps, aprobs) def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p11: float = 0.911) -> NoiseModel: - """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. + """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. - For simplicity, we use the default values for T1, T2, gate times, et al. and only allow - the specification of readout fidelities. - """ - gates = _get_qvm_noise_supported_gates(isa) - noise_model = _decoherence_noise_model(gates) - aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) - aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} - return NoiseModel(noise_model.gates, aprobs) + For simplicity, we use the default values for T1, T2, gate times, et al. and only allow + the specification of readout fidelities. + """ + gates = _get_qvm_noise_supported_gates(isa) + noise_model = _decoherence_noise_model(gates) + aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) + aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} + return NoiseModel(noise_model.gates, aprobs) 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. - - :param noise_model: The assumed noise model. - :return: A quil Program with the noise pragmas. - """ - from pyquil.quil import 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)) - - # if ideal version of gate has not yet been DEFGATE'd, do this - if new_name not in defgates: - p.defgate(new_name, ideal_gate) - defgates.add(new_name) - except NoisyGateUndefined: - print( - "WARNING: Could not find ideal gate definition for gate {}".format(k.gate), - file=sys.stderr, - ) - new_name = k.gate - - # define noisy version of gate on specific targets - p.define_noisy_gate(new_name, k.targets, k.kraus_ops) - - # define noisy readouts - for q, ap in noise_model.assignment_probs.items(): - p.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) - return p + """ + 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. + + :param noise_model: The assumed noise model. + :return: A quil Program with the noise pragmas. + """ + from pyquil.quil import 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)) + + # if ideal version of gate has not yet been DEFGATE'd, do this + if new_name not in defgates: + p.defgate(new_name, ideal_gate) + defgates.add(new_name) + except NoisyGateUndefined: + print( + "WARNING: Could not find ideal gate definition for gate {}".format(k.gate), + file=sys.stderr, + ) + new_name = k.gate + + # define noisy version of gate on specific targets + p.define_noisy_gate(new_name, k.targets, k.kraus_ops) + + # define noisy readouts + for q, ap in noise_model.assignment_probs.items(): + p.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) + return p def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": - """ - Apply a noise model to a program and generated a 'noisy-fied' version of the program. - - :param prog: A Quil Program object. - :param noise_model: A NoiseModel, either generated from an ISA or - from a simple decoherence model. - :return: A new program translated to a noisy gateset and with noisy readout as described by the - noisemodel. - """ - new_prog = _noise_model_program_header(noise_model) - for i in prog: - if isinstance(i, Gate) and noise_model.gates: - try: - _, new_name = get_noisy_gate(i.name, tuple(i.params)) - new_prog += Gate(new_name, [], i.qubits) - except NoisyGateUndefined: - new_prog += i - else: - new_prog += i - return prog.copy_everything_except_instructions() + new_prog + """ + Apply a noise model to a program and generated a 'noisy-fied' version of the program. + + :param prog: A Quil Program object. + :param noise_model: A NoiseModel, either generated from an ISA or + from a simple decoherence model. + :return: A new program translated to a noisy gateset and with noisy readout as described by the + noisemodel. + """ + new_prog = _noise_model_program_header(noise_model) + for i in prog: + if isinstance(i, Gate) and noise_model.gates: + try: + _, new_name = get_noisy_gate(i.name, tuple(i.params)) + new_prog += Gate(new_name, [], i.qubits) + except NoisyGateUndefined: + new_prog += i + else: + new_prog += i + return prog.copy_everything_except_instructions() + new_prog def add_decoherence_noise( - prog: "Program", - T1: Union[Dict[int, float], float] = 30e-6, - T2: Union[Dict[int, float], float] = 30e-6, - gate_time_1q: float = 50e-9, - gate_time_2q: float = 150e-09, - ro_fidelity: Union[Dict[int, float], float] = 0.95, + prog: "Program", + T1: Union[Dict[int, float], float] = 30e-6, + T2: Union[Dict[int, float], float] = 30e-6, + gate_time_1q: float = 50e-9, + gate_time_2q: float = 150e-09, + ro_fidelity: Union[Dict[int, float], float] = 0.95, ) -> "Program": - """ - Add generic damping and dephasing noise to a program. - - This high-level function is provided as a convenience to investigate the effects of a - generic noise model on a program. For more fine-grained control, please investigate - the other methods available in the ``pyquil.noise`` module. - - In an attempt to closely model the QPU, noisy versions of RX(+-pi/2) and CZ are provided; - I and parametric RZ are noiseless, and other gates are not allowed. To use this function, - you need to compile your program to this native gate set. - - The default noise parameters - - - T1 = 30 us - - T2 = 30 us - - 1q gate time = 50 ns - - 2q gate time = 150 ns - - are currently typical for near-term devices. - - This function will define new gates and add Kraus noise to these gates. It will translate - the input program to use the noisy version of the gates. - - :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. - :param T2: The T2 dephasing time either globally or in a - 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. - :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - 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. - :return: A new program with noisy operators. - """ - gates = _get_program_gates(prog) - noise_model = _decoherence_noise_model( - gates, - T1=T1, - T2=T2, - gate_time_1q=gate_time_1q, - gate_time_2q=gate_time_2q, - ro_fidelity=ro_fidelity, - ) - return apply_noise_model(prog, noise_model) + """ + Add generic damping and dephasing noise to a program. + + This high-level function is provided as a convenience to investigate the effects of a + generic noise model on a program. For more fine-grained control, please investigate + the other methods available in the ``pyquil.noise`` module. + + In an attempt to closely model the QPU, noisy versions of RX(+-pi/2) and CZ are provided; + I and parametric RZ are noiseless, and other gates are not allowed. To use this function, + you need to compile your program to this native gate set. + + The default noise parameters + + - T1 = 30 us + - T2 = 30 us + - 1q gate time = 50 ns + - 2q gate time = 150 ns + + are currently typical for near-term devices. + + This function will define new gates and add Kraus noise to these gates. It will translate + the input program to use the noisy version of the gates. + + :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. + :param T2: The T2 dephasing time either globally or in a + 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. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. + 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. + :return: A new program with noisy operators. + """ + gates = _get_program_gates(prog) + noise_model = _decoherence_noise_model( + gates, + T1=T1, + T2=T2, + gate_time_1q=gate_time_1q, + gate_time_2q=gate_time_2q, + ro_fidelity=ro_fidelity, + ) + return apply_noise_model(prog, noise_model) def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray: - """ - Ensure that an array ``p`` with bitstring probabilities has a separate axis for each qubit such - that ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + """ + Ensure that an array ``p`` with bitstring probabilities has a separate axis for each qubit such + that ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - This should not allocate much memory if ``p`` is already in ``C``-contiguous order (row-major). + 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. - :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. - """ - p = np.asarray(p, order="C") - num_qubits = int(round(np.log2(p.size))) - return p.reshape((2,) * num_qubits) + :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. + :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. + """ + p = np.asarray(p, order="C") + num_qubits = int(round(np.log2(p.size))) + return p.reshape((2,) * num_qubits) def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray: - """ - Given an array of single shot results estimate the probability distribution over all bitstrings. + """ + 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. - :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``. - """ - nshots, nq = np.shape(results) - outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) - probs = np.histogram(outcomes, bins=np.arange(-0.5, 2**nq, 1))[0] / float(nshots) # type: ignore - return _bitstring_probs_by_qubit(probs) + :param results: A 2d array where the outer axis iterates over shots + 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``. + """ + nshots, nq = np.shape(results) + outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) + probs = np.histogram(outcomes, bins=np.arange(-0.5, 2 ** nq, 1))[0] / float(nshots) # type: ignore + return _bitstring_probs_by_qubit(probs) _CHARS = "klmnopqrstuvwxyzabcdefgh0123456789" def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarray: - """ - Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits) - and a list of assignment probability matrices (one for each bit in the readout, ordered like - the inner axis of results) apply local 2x2 matrices to each bit index. + """ + Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits) + and a list of assignment probability matrices (one for each bit in the readout, ordered like + the inner axis of results) apply local 2x2 matrices to each bit index. - :param p: An array that enumerates a function indexed by - bitstrings:: + :param p: An array that enumerates a function indexed by + 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:: + :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:: - 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 - for idx, trafo_idx in enumerate(ts): - # this contraction pattern looks like - # 'ij,abcd...jklm...->abcd...iklm...' so it properly applies a "local" - # transformation to a single tensor-index without changing the order of - # indices - einsum_pat = ( - "ij," + _CHARS[:idx] + "j" + _CHARS[idx : nq - 1] + "->" + _CHARS[:idx] + "i" + _CHARS[idx : nq - 1] - ) - p_corrected = np.einsum(einsum_pat, trafo_idx, p_corrected) + 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 + for idx, trafo_idx in enumerate(ts): + # this contraction pattern looks like + # 'ij,abcd...jklm...->abcd...iklm...' so it properly applies a "local" + # transformation to a single tensor-index without changing the order of + # indices + einsum_pat = ( + "ij," + _CHARS[:idx] + "j" + _CHARS[idx: nq - 1] + "->" + _CHARS[:idx] + "i" + _CHARS[idx: nq - 1] + ) + p_corrected = np.einsum(einsum_pat, trafo_idx, p_corrected) - return p_corrected + return p_corrected def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.ndarray]) -> np.ndarray: - """ - Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis - over bits) and a list of assignment probability matrices (one for each bit in the readout, - 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``. - :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: - - [[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``. - """ - return _apply_local_transforms(p, assignment_probabilities) + """ + Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis + over bits) and a list of assignment probability matrices (one for each bit in the readout, + 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``. + :param assignment_probabilities: A list of assignment probability matrices + per qubit. Each assignment probability matrix is expected to be of the form:: + + [[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``. + """ + return _apply_local_transforms(p, assignment_probabilities) def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.ndarray]) -> np.ndarray: - """ - Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner - axis over bits) and a list of assignment probability matrices (one for each bit in the readout) - 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``. - :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: - - [[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``. - """ - return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) # type: ignore + """ + Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner + axis over bits) and a list of assignment probability matrices (one for each bit in the readout) + 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``. + :param assignment_probabilities: A list of assignment probability matrices + per qubit. Each assignment probability matrix is expected to be of the form:: + + [[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``. + """ + return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) # type: ignore def bitstring_probs_to_z_moments(p: np.ndarray) -> np.ndarray: - """ - Convert between bitstring probabilities and joint Z moment expectations. + """ + 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``. - :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:: + :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``. + :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:: - = 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))) + = 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))) def estimate_assignment_probs( - q: int, - trials: int, - qc: "PyquilApiQuantumComputer", - p0: Optional["Program"] = None, + q: int, + trials: int, + qc: "PyquilApiQuantumComputer", + p0: Optional["Program"] = None, ) -> np.ndarray: + """ + Estimate the readout assignment probabilities for a given qubit ``q``. + The returned matrix is of the form:: + + [[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. + :return: The assignment probability matrix + """ + from pyquil.quil import Program + + if p0 is None: # pragma no coverage + p0 = Program() + + p_i = ( + p0 + + Program( + Declare("ro", "BIT", 1), + I(q), + MEASURE(q, MemoryReference("ro", 0)), + ) + ).wrap_in_numshots_loop(trials) + results_i = np.sum(_run(qc, p_i)) + + p_x = ( + p0 + + Program( + Declare("ro", "BIT", 1), + RX(np.pi, q), + MEASURE(q, MemoryReference("ro", 0)), + ) + ).wrap_in_numshots_loop(trials) + results_x = np.sum(_run(qc, p_x)) + + p00 = 1.0 - results_i / float(trials) + p11 = results_x / float(trials) + return np.array([[p00, 1 - p11], [1 - p00, p11]]) + + +def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> List[List[int]]: + result = qc.run(qc.compiler.native_quil_to_executable(program)) + 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 get_qc_name(qc: QuantumComputer): + """ + returns the name of the quantum computer `qc`, + without the ending 'qvm' if it exists. + """ + name = qc.name + if name[-4:] == "-qvm": + name = name[0:-4] + return name + else: + return name + + +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 + + +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 his 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, qc: Optional[QuantumComputer] = None, cal=None): + self.fidelities = {} + self.readout_fidelity = {} + self.T2 = {} + self.T1 = {} + self.two_q_gates = set() + + if cal is not None: + self.T1 = cal.T1.copy() + self.T2 = cal.T2.copy() + self.readout_fidelity = cal.readout_fidelity.copy() + self.fidelities = {} + for key, val in cal.fidelities.items(): + self.fidelities[key] = val.copy() + self.two_q_gates = cal.two_q_gates + return + + if qc is None: + return # user can set his own values + + else: + qc_name = get_qc_name(qc) + if qc_name not in ["Aspen-M-2", "Aspen-M-3"]: + raise ValueError("qc must be Aspen-M-2 or Aspen-M-3") + else: + url = "https://forest-server.qcs.rigetti.com/lattices/" + response = requests.get(url + qc_name) + file = json.loads(response.text) + self.calibrations = file["lattice"]["specs"] + self.two_q_gates = self._get_qc_2q_gates(file["lattice"]["isa"]["2Q"]) + self._create_1q_dicts() + self._create_2q_dicts() + + def _create_1q_dicts(self): + qs = self.calibrations['1Q'].keys() + t1 = [self.calibrations['1Q'][q]['T1'] for q in qs] + t2 = [self.calibrations['1Q'][q]['T2'] for q in qs] + fidelities = [self.calibrations['1Q'][q]["f1QRB"] for q in qs] + readout = [self.calibrations['1Q'][q]["fRO"] for q in qs] + qubits_indexes = [int(q) for q in qs] + self.T1 = dict(zip(qubits_indexes, t1)) + self.T2 = dict(zip(qubits_indexes, t2)) + self.fidelities[Depolarizing_1Q_gate] = dict(zip(qs, fidelities)) + self.readout_fidelity = dict(zip(qubits_indexes, readout)) + + def _create_2q_dicts(self): + pairs = self.calibrations['2Q'].keys() + for gate in self.two_q_gates: + fidelity = [self.calibrations['2Q'][pair].get("f" + gate, 1.0) for pair in pairs] + self.fidelities[gate] = dict(zip(pairs, fidelity)) + + def change_noise_intensity(self, intensity: float): + 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): + """ + 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 = .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 """ - Estimate the readout assignment probabilities for a given qubit ``q``. - The returned matrix is of the form:: - - [[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. - :return: The assignment probability matrix + 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. """ - from pyquil.quil import Program - - if p0 is None: # pragma no coverage - p0 = Program() - - p_i = ( - p0 - + Program( - Declare("ro", "BIT", 1), - I(q), - MEASURE(q, MemoryReference("ro", 0)), - ) - ).wrap_in_numshots_loop(trials) - results_i = np.sum(_run(qc, p_i)) - - p_x = ( - p0 - + Program( - Declare("ro", "BIT", 1), - RX(np.pi, q), - MEASURE(q, MemoryReference("ro", 0)), - ) - ).wrap_in_numshots_loop(trials) - results_x = np.sum(_run(qc, p_x)) - - p00 = 1.0 - results_i / float(trials) - p11 = results_x / float(trials) - return np.array([[p00, 1 - p11], [1 - p00, p11]]) + 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( + qc: QuantumComputer, + 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 qc: A Quantum computer object + :param p: A pyquil program + :param convert_to_native: put `False` if the program is already in native pyquil or is not needed - + Note that it removes any delays. + :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. + """ -def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> List[List[int]]: - result = qc.run(qc.compiler.native_quil_to_executable(program)) - bitstrings = result.readout_data.get("ro") - assert bitstrings is not None - return cast(List[List[int]], bitstrings.tolist()) + if convert_to_native: + p = qc.compiler.quil_to_native_quil(p) + + if calibrations is None: + calibrations = Calibrations(qc=qc) + + 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(cal=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/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 From 0f1ce9b5d7b67f8bd877467c72e2d94fece76eec Mon Sep 17 00:00:00 2001 From: shrapp Date: Fri, 19 May 2023 01:02:23 +0300 Subject: [PATCH 02/19] =?UTF-8?q?A=20new=20realistic=20noise=20simulation?= =?UTF-8?q?=20of=20Rigetti=E2=80=99s=20QPU=20implemented=20in=20addition?= =?UTF-8?q?=20to=20former=20features=20in=20noise.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/noise.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/noise.rst b/docs/source/noise.rst index 6c1a53223..11e6a7eb8 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -586,7 +586,7 @@ Adding Decoherence Noise In this example, we investigate how a program might behave on a near-term device that is subject to *T1*- and *T2*-type noise using the convenience function -:py:func:`pyquil.noise.add_single_qubit_noise`. The same module also contains some other useful +:py:func:`pyquil.noise.add_decoherence_noise`. The same module also contains some other useful functions to define your own types of noise models, e.g., :py:func:`pyquil.noise.tensor_kraus_maps` for generating multi-qubit noise processes, :py:func:`pyquil.noise.combine_kraus_maps` for describing the succession of two noise processes and @@ -700,12 +700,12 @@ gate noise, respectively. .. code:: python - from pyquil.noise import add_single_qubit_noise + from pyquil.noise import add_decoherence_noise records = [] for theta in thetas: for t1 in t1s: prog = get_compiled_prog(theta) - noisy = add_single_qubit_noise(prog, T1=t1).inst([ + noisy = add_decoherence_noise(prog, T1=t1).inst([ Declare("ro", "BIT", 2), MEASURE(0, ("ro", 0)), MEASURE(1, ("ro", 1)), From 73e127ab0aab1e91439f1014fe45d578e090fc3a Mon Sep 17 00:00:00 2001 From: shrapp <93125928+shrapp@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:11:00 +0300 Subject: [PATCH 03/19] Update pyquil/noise.py Co-authored-by: Marquess Valdez --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 4f9e48b13..4ba0512f1 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -867,7 +867,7 @@ class Calibrations: readout args: qc (QuantumComputer, optional): a Quantum Computer (Aspen-M-2 or Aspen-M-3). - Defaults to None, where the user can define his own calibration data. + 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. From 4b1810c0bc68472016741ca48cad48fa98a3fed4 Mon Sep 17 00:00:00 2001 From: shrapp <93125928+shrapp@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:12:26 +0300 Subject: [PATCH 04/19] Update pyquil/noise.py Co-authored-by: Marquess Valdez --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 4ba0512f1..88c71af17 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -891,7 +891,7 @@ def __init__(self, qc: Optional[QuantumComputer] = None, cal=None): return if qc is None: - return # user can set his own values + return # user can set their own values else: qc_name = get_qc_name(qc) From 708ff9ba6daeaa6754236496342ff4ee2d845753 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 17:19:54 +0300 Subject: [PATCH 05/19] fix to use isa instead of http for calibration --- pyquil/_parser/parser.py | 2 +- pyquil/api/_qpu.py | 1 - pyquil/experiment/_result.py | 1 - pyquil/noise.py | 2173 +++++++++++++++++---------------- pyquil/operator_estimation.py | 1 - pyquil/paulis.py | 1 - pyquil/quilbase.py | 1 - pyquil/quiltwaveforms.py | 1 - 8 files changed, 1104 insertions(+), 1077 deletions(-) diff --git a/pyquil/_parser/parser.py b/pyquil/_parser/parser.py index c8c9a82c5..ea79b8473 100644 --- a/pyquil/_parser/parser.py +++ b/pyquil/_parser/parser.py @@ -142,7 +142,7 @@ def def_frame(self, frame, *specs): } options = {} - for (spec_name, spec_value) in specs: + for spec_name, spec_value in specs: name = names.get(spec_name, None) if name: options[name] = json.loads(str(spec_value)) 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 88c71af17..477614f8f 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -19,7 +19,7 @@ import json import sys from collections import namedtuple -from typing import Dict, List, Sequence, Optional, Any, Tuple, Set, Iterable, TYPE_CHECKING, Union, cast +from typing import Dict, List, Sequence, Optional, Any, Tuple, Set, Iterable, TYPE_CHECKING, Union, cast, Self import numpy as np import requests @@ -28,12 +28,14 @@ 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.quantum_processor.qcs import get_qcs_quantum_processor from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits +from pyquil.quantum_processor import QCSQuantumProcessor if TYPE_CHECKING: - from pyquil.quil import Program - from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + from pyquil.quil import Program + from pyquil.api import QuantumComputer as PyquilApiQuantumComputer INFINITY = float("inf") "Used for infinite coherence times." @@ -42,290 +44,290 @@ class KrausModel(_KrausModel): - """ - Encapsulate a single gate's noise model. - - :ivar str gate: The name of the gate. - :ivar Sequence[float] params: Optional parameters for the gate. - :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. - """ - - @staticmethod - 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. - :return: A complex square numpy array representing the Kraus operator. - """ - m = np.asarray(m, dtype=complex) - if m.ndim == 3: - m = m[0] + 1j * m[1] - if not m.ndim == 2: # pragma no coverage - raise ValueError("Need 2d array.") - if not m.shape[0] == m.shape[1]: # pragma no coverage - raise ValueError("Need square matrix.") - return m - - def to_dict(self) -> Dict[str, Any]: - """ - Create a dictionary representation of a KrausModel. - - 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 - } - - :return: A JSON compatible dictionary representation. - :rtype: Dict[str,Any] - """ - res = self._asdict() - res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops] - return res - - @staticmethod - 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. - :return: The deserialized KrausModel. - """ - kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] - return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"]) - - def __eq__(self, other: object) -> bool: - return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() - - def __neq__(self, other: object) -> bool: - return not self.__eq__(other) + """ + Encapsulate a single gate's noise model. + + :ivar str gate: The name of the gate. + :ivar Sequence[float] params: Optional parameters for the gate. + :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. + """ + + @staticmethod + 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. + :return: A complex square numpy array representing the Kraus operator. + """ + m = np.asarray(m, dtype=complex) + if m.ndim == 3: + m = m[0] + 1j * m[1] + if not m.ndim == 2: # pragma no coverage + raise ValueError("Need 2d array.") + if not m.shape[0] == m.shape[1]: # pragma no coverage + raise ValueError("Need square matrix.") + return m + + def to_dict(self) -> Dict[str, Any]: + """ + Create a dictionary representation of a KrausModel. + + 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 + } + + :return: A JSON compatible dictionary representation. + :rtype: Dict[str,Any] + """ + res = self._asdict() + res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops] + return res + + @staticmethod + 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. + :return: The deserialized KrausModel. + """ + kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] + return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"]) + + def __eq__(self, other: object) -> bool: + return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() + + def __neq__(self, other: object) -> bool: + return not self.__eq__(other) _NoiseModel = namedtuple("_NoiseModel", ["gates", "assignment_probs"]) class NoiseModel(_NoiseModel): - """ - Encapsulate the QPU noise model containing information about the noisy gates. - - :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. - """ - - def to_dict(self) -> Dict[str, Any]: - """ - Create a JSON serializable representation of the noise model. - - For example:: - - { - "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. - """ - return { - "gates": [km.to_dict() for km in self.gates], - "assignment_probs": {str(qid): a.tolist() for qid, a in self.assignment_probs.items()}, - } - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "NoiseModel": - """ - Re-create the noise model from a dictionary representation. - - :param d: The dictionary representation. - :return: The restored noise model. - """ - return NoiseModel( - gates=[KrausModel.from_dict(t) for t in d["gates"]], - assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, - ) - - def gates_by_name(self, name: str) -> List[KrausModel]: - """ - Return all defined noisy gates of a particular gate name. - - :param str name: The gate name. - :return: A list of noise models representing that gate. - """ - return [g for g in self.gates if g.gate == name] - - def __eq__(self, other: object) -> bool: - return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict() - - def __neq__(self, other: object) -> bool: - return not self.__eq__(other) + """ + Encapsulate the QPU noise model containing information about the noisy gates. + + :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. + """ + + def to_dict(self) -> Dict[str, Any]: + """ + Create a JSON serializable representation of the noise model. + + For example:: + + { + "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. + """ + return { + "gates": [km.to_dict() for km in self.gates], + "assignment_probs": {str(qid): a.tolist() for qid, a in self.assignment_probs.items()}, + } + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "NoiseModel": + """ + Re-create the noise model from a dictionary representation. + + :param d: The dictionary representation. + :return: The restored noise model. + """ + return NoiseModel( + gates=[KrausModel.from_dict(t) for t in d["gates"]], + assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, + ) + + def gates_by_name(self, name: str) -> List[KrausModel]: + """ + Return all defined noisy gates of a particular gate name. + + :param str name: The gate name. + :return: A list of noise models representing that gate. + """ + return [g for g in self.gates if g.gate == name] + + def __eq__(self, other: object) -> bool: + return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict() + + def __neq__(self, other: object) -> bool: + return not self.__eq__(other) def _check_kraus_ops(n: int, kraus_ops: Sequence[np.ndarray]) -> None: - """ - Verify that the Kraus operators are of the correct shape and satisfy the correct normalization. + """ + Verify that the Kraus operators are of the correct shape and satisfy the correct normalization. - :param n: Number of qubits - :param kraus_ops: The Kraus operators as numpy.ndarrays. - """ - for k in kraus_ops: - if not np.shape(k) == (2 ** n, 2 ** n): - raise ValueError("Kraus operators for {0} qubits must have shape {1}x{1}: {2}".format(n, 2 ** n, k)) + :param n: Number of qubits + :param kraus_ops: The Kraus operators as numpy.ndarrays. + """ + for k in kraus_ops: + if not np.shape(k) == (2**n, 2**n): + raise ValueError("Kraus operators for {0} qubits must have shape {1}x{1}: {2}".format(n, 2**n, k)) - kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops) - if not np.allclose(kdk_sum, np.eye(2 ** n), atol=1e-3): - raise ValueError("Kraus operator not correctly normalized: sum_j K_j^*K_j == {}".format(kdk_sum)) + kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops) + if not np.allclose(kdk_sum, np.eye(2**n), atol=1e-3): + raise ValueError("Kraus operator not correctly normalized: sum_j K_j^*K_j == {}".format(kdk_sum)) def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[np.ndarray]) -> List[Pragma]: - """ - Generate the pragmas to define a Kraus map for a specific gate on some qubits. - - :param name: The name of the gate. - :param qubit_indices: The qubits - :param kraus_ops: The Kraus operators as matrices. - :return: A QUIL string with PRAGMA ADD-KRAUS ... statements. - """ - - pragmas = [ - Pragma( - "ADD-KRAUS", - (name,) + tuple(qubit_indices), - "({})".format(" ".join(map(format_parameter, np.ravel(k)))), - ) - for k in kraus_ops - ] - return pragmas + """ + Generate the pragmas to define a Kraus map for a specific gate on some qubits. + + :param name: The name of the gate. + :param qubit_indices: The qubits + :param kraus_ops: The Kraus operators as matrices. + :return: A QUIL string with PRAGMA ADD-KRAUS ... statements. + """ + + pragmas = [ + Pragma( + "ADD-KRAUS", + (name,) + tuple(qubit_indices), + "({})".format(" ".join(map(format_parameter, np.ravel(k)))), + ) + for k in kraus_ops + ] + return pragmas def append_kraus_to_gate( - kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray + kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray ) -> List[Union[np.number, np.ndarray]]: - """ - Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. + """ + Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. - :param kraus_ops: The Kraus operators. - :param gate_matrix: The unitary gate. - :return: A list of transformed Kraus operators. - """ - return [kj.dot(gate_matrix) for kj in kraus_ops] + :param kraus_ops: The Kraus operators. + :param gate_matrix: The unitary gate. + :return: A list of transformed Kraus operators. + """ + return [kj.dot(gate_matrix) for kj in kraus_ops] def pauli_kraus_map(probabilities: Sequence[float]) -> List[np.ndarray]: - r""" - Generate the Kraus operators corresponding to a pauli channel. + r""" + 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. + :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. - 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. - """ - if len(probabilities) not in [4, 16]: - raise ValueError( - "Currently we only support one or two qubits, " - "so the provided list of probabilities must have length 4 or 16." - ) - if not np.allclose(sum(probabilities), 1.0, atol=1e-3): - raise ValueError("Probabilities must sum to one.") + :return: A list of the 4^num_qubits Kraus operators that parametrize the map. + """ + if len(probabilities) not in [4, 16]: + raise ValueError( + "Currently we only support one or two qubits, " + "so the provided list of probabilities must have length 4 or 16." + ) + if not np.allclose(sum(probabilities), 1.0, atol=1e-3): + raise ValueError("Probabilities must sum to one.") - paulis = [ - np.eye(2), - np.array([[0, 1], [1, 0]]), - np.array([[0, -1j], [1j, 0]]), - np.array([[1, 0], [0, -1]]), - ] + paulis = [ + np.eye(2), + np.array([[0, 1], [1, 0]]), + np.array([[0, -1j], [1j, 0]]), + np.array([[1, 0], [0, -1]]), + ] - if len(probabilities) == 4: - operators = paulis - else: - operators = np.kron(paulis, paulis) # type: ignore + if len(probabilities) == 4: + operators = paulis + else: + operators = np.kron(paulis, paulis) # type: ignore - return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] + return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] def damping_kraus_map(p: float = 0.10) -> List[np.ndarray]: - """ - Generate the Kraus operators corresponding to an amplitude damping - noise channel. + """ + Generate the Kraus operators corresponding to an amplitude damping + noise channel. - :param p: The one-step damping probability. - :return: A list [k1, k2] of the Kraus operators that parametrize the map. - :rtype: list - """ - damping_op = np.sqrt(p) * np.array([[0, 1], [0, 0]]) + :param p: The one-step damping probability. + :return: A list [k1, k2] of the Kraus operators that parametrize the map. + :rtype: list + """ + damping_op = np.sqrt(p) * np.array([[0, 1], [0, 0]]) - residual_kraus = np.diag([1, np.sqrt(1 - p)]) # type: ignore - return [residual_kraus, damping_op] + residual_kraus = np.diag([1, np.sqrt(1 - p)]) # type: ignore + return [residual_kraus, damping_op] def dephasing_kraus_map(p: float = 0.10) -> List[np.ndarray]: - """ - Generate the Kraus operators corresponding to a dephasing channel. + """ + Generate the Kraus operators corresponding to a dephasing channel. - :params float p: The one-step dephasing probability. - :return: A list [k1, k2] of the Kraus operators that parametrize the map. - :rtype: list - """ - return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])] # type: ignore + :params float p: The one-step dephasing probability. + :return: A list [k1, k2] of the Kraus operators that parametrize the map. + :rtype: list + """ + return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])] # type: ignore def tensor_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.ndarray]: - """ - Generate the Kraus map corresponding to the composition - of two maps on different qubits. + """ + Generate the Kraus map corresponding to the composition + of two maps on different qubits. - :param k1: The Kraus operators for the first qubit. - :param k2: The Kraus operators for the second qubit. - :return: A list of tensored Kraus operators. - """ - return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore + :param k1: The Kraus operators for the first qubit. + :param k2: The Kraus operators for the second qubit. + :return: A list of tensored Kraus operators. + """ + return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore def combine_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.ndarray]: - """ - Generate the Kraus map corresponding to the composition - of two maps on the same qubits with k1 being applied to the state - after k2. + """ + Generate the Kraus map corresponding to the composition + of two maps on the same qubits with k1 being applied to the state + after k2. - :param k1: The list of Kraus operators that are applied second. - :param k2: The list of Kraus operators that are applied first. - :return: A combinatorially generated list of composed Kraus operators. - """ - return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore + :param k1: The list of Kraus operators that are applied second. + :param k2: The list of Kraus operators that are applied first. + :return: A combinatorially generated list of composed Kraus operators. + """ + return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2] # type: ignore def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.ndarray]: - """ + """ Generate the Kraus map corresponding to the composition of a dephasing channel followed by an amplitude damping channel. @@ -334,24 +336,24 @@ def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.n :param gate_time: The gate duration. :return: A list of Kraus operators. """ - assert T1 >= 0 - assert T2 >= 0 - - if T1 != INFINITY: - 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: - 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)] - return combine_kraus_maps(damping, dephasing) + assert T1 >= 0 + assert T2 >= 0 + + if T1 != INFINITY: + 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: + 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)] + return combine_kraus_maps(damping, dephasing) # You can only apply gate-noise to non-parametrized gates or parametrized gates at fixed parameters. @@ -360,470 +362,469 @@ def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.n class NoisyGateUndefined(Exception): - """Raise when user attempts to use noisy gate outside of currently supported set.""" + """Raise when user attempts to use noisy gate outside of currently supported set.""" - pass + pass def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> Tuple[np.ndarray, str]: - """ - Look up the numerical gate representation and a proposed 'noisy' name. - - :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. - """ - params = tuple(params) - if gate_name == "I": - assert params == () - return np.eye(2), "NOISY-I" - if gate_name == "RX": - (angle,) = params - if not isinstance(angle, (int, float, complex)): - raise TypeError(f"Cannot produce noisy gate for parameter of type {type(angle)}") - - if np.isclose(angle, np.pi / 2, atol=ANGLE_TOLERANCE): - return np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), "NOISY-RX-PLUS-90" - elif np.isclose(angle, -np.pi / 2, atol=ANGLE_TOLERANCE): - return np.array([[1, 1j], [1j, 1]]) / np.sqrt(2), "NOISY-RX-MINUS-90" - elif np.isclose(angle, np.pi, atol=ANGLE_TOLERANCE): - return np.array([[0, -1j], [-1j, 0]]), "NOISY-RX-PLUS-180" - elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE): - return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180" - elif gate_name == "CZ": - assert params == () - return np.diag([1, 1, 1, -1]), "NOISY-CZ" # type: ignore - - raise NoisyGateUndefined( - "Undefined gate and params: {}{}\n" - "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ".format(gate_name, params) - ) + """ + Look up the numerical gate representation and a proposed 'noisy' name. + + :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. + """ + params = tuple(params) + if gate_name == "I": + assert params == () + return np.eye(2), "NOISY-I" + if gate_name == "RX": + (angle,) = params + if not isinstance(angle, (int, float, complex)): + raise TypeError(f"Cannot produce noisy gate for parameter of type {type(angle)}") + + if np.isclose(angle, np.pi / 2, atol=ANGLE_TOLERANCE): + return np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), "NOISY-RX-PLUS-90" + elif np.isclose(angle, -np.pi / 2, atol=ANGLE_TOLERANCE): + return np.array([[1, 1j], [1j, 1]]) / np.sqrt(2), "NOISY-RX-MINUS-90" + elif np.isclose(angle, np.pi, atol=ANGLE_TOLERANCE): + return np.array([[0, -1j], [-1j, 0]]), "NOISY-RX-PLUS-180" + elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE): + return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180" + elif gate_name == "CZ": + assert params == () + return np.diag([1, 1, 1, -1]), "NOISY-CZ" # type: ignore + + raise NoisyGateUndefined( + "Undefined gate and params: {}{}\n" + "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ".format(gate_name, params) + ) def _get_program_gates(prog: "Program") -> List[Gate]: - """ - Get all gate applications appearing in prog. + """ + Get all gate applications appearing in prog. - :param prog: The program - :return: A list of all Gates in prog (without duplicates). - """ - return sorted({i for i in prog if isinstance(i, Gate)}, key=lambda g: g.out()) + :param prog: The program + :return: A list of all Gates in prog (without duplicates). + """ + return sorted({i for i in prog if isinstance(i, Gate)}, key=lambda g: g.out()) def _decoherence_noise_model( - gates: Sequence[Gate], - T1: Union[Dict[int, float], float] = 30e-6, - T2: Union[Dict[int, float], float] = 30e-6, - gate_time_1q: float = 50e-9, - gate_time_2q: float = 150e-09, - ro_fidelity: Union[Dict[int, float], float] = 0.95, + gates: Sequence[Gate], + T1: Union[Dict[int, float], float] = 30e-6, + T2: Union[Dict[int, float], float] = 30e-6, + gate_time_1q: float = 50e-9, + gate_time_2q: float = 150e-09, + ro_fidelity: Union[Dict[int, float], float] = 0.95, ) -> NoiseModel: - """ - The default noise parameters - - - T1 = 30 us - - T2 = 30 us - - 1q gate time = 50 ns - - 2q gate time = 150 ns - - are currently typical for near-term devices. - - This function will define new gates and add Kraus noise to these gates. It will translate - the input program to use the noisy version of the gates. - - :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. - :param T2: The T2 dephasing time either globally or in a - 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. - :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - 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. - :return: A NoiseModel with the appropriate Kraus operators defined. - """ - all_qubits = set(sum(([t.index for t in g.qubits] for g in gates), [])) - if isinstance(T1, dict): - all_qubits.update(T1.keys()) - if isinstance(T2, dict): - all_qubits.update(T2.keys()) - if isinstance(ro_fidelity, dict): - all_qubits.update(ro_fidelity.keys()) - - if not isinstance(T1, dict): - T1 = {q: T1 for q in all_qubits} - - if not isinstance(T2, dict): - T2 = {q: T2 for q in all_qubits} - - if not isinstance(ro_fidelity, dict): - ro_fidelity = {q: ro_fidelity for q in all_qubits} - - noisy_identities_1q = { - q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_1q) for q in all_qubits - } - noisy_identities_2q = { - q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_2q) for q in all_qubits - } - kraus_maps = [] - for g in gates: - targets = tuple(t.index for t in g.qubits) - if g.name in NO_NOISE: - continue - matrix, _ = get_noisy_gate(g.name, g.params) - - if len(targets) == 1: - noisy_I = noisy_identities_1q[targets[0]] - else: - if len(targets) != 2: - raise ValueError("Noisy gates on more than 2Q not currently supported") - - # note this ordering of the tensor factors is necessary due to how the QVM orders - # the wavefunction basis - noisy_I = tensor_kraus_maps(noisy_identities_2q[targets[1]], noisy_identities_2q[targets[0]]) - kraus_maps.append( - KrausModel( - g.name, - tuple(g.params), - targets, - combine_kraus_maps(noisy_I, [matrix]), - # FIXME (Nik): compute actual avg gate fidelity for this simple - # noise model - 1.0, - ) - ) - aprobs = {} - for q, f_ro in ro_fidelity.items(): - aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) - - return NoiseModel(kraus_maps, aprobs) + """ + The default noise parameters + + - T1 = 30 us + - T2 = 30 us + - 1q gate time = 50 ns + - 2q gate time = 150 ns + + are currently typical for near-term devices. + + This function will define new gates and add Kraus noise to these gates. It will translate + the input program to use the noisy version of the gates. + + :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. + :param T2: The T2 dephasing time either globally or in a + 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. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. + 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. + :return: A NoiseModel with the appropriate Kraus operators defined. + """ + all_qubits = set(sum(([t.index for t in g.qubits] for g in gates), [])) + if isinstance(T1, dict): + all_qubits.update(T1.keys()) + if isinstance(T2, dict): + all_qubits.update(T2.keys()) + if isinstance(ro_fidelity, dict): + all_qubits.update(ro_fidelity.keys()) + + if not isinstance(T1, dict): + T1 = {q: T1 for q in all_qubits} + + if not isinstance(T2, dict): + T2 = {q: T2 for q in all_qubits} + + if not isinstance(ro_fidelity, dict): + ro_fidelity = {q: ro_fidelity for q in all_qubits} + + noisy_identities_1q = { + q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_1q) for q in all_qubits + } + noisy_identities_2q = { + q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_2q) for q in all_qubits + } + kraus_maps = [] + for g in gates: + targets = tuple(t.index for t in g.qubits) + if g.name in NO_NOISE: + continue + matrix, _ = get_noisy_gate(g.name, g.params) + + if len(targets) == 1: + noisy_I = noisy_identities_1q[targets[0]] + else: + if len(targets) != 2: + raise ValueError("Noisy gates on more than 2Q not currently supported") + + # note this ordering of the tensor factors is necessary due to how the QVM orders + # the wavefunction basis + noisy_I = tensor_kraus_maps(noisy_identities_2q[targets[1]], noisy_identities_2q[targets[0]]) + kraus_maps.append( + KrausModel( + g.name, + tuple(g.params), + targets, + combine_kraus_maps(noisy_I, [matrix]), + # FIXME (Nik): compute actual avg gate fidelity for this simple + # noise model + 1.0, + ) + ) + aprobs = {} + for q, f_ro in ro_fidelity.items(): + aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) + + return NoiseModel(kraus_maps, aprobs) def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p11: float = 0.911) -> NoiseModel: - """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. + """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. - For simplicity, we use the default values for T1, T2, gate times, et al. and only allow - the specification of readout fidelities. - """ - gates = _get_qvm_noise_supported_gates(isa) - noise_model = _decoherence_noise_model(gates) - aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) - aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} - return NoiseModel(noise_model.gates, aprobs) + For simplicity, we use the default values for T1, T2, gate times, et al. and only allow + the specification of readout fidelities. + """ + gates = _get_qvm_noise_supported_gates(isa) + noise_model = _decoherence_noise_model(gates) + aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) + aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} + return NoiseModel(noise_model.gates, aprobs) 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. - - :param noise_model: The assumed noise model. - :return: A quil Program with the noise pragmas. - """ - from pyquil.quil import 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)) - - # if ideal version of gate has not yet been DEFGATE'd, do this - if new_name not in defgates: - p.defgate(new_name, ideal_gate) - defgates.add(new_name) - except NoisyGateUndefined: - print( - "WARNING: Could not find ideal gate definition for gate {}".format(k.gate), - file=sys.stderr, - ) - new_name = k.gate - - # define noisy version of gate on specific targets - p.define_noisy_gate(new_name, k.targets, k.kraus_ops) - - # define noisy readouts - for q, ap in noise_model.assignment_probs.items(): - p.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) - return p + """ + 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. + + :param noise_model: The assumed noise model. + :return: A quil Program with the noise pragmas. + """ + from pyquil.quil import 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)) + + # if ideal version of gate has not yet been DEFGATE'd, do this + if new_name not in defgates: + p.defgate(new_name, ideal_gate) + defgates.add(new_name) + except NoisyGateUndefined: + print( + "WARNING: Could not find ideal gate definition for gate {}".format(k.gate), + file=sys.stderr, + ) + new_name = k.gate + + # define noisy version of gate on specific targets + p.define_noisy_gate(new_name, k.targets, k.kraus_ops) + + # define noisy readouts + for q, ap in noise_model.assignment_probs.items(): + p.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) + return p def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": - """ - Apply a noise model to a program and generated a 'noisy-fied' version of the program. - - :param prog: A Quil Program object. - :param noise_model: A NoiseModel, either generated from an ISA or - from a simple decoherence model. - :return: A new program translated to a noisy gateset and with noisy readout as described by the - noisemodel. - """ - new_prog = _noise_model_program_header(noise_model) - for i in prog: - if isinstance(i, Gate) and noise_model.gates: - try: - _, new_name = get_noisy_gate(i.name, tuple(i.params)) - new_prog += Gate(new_name, [], i.qubits) - except NoisyGateUndefined: - new_prog += i - else: - new_prog += i - return prog.copy_everything_except_instructions() + new_prog + """ + Apply a noise model to a program and generated a 'noisy-fied' version of the program. + + :param prog: A Quil Program object. + :param noise_model: A NoiseModel, either generated from an ISA or + from a simple decoherence model. + :return: A new program translated to a noisy gateset and with noisy readout as described by the + noisemodel. + """ + new_prog = _noise_model_program_header(noise_model) + for i in prog: + if isinstance(i, Gate) and noise_model.gates: + try: + _, new_name = get_noisy_gate(i.name, tuple(i.params)) + new_prog += Gate(new_name, [], i.qubits) + except NoisyGateUndefined: + new_prog += i + else: + new_prog += i + return prog.copy_everything_except_instructions() + new_prog def add_decoherence_noise( - prog: "Program", - T1: Union[Dict[int, float], float] = 30e-6, - T2: Union[Dict[int, float], float] = 30e-6, - gate_time_1q: float = 50e-9, - gate_time_2q: float = 150e-09, - ro_fidelity: Union[Dict[int, float], float] = 0.95, + prog: "Program", + T1: Union[Dict[int, float], float] = 30e-6, + T2: Union[Dict[int, float], float] = 30e-6, + gate_time_1q: float = 50e-9, + gate_time_2q: float = 150e-09, + ro_fidelity: Union[Dict[int, float], float] = 0.95, ) -> "Program": - """ - Add generic damping and dephasing noise to a program. - - This high-level function is provided as a convenience to investigate the effects of a - generic noise model on a program. For more fine-grained control, please investigate - the other methods available in the ``pyquil.noise`` module. - - In an attempt to closely model the QPU, noisy versions of RX(+-pi/2) and CZ are provided; - I and parametric RZ are noiseless, and other gates are not allowed. To use this function, - you need to compile your program to this native gate set. - - The default noise parameters - - - T1 = 30 us - - T2 = 30 us - - 1q gate time = 50 ns - - 2q gate time = 150 ns - - are currently typical for near-term devices. - - This function will define new gates and add Kraus noise to these gates. It will translate - the input program to use the noisy version of the gates. - - :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. - :param T2: The T2 dephasing time either globally or in a - 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. - :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - 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. - :return: A new program with noisy operators. - """ - gates = _get_program_gates(prog) - noise_model = _decoherence_noise_model( - gates, - T1=T1, - T2=T2, - gate_time_1q=gate_time_1q, - gate_time_2q=gate_time_2q, - ro_fidelity=ro_fidelity, - ) - return apply_noise_model(prog, noise_model) + """ + Add generic damping and dephasing noise to a program. + + This high-level function is provided as a convenience to investigate the effects of a + generic noise model on a program. For more fine-grained control, please investigate + the other methods available in the ``pyquil.noise`` module. + + In an attempt to closely model the QPU, noisy versions of RX(+-pi/2) and CZ are provided; + I and parametric RZ are noiseless, and other gates are not allowed. To use this function, + you need to compile your program to this native gate set. + + The default noise parameters + + - T1 = 30 us + - T2 = 30 us + - 1q gate time = 50 ns + - 2q gate time = 150 ns + + are currently typical for near-term devices. + + This function will define new gates and add Kraus noise to these gates. It will translate + the input program to use the noisy version of the gates. + + :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. + :param T2: The T2 dephasing time either globally or in a + 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. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. + 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. + :return: A new program with noisy operators. + """ + gates = _get_program_gates(prog) + noise_model = _decoherence_noise_model( + gates, + T1=T1, + T2=T2, + gate_time_1q=gate_time_1q, + gate_time_2q=gate_time_2q, + ro_fidelity=ro_fidelity, + ) + return apply_noise_model(prog, noise_model) def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray: - """ - Ensure that an array ``p`` with bitstring probabilities has a separate axis for each qubit such - that ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + """ + Ensure that an array ``p`` with bitstring probabilities has a separate axis for each qubit such + that ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - This should not allocate much memory if ``p`` is already in ``C``-contiguous order (row-major). + 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. - :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. - """ - p = np.asarray(p, order="C") - num_qubits = int(round(np.log2(p.size))) - return p.reshape((2,) * num_qubits) + :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. + :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. + """ + p = np.asarray(p, order="C") + num_qubits = int(round(np.log2(p.size))) + return p.reshape((2,) * num_qubits) def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray: - """ - Given an array of single shot results estimate the probability distribution over all bitstrings. + """ + 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. - :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``. - """ - nshots, nq = np.shape(results) - outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) - probs = np.histogram(outcomes, bins=np.arange(-0.5, 2 ** nq, 1))[0] / float(nshots) # type: ignore - return _bitstring_probs_by_qubit(probs) + :param results: A 2d array where the outer axis iterates over shots + 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``. + """ + nshots, nq = np.shape(results) + outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) + probs = np.histogram(outcomes, bins=np.arange(-0.5, 2**nq, 1))[0] / float(nshots) # type: ignore + return _bitstring_probs_by_qubit(probs) _CHARS = "klmnopqrstuvwxyzabcdefgh0123456789" def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarray: - """ - Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits) - and a list of assignment probability matrices (one for each bit in the readout, ordered like - the inner axis of results) apply local 2x2 matrices to each bit index. + """ + Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits) + and a list of assignment probability matrices (one for each bit in the readout, ordered like + the inner axis of results) apply local 2x2 matrices to each bit index. - :param p: An array that enumerates a function indexed by - bitstrings:: + :param p: An array that enumerates a function indexed by + 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:: + :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:: - 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 - for idx, trafo_idx in enumerate(ts): - # this contraction pattern looks like - # 'ij,abcd...jklm...->abcd...iklm...' so it properly applies a "local" - # transformation to a single tensor-index without changing the order of - # indices - einsum_pat = ( - "ij," + _CHARS[:idx] + "j" + _CHARS[idx: nq - 1] + "->" + _CHARS[:idx] + "i" + _CHARS[idx: nq - 1] - ) - p_corrected = np.einsum(einsum_pat, trafo_idx, p_corrected) + 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 + for idx, trafo_idx in enumerate(ts): + # this contraction pattern looks like + # 'ij,abcd...jklm...->abcd...iklm...' so it properly applies a "local" + # transformation to a single tensor-index without changing the order of + # indices + einsum_pat = ( + "ij," + _CHARS[:idx] + "j" + _CHARS[idx : nq - 1] + "->" + _CHARS[:idx] + "i" + _CHARS[idx : nq - 1] + ) + p_corrected = np.einsum(einsum_pat, trafo_idx, p_corrected) - return p_corrected + return p_corrected def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.ndarray]) -> np.ndarray: - """ - Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis - over bits) and a list of assignment probability matrices (one for each bit in the readout, - 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``. - :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: - - [[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``. - """ - return _apply_local_transforms(p, assignment_probabilities) + """ + Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis + over bits) and a list of assignment probability matrices (one for each bit in the readout, + 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``. + :param assignment_probabilities: A list of assignment probability matrices + per qubit. Each assignment probability matrix is expected to be of the form:: + + [[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``. + """ + return _apply_local_transforms(p, assignment_probabilities) def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.ndarray]) -> np.ndarray: - """ - Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner - axis over bits) and a list of assignment probability matrices (one for each bit in the readout) - 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``. - :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: - - [[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``. - """ - return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) # type: ignore + """ + Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner + axis over bits) and a list of assignment probability matrices (one for each bit in the readout) + 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``. + :param assignment_probabilities: A list of assignment probability matrices + per qubit. Each assignment probability matrix is expected to be of the form:: + + [[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``. + """ + return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) # type: ignore def bitstring_probs_to_z_moments(p: np.ndarray) -> np.ndarray: - """ - Convert between bitstring probabilities and joint Z moment expectations. + """ + 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``. - :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:: + :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``. + :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:: - = 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))) + = 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))) def estimate_assignment_probs( - q: int, - trials: int, - qc: "PyquilApiQuantumComputer", - p0: Optional["Program"] = None, + q: int, + trials: int, + qc: "PyquilApiQuantumComputer", + p0: Optional["Program"] = None, ) -> np.ndarray: - """ - Estimate the readout assignment probabilities for a given qubit ``q``. - The returned matrix is of the form:: - - [[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. - :return: The assignment probability matrix - """ - from pyquil.quil import Program - - if p0 is None: # pragma no coverage - p0 = Program() - - p_i = ( - p0 - + Program( - Declare("ro", "BIT", 1), - I(q), - MEASURE(q, MemoryReference("ro", 0)), - ) - ).wrap_in_numshots_loop(trials) - results_i = np.sum(_run(qc, p_i)) - - p_x = ( - p0 - + Program( - Declare("ro", "BIT", 1), - RX(np.pi, q), - MEASURE(q, MemoryReference("ro", 0)), - ) - ).wrap_in_numshots_loop(trials) - results_x = np.sum(_run(qc, p_x)) - - p00 = 1.0 - results_i / float(trials) - p11 = results_x / float(trials) - return np.array([[p00, 1 - p11], [1 - p00, p11]]) + """ + Estimate the readout assignment probabilities for a given qubit ``q``. + The returned matrix is of the form:: + + [[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. + :return: The assignment probability matrix + """ + from pyquil.quil import Program + + if p0 is None: # pragma no coverage + p0 = Program() + + p_i = ( + p0 + + Program( + Declare("ro", "BIT", 1), + I(q), + MEASURE(q, MemoryReference("ro", 0)), + ) + ).wrap_in_numshots_loop(trials) + results_i = np.sum(_run(qc, p_i)) + + p_x = ( + p0 + + Program( + Declare("ro", "BIT", 1), + RX(np.pi, q), + MEASURE(q, MemoryReference("ro", 0)), + ) + ).wrap_in_numshots_loop(trials) + results_x = np.sum(_run(qc, p_x)) + + p00 = 1.0 - results_i / float(trials) + p11 = results_x / float(trials) + return np.array([[p00, 1 - p11], [1 - p00, p11]]) def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> List[List[int]]: - result = qc.run(qc.compiler.native_quil_to_executable(program)) - bitstrings = result.readout_data.get("ro") - assert bitstrings is not None - return cast(List[List[int]], bitstrings.tolist()) + result = qc.run(qc.compiler.native_quil_to_executable(program)) + 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" @@ -833,130 +834,137 @@ def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> List[List[int]]: No_Depolarizing = "No_Depolarizing" -def get_qc_name(qc: QuantumComputer): - """ - returns the name of the quantum computer `qc`, - without the ending 'qvm' if it exists. - """ - name = qc.name - if name[-4:] == "-qvm": - name = name[0:-4] - return name - else: - return name +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 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(qpu: QCSQuantumProcessor) -> Dict[int, float]: + benchmarks = qpu._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(qpu: QCSQuantumProcessor) -> Dict[int, float]: + benchmarks = qpu._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(qpu: QCSQuantumProcessor) -> Dict[int, float]: + benchmarks = qpu._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(qpu: QCSQuantumProcessor) -> Dict[int, float]: + instructions = qpu._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 + 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, qc: Optional[QuantumComputer] = None, cal=None): - self.fidelities = {} - self.readout_fidelity = {} - self.T2 = {} - self.T1 = {} - self.two_q_gates = set() - - if cal is not None: - self.T1 = cal.T1.copy() - self.T2 = cal.T2.copy() - self.readout_fidelity = cal.readout_fidelity.copy() - self.fidelities = {} - for key, val in cal.fidelities.items(): - self.fidelities[key] = val.copy() - self.two_q_gates = cal.two_q_gates - return - - if qc is None: - return # user can set their own values - - else: - qc_name = get_qc_name(qc) - if qc_name not in ["Aspen-M-2", "Aspen-M-3"]: - raise ValueError("qc must be Aspen-M-2 or Aspen-M-3") - else: - url = "https://forest-server.qcs.rigetti.com/lattices/" - response = requests.get(url + qc_name) - file = json.loads(response.text) - self.calibrations = file["lattice"]["specs"] - self.two_q_gates = self._get_qc_2q_gates(file["lattice"]["isa"]["2Q"]) - self._create_1q_dicts() - self._create_2q_dicts() - - def _create_1q_dicts(self): - qs = self.calibrations['1Q'].keys() - t1 = [self.calibrations['1Q'][q]['T1'] for q in qs] - t2 = [self.calibrations['1Q'][q]['T2'] for q in qs] - fidelities = [self.calibrations['1Q'][q]["f1QRB"] for q in qs] - readout = [self.calibrations['1Q'][q]["fRO"] for q in qs] - qubits_indexes = [int(q) for q in qs] - self.T1 = dict(zip(qubits_indexes, t1)) - self.T2 = dict(zip(qubits_indexes, t2)) - self.fidelities[Depolarizing_1Q_gate] = dict(zip(qs, fidelities)) - self.readout_fidelity = dict(zip(qubits_indexes, readout)) - - def _create_2q_dicts(self): - pairs = self.calibrations['2Q'].keys() - for gate in self.two_q_gates: - fidelity = [self.calibrations['2Q'][pair].get("f" + gate, 1.0) for pair in pairs] - self.fidelities[gate] = dict(zip(pairs, fidelity)) - - def change_noise_intensity(self, intensity: float): - 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): - """ - 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 + 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, qc: Optional[QuantumComputer] = None, calibrations: Self = 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 qc is None: + return # user can set their own values + + else: + name = qc.name if "qvm" not in qc.name else qc.name[:-4] + qpu = get_qcs_quantum_processor(name) + self.T1 = get_t1s(qpu) + self.T2 = get_t2s(qpu) + self.fidelities[Depolarizing_1Q_gate] = get_1q_fidelities(qpu) + self.readout_fidelity = get_readout_fidelities(qpu) + self._create_2q_dicts(qpu) + + def _create_2q_dicts(self, qpu: QCSQuantumProcessor) -> None: + instructions = qpu._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, + 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. @@ -965,43 +973,41 @@ def create_damping_after_dephasing_kraus_maps( :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), [])) + 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]] + 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 + 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 + 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, + 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. @@ -1010,75 +1016,73 @@ def add_single_qubit_noise( :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 + 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], + 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 + # 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], + 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 = .95) -> List[np.ndarray]: - """ + """ + 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. @@ -1087,33 +1091,32 @@ def depolarizing_kraus(num_qubits: int, p: float = .95) -> List[np.ndarray]: :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) + 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 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. @@ -1122,176 +1125,192 @@ def add_delay_maps(prog: Program, delay_gates: Dict[str, float], T1: Dict[int, f :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 + 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`. + """ + 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() + :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 + 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 + """ + 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( - qc: QuantumComputer, - 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 + qc: QuantumComputer, + 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. @@ -1304,45 +1323,59 @@ def add_noise_to_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. + :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. """ - if convert_to_native: - p = qc.compiler.quil_to_native_quil(p) - - if calibrations is None: - calibrations = Calibrations(qc=qc) - - 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(cal=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 + if convert_to_native: + p = qc.compiler.quil_to_native_quil(p) + + if calibrations is None: + calibrations = Calibrations(qc=qc) + + 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/paulis.py b/pyquil/paulis.py index 5325c2e8c..bcb94795d 100644 --- a/pyquil/paulis.py +++ b/pyquil/paulis.py @@ -840,7 +840,6 @@ def commuting_sets(pauli_terms: PauliSum) -> List[List[PauliTerm]]: isAssigned_bool = False for p in range(m_s): # check if it commutes with each group if isAssigned_bool is False: - if check_commutation(groups[p], pauli_terms.terms[j]): isAssigned_bool = True groups[p].append(pauli_terms.terms[j]) diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py index 3b80a3b28..66fa84bcd 100644 --- a/pyquil/quilbase.py +++ b/pyquil/quilbase.py @@ -1225,7 +1225,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. """ From 9d405aad5a405099c8ac2f4bc3aef905a1a35e46 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 18:07:36 +0300 Subject: [PATCH 06/19] Self deleted --- pyquil/noise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 477614f8f..4b7813ea1 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -19,7 +19,7 @@ import json import sys from collections import namedtuple -from typing import Dict, List, Sequence, Optional, Any, Tuple, Set, Iterable, TYPE_CHECKING, Union, cast, Self +from typing import Dict, List, Sequence, Optional, Any, Tuple, Set, Iterable, TYPE_CHECKING, Union, cast import numpy as np import requests @@ -889,7 +889,7 @@ class Calibrations: this may change in time, and require changes in the class. """ - def __init__(self, qc: Optional[QuantumComputer] = None, calibrations: Self = None) -> None: + def __init__(self, qc: Optional[QuantumComputer] = None, calibrations = None) -> None: self.fidelities = {} self.readout_fidelity = {} self.T2 = {} From f92d6a457d46b379b4fba7a325bb17e4d8499ec2 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 18:16:53 +0300 Subject: [PATCH 07/19] imports fix --- pyquil/noise.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 4b7813ea1..da7a1a7f3 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -16,26 +16,24 @@ """ Module for creating and verifying noisy gate and readout definitions. """ -import json import sys from collections import namedtuple from typing import Dict, List, Sequence, Optional, Any, Tuple, Set, Iterable, TYPE_CHECKING, Union, cast import numpy as np -import requests -from pyquil.api import QuantumComputer 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.quantum_processor.qcs import get_qcs_quantum_processor from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits -from pyquil.quantum_processor import QCSQuantumProcessor +from pyquil.quantum_processor.qcs import get_qcs_quantum_processor if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + from pyquil.quantum_processor import QCSQuantumProcessor + INFINITY = float("inf") "Used for infinite coherence times." @@ -889,7 +887,7 @@ class Calibrations: this may change in time, and require changes in the class. """ - def __init__(self, qc: Optional[QuantumComputer] = None, calibrations = None) -> None: + def __init__(self, qc: Optional[PyquilApiQuantumComputer] = None, calibrations = None) -> None: self.fidelities = {} self.readout_fidelity = {} self.T2 = {} @@ -1299,7 +1297,7 @@ def add_kraus_maps_to_program( def add_noise_to_program( - qc: QuantumComputer, + qc: PyquilApiQuantumComputer, p: Program, convert_to_native: bool = True, calibrations: Optional[Calibrations] = None, From 50f52d7db46eda8bfd09d8866bc21bef5029c76f Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 18:20:49 +0300 Subject: [PATCH 08/19] import fix 2 --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index da7a1a7f3..d55cfb58a 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,12 +27,12 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits -from pyquil.quantum_processor.qcs import get_qcs_quantum_processor if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer from pyquil.quantum_processor import QCSQuantumProcessor + from pyquil.quantum_processor.qcs import get_qcs_quantum_processor INFINITY = float("inf") From 0a5c1725adf4e02ed6750594b5f9dc0a54304160 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 18:28:47 +0300 Subject: [PATCH 09/19] imports fix 3 --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index d55cfb58a..003fa98aa 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -32,7 +32,7 @@ from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer from pyquil.quantum_processor import QCSQuantumProcessor - from pyquil.quantum_processor.qcs import get_qcs_quantum_processor + INFINITY = float("inf") From 187397642de4ce95c71815e7a9e289f46e80619f Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 18:31:45 +0300 Subject: [PATCH 10/19] imports fix 4 --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 003fa98aa..1eabb07d7 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer - from pyquil.quantum_processor import QCSQuantumProcessor + from pyquil.quantum_processor import QCSQuantumProcessor, get_qcs_quantum_processor From 0666ca6bddbf73073f13401c43ca7c472761b417 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 18:37:53 +0300 Subject: [PATCH 11/19] imports fix 5 --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 1eabb07d7..40fad4c7b 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,11 +27,11 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits +from pyquil.quantum_processor import QCSQuantumProcessor, get_qcs_quantum_processor if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer - from pyquil.quantum_processor import QCSQuantumProcessor, get_qcs_quantum_processor From dd423f366b8d310ce835778e4b42663cf5a057c8 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 20:38:34 +0300 Subject: [PATCH 12/19] import fix 6 --- pyquil/noise.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 40fad4c7b..fb9ceae94 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,11 +27,12 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits -from pyquil.quantum_processor import QCSQuantumProcessor, get_qcs_quantum_processor +from pyquil.quantum_processor import get_qcs_quantum_processor if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + from pyquil.quantum_processor import QCSQuantumProcessor From bf5dff592a7a649e161d3ca4afd91d16d82fa074 Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 21:48:19 +0300 Subject: [PATCH 13/19] remove compile in add noise function --- pyquil/noise.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index fb9ceae94..806ebace9 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,7 +27,6 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits -from pyquil.quantum_processor import get_qcs_quantum_processor if TYPE_CHECKING: from pyquil.quil import Program @@ -888,7 +887,7 @@ class Calibrations: this may change in time, and require changes in the class. """ - def __init__(self, qc: Optional[PyquilApiQuantumComputer] = None, calibrations = None) -> None: + def __init__(self, qpu: Optional[PyquilApiQuantumComputer] = None, calibrations = None) -> None: self.fidelities = {} self.readout_fidelity = {} self.T2 = {} @@ -905,12 +904,10 @@ def __init__(self, qc: Optional[PyquilApiQuantumComputer] = None, calibrations = self.two_q_gates = calibrations.two_q_gates return - if qc is None: + if qpu is None: return # user can set their own values else: - name = qc.name if "qvm" not in qc.name else qc.name[:-4] - qpu = get_qcs_quantum_processor(name) self.T1 = get_t1s(qpu) self.T2 = get_t2s(qpu) self.fidelities[Depolarizing_1Q_gate] = get_1q_fidelities(qpu) @@ -1298,7 +1295,7 @@ def add_kraus_maps_to_program( def add_noise_to_program( - qc: PyquilApiQuantumComputer, + qcs_quantum_processor: QCSQuantumProcessor, p: Program, convert_to_native: bool = True, calibrations: Optional[Calibrations] = None, @@ -1315,10 +1312,8 @@ def add_noise_to_program( 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 qc: A Quantum computer object + :param qcs_quantum_processor: A QCSQuantumProcessor object. :param p: A pyquil program - :param convert_to_native: put `False` if the program is already in native pyquil or is not needed - - Note that it removes any delays. :param calibrations: optional, can get the calibrations in advance, instead of producing them from the URL. :param depolarizing: add depolarizing noise, default is True. @@ -1333,11 +1328,8 @@ def add_noise_to_program( :return: A new program with noisy operators. """ - if convert_to_native: - p = qc.compiler.quil_to_native_quil(p) - if calibrations is None: - calibrations = Calibrations(qc=qc) + calibrations = Calibrations(qpu=qcs_quantum_processor) new_p = Program() From 2a67108d27a7bf425b434176447ecde848b5d99e Mon Sep 17 00:00:00 2001 From: shrapp Date: Tue, 29 Aug 2023 23:44:48 +0300 Subject: [PATCH 14/19] doc noise example fix --- docs/source/noise.rst | 21 ++++++++++++--------- pyquil/noise.py | 5 ++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/source/noise.rst b/docs/source/noise.rst index 11e6a7eb8..d00ac21b8 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -1556,9 +1556,10 @@ After performing these operations, the resulting GHZ state is :math:`(\ket{000} 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(qc, qubits, numshots, noise=False, cal=None, intensity=1.0): + def ghz(qvm, qcs_qpu, qubits, numshots, noise=False, cal=None, intensity=1.0): p = Program() p.declare("ro", "BIT", 3) p += H(qubits[0]) @@ -1568,19 +1569,21 @@ After performing these operations, the resulting GHZ state is :math:`(\ket{000} 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(qc, p, calibrations=cal, noise_intensity=intensity) + p = add_noise_to_program(qcs_qpu, p, calibrations=cal, noise_intensity=intensity) else: - p = add_noise_to_program(qc, p, noise_intensity=intensity) + p = add_noise_to_program(qcs_qpu, p, noise_intensity=intensity) return p - def run_experiment(qpu, qubits, numshots): - qvm = get_qc(qpu, as_qvm=True, execution_timeout=1000) - cal = Calibrations(qvm) - no_noise = qvm.run(ghz(qvm,qubits,numshots, False)).readout_data.get("ro") - half_noise = qvm.run(ghz(qvm,qubits,numshots, True, cal, intensity=0.5)).readout_data.get("ro") - noisy = qvm.run(ghz(qvm,qubits,numshots, True, cal)).readout_data.get("ro") + 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) + cal = Calibrations(qcs_qpu) + no_noise = qvm.run(ghz(qvm,qcs_qpu,qubits,numshots, False)).readout_data.get("ro") + half_noise = qvm.run(ghz(qvm,qcs_qpu,qubits,numshots, True, cal, intensity=0.5)).readout_data.get("ro") + noisy = qvm.run(ghz(qvm,qcs_qpu,qubits,numshots, True, cal)).readout_data.get("ro") return no_noise, half_noise, noisy def plot_results(results, label): diff --git a/pyquil/noise.py b/pyquil/noise.py index 806ebace9..e64f9dc6a 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -32,8 +32,7 @@ from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer from pyquil.quantum_processor import QCSQuantumProcessor - - + INFINITY = float("inf") "Used for infinite coherence times." @@ -887,7 +886,7 @@ class Calibrations: this may change in time, and require changes in the class. """ - def __init__(self, qpu: Optional[PyquilApiQuantumComputer] = None, calibrations = None) -> None: + def __init__(self, qpu: Optional[QCSQuantumProcessor] = None, calibrations=None) -> None: self.fidelities = {} self.readout_fidelity = {} self.T2 = {} From e553ba4f4ff411e99f676e30b636dc3a73c7e50e Mon Sep 17 00:00:00 2001 From: shrapp Date: Wed, 30 Aug 2023 15:09:46 +0300 Subject: [PATCH 15/19] import fix 7 --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index e64f9dc6a..a07874cc2 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer - from pyquil.quantum_processor import QCSQuantumProcessor + from pyquil.quantum_processor.qcs import QCSQuantumProcessor INFINITY = float("inf") From ac4d1d744f72fa82964f463274d4ebd2c504eac9 Mon Sep 17 00:00:00 2001 From: shrapp Date: Wed, 30 Aug 2023 15:12:24 +0300 Subject: [PATCH 16/19] import fix 8 --- pyquil/noise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index a07874cc2..8f7252541 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,11 +27,11 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits +from pyquil.quantum_processor.qcs import QCSQuantumProcessor if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer - from pyquil.quantum_processor.qcs import QCSQuantumProcessor INFINITY = float("inf") From 3274ab23cf9e07825442ad09eb0ab45482a0a5a1 Mon Sep 17 00:00:00 2001 From: shrapp Date: Wed, 30 Aug 2023 15:17:21 +0300 Subject: [PATCH 17/19] import fix 9 --- pyquil/noise.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index 8f7252541..2324baef7 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,11 +27,12 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator, Qubit from pyquil.quilbase import Declare, Gate, DefGate, Pragma, DelayQubits -from pyquil.quantum_processor.qcs import QCSQuantumProcessor + if TYPE_CHECKING: from pyquil.quil import Program from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + from pyquil.quantum_processor import AbstractQuantumProcessor INFINITY = float("inf") @@ -841,28 +842,28 @@ def change_times_by_ratio(times: Dict[Any, float], ratio: float): return times -def get_t1s(qpu: QCSQuantumProcessor) -> Dict[int, float]: +def get_t1s(qpu: AbstractQuantumProcessor) -> Dict[int, float]: benchmarks = qpu._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(qpu: QCSQuantumProcessor) -> Dict[int, float]: +def get_t2s(qpu: AbstractQuantumProcessor) -> Dict[int, float]: benchmarks = qpu._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(qpu: QCSQuantumProcessor) -> Dict[int, float]: +def get_1q_fidelities(qpu: AbstractQuantumProcessor) -> Dict[int, float]: benchmarks = qpu._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(qpu: QCSQuantumProcessor) -> Dict[int, float]: +def get_readout_fidelities(qpu: AbstractQuantumProcessor) -> Dict[int, float]: instructions = qpu._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} @@ -886,7 +887,7 @@ class Calibrations: this may change in time, and require changes in the class. """ - def __init__(self, qpu: Optional[QCSQuantumProcessor] = None, calibrations=None) -> None: + def __init__(self, qpu: Optional[AbstractQuantumProcessor] = None, calibrations=None) -> None: self.fidelities = {} self.readout_fidelity = {} self.T2 = {} @@ -913,7 +914,7 @@ def __init__(self, qpu: Optional[QCSQuantumProcessor] = None, calibrations=None) self.readout_fidelity = get_readout_fidelities(qpu) self._create_2q_dicts(qpu) - def _create_2q_dicts(self, qpu: QCSQuantumProcessor) -> None: + def _create_2q_dicts(self, qpu: AbstractQuantumProcessor) -> None: instructions = qpu._isa.instructions self.two_q_gates = { operation.name @@ -1294,7 +1295,7 @@ def add_kraus_maps_to_program( def add_noise_to_program( - qcs_quantum_processor: QCSQuantumProcessor, + qcs_quantum_processor: AbstractQuantumProcessor, p: Program, convert_to_native: bool = True, calibrations: Optional[Calibrations] = None, @@ -1311,7 +1312,7 @@ def add_noise_to_program( 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 QCSQuantumProcessor object. + :param qcs_quantum_processor: A AbstractQuantumProcessor object. :param p: A pyquil program :param calibrations: optional, can get the calibrations in advance, instead of producing them from the URL. From 2dfecf4b5ab3e736fdf2e126d5f13d4a6dd38e27 Mon Sep 17 00:00:00 2001 From: shrapp Date: Wed, 30 Aug 2023 15:44:06 +0300 Subject: [PATCH 18/19] import fix 10 --- docs/source/noise.rst | 13 +++++++------ pyquil/noise.py | 44 +++++++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/docs/source/noise.rst b/docs/source/noise.rst index d00ac21b8..3d1303710 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -1559,7 +1559,7 @@ After performing these operations, the resulting GHZ state is :math:`(\ket{000} from pyquil.quantum_processor.qcs import get_qcs_quantum_processor import matplotlib.pyplot as plt - def ghz(qvm, qcs_qpu, qubits, numshots, noise=False, cal=None, intensity=1.0): + def ghz(qvm, isa, qubits, numshots, noise=False, cal=None, intensity=1.0): p = Program() p.declare("ro", "BIT", 3) p += H(qubits[0]) @@ -1572,18 +1572,19 @@ After performing these operations, the resulting GHZ state is :math:`(\ket{000} p = qvm.compiler.quil_to_native_quil(p) if noise: if cal is not None: - p = add_noise_to_program(qcs_qpu, p, calibrations=cal, noise_intensity=intensity) + p = add_noise_to_program(isa, p, calibrations=cal, noise_intensity=intensity) else: - p = add_noise_to_program(qcs_qpu, p, noise_intensity=intensity) + 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,qcs_qpu,qubits,numshots, False)).readout_data.get("ro") - half_noise = qvm.run(ghz(qvm,qcs_qpu,qubits,numshots, True, cal, intensity=0.5)).readout_data.get("ro") - noisy = qvm.run(ghz(qvm,qcs_qpu,qubits,numshots, True, cal)).readout_data.get("ro") + 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): diff --git a/pyquil/noise.py b/pyquil/noise.py index 2324baef7..d20c54e6c 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -27,12 +27,12 @@ from pyquil.noise_gates import _get_qvm_noise_supported_gates 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 - from pyquil.quantum_processor import AbstractQuantumProcessor INFINITY = float("inf") @@ -842,29 +842,29 @@ def change_times_by_ratio(times: Dict[Any, float], ratio: float): return times -def get_t1s(qpu: AbstractQuantumProcessor) -> Dict[int, float]: - benchmarks = qpu._isa.benchmarks +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(qpu: AbstractQuantumProcessor) -> Dict[int, float]: - benchmarks = qpu._isa.benchmarks +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(qpu: AbstractQuantumProcessor) -> Dict[int, float]: - benchmarks = qpu._isa.benchmarks +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(qpu: AbstractQuantumProcessor) -> Dict[int, float]: - instructions = qpu._isa.instructions +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 @@ -887,7 +887,7 @@ class Calibrations: this may change in time, and require changes in the class. """ - def __init__(self, qpu: Optional[AbstractQuantumProcessor] = None, calibrations=None) -> None: + def __init__(self, isa: Optional[InstructionSetArchitecture] = None, calibrations=None) -> None: self.fidelities = {} self.readout_fidelity = {} self.T2 = {} @@ -904,18 +904,18 @@ def __init__(self, qpu: Optional[AbstractQuantumProcessor] = None, calibrations= self.two_q_gates = calibrations.two_q_gates return - if qpu is None: + if isa is None: return # user can set their own values else: - self.T1 = get_t1s(qpu) - self.T2 = get_t2s(qpu) - self.fidelities[Depolarizing_1Q_gate] = get_1q_fidelities(qpu) - self.readout_fidelity = get_readout_fidelities(qpu) - self._create_2q_dicts(qpu) - - def _create_2q_dicts(self, qpu: AbstractQuantumProcessor) -> None: - instructions = qpu._isa.instructions + 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 @@ -1295,7 +1295,7 @@ def add_kraus_maps_to_program( def add_noise_to_program( - qcs_quantum_processor: AbstractQuantumProcessor, + isa: InstructionSetArchitecture, p: Program, convert_to_native: bool = True, calibrations: Optional[Calibrations] = None, @@ -1312,7 +1312,7 @@ def add_noise_to_program( 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 AbstractQuantumProcessor object. + :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. @@ -1329,7 +1329,7 @@ def add_noise_to_program( """ if calibrations is None: - calibrations = Calibrations(qpu=qcs_quantum_processor) + calibrations = Calibrations(isa=InstructionSetArchitecture) new_p = Program() From cc1bd982b1f02157b4fae8c08e424e8813ef8688 Mon Sep 17 00:00:00 2001 From: shrapp Date: Wed, 30 Aug 2023 15:52:01 +0300 Subject: [PATCH 19/19] import fix 11 --- pyquil/noise.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pyquil/noise.py b/pyquil/noise.py index d20c54e6c..1cce556d9 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -981,7 +981,7 @@ def create_damping_after_dephasing_kraus_maps( return kraus_maps -def create_kraus_maps(prog: Program, gate_name: str, gate_time: float, T1: Dict[int, float], T2: Dict[int, float]): +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, @@ -997,12 +997,12 @@ def create_kraus_maps(prog: Program, gate_name: str, gate_time: float, T1: Dict[ def add_single_qubit_noise( - prog: Program, + prog: "Program", T1: Dict[int, float], T2: Dict[int, float], gate_time_1q: float = 32e-9, gate_time_2q: float = 176e-09, -) -> Program: +) -> "Program": """ Applies the model on the different kinds of I. :param prog: The program including I's that are not noisy yet. @@ -1018,9 +1018,9 @@ def add_single_qubit_noise( def add_readout_noise( - prog: Program, + prog: "Program", ro_fidelity: Dict[int, float], -) -> Program: +) -> "Program": """ adds readout noise to the program. :param prog: The program without readout noise yet. @@ -1093,7 +1093,7 @@ def depolarizing_kraus(num_qubits: int, p: float = 0.95) -> List[np.ndarray]: return pauli_kraus_map(probabilities) -def add_depolarizing_noise(prog: Program, fidelities: Dict[str, Dict[str, float]]) -> Program: +def add_depolarizing_noise(prog: "Program", fidelities: Dict[str, Dict[str, float]]) -> "Program": """ add depolarizing noise to the program. @@ -1111,7 +1111,9 @@ def add_depolarizing_noise(prog: Program, fidelities: Dict[str, Dict[str, float] return prog -def add_delay_maps(prog: Program, delay_gates: Dict[str, float], T1: Dict[int, float], T2: Dict[int, float]) -> Program: +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. @@ -1127,7 +1129,7 @@ def add_delay_maps(prog: Program, delay_gates: Dict[str, float], T1: Dict[int, f return prog -def def_gate_to_prog(name: str, dim: int, new_p: Program): +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`. @@ -1143,13 +1145,13 @@ def def_gate_to_prog(name: str, dim: int, new_p: Program): def define_noisy_gates_on_new_program( - new_p: Program, - prog: 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]: +) -> Tuple["Program", Dict]: """ defines noisy gates for the new program `new_p`, and returns a Dictionary with the new noise gates. @@ -1209,8 +1211,8 @@ def define_noisy_gates_on_new_program( def add_noisy_gates_to_program( - new_p: Program, - prog: Program, + new_p: "Program", + prog: "Program", noise_gates: Dict, damping_after_dephasing_after_2q_gate: bool, damping_after_dephasing_after_1q_gate: bool, @@ -1273,7 +1275,7 @@ def add_noisy_gates_to_program( def add_kraus_maps_to_program( - new_p: Program, + new_p: "Program", calibrations: Calibrations, delay_gates: Dict, depolarizing: bool, @@ -1296,7 +1298,7 @@ def add_kraus_maps_to_program( def add_noise_to_program( isa: InstructionSetArchitecture, - p: Program, + p: "Program", convert_to_native: bool = True, calibrations: Optional[Calibrations] = None, depolarizing: bool = True, @@ -1305,7 +1307,7 @@ def add_noise_to_program( damping_after_dephasing_only_on_targets: bool = False, readout_noise: bool = True, noise_intensity: float = 1.0, -) -> Program: +) -> "Program": """ Add generic damping and dephasing noise to a program. Noise is added to all qubits, after a 2-qubit gate operation. @@ -1327,6 +1329,7 @@ def add_noise_to_program( :return: A new program with noisy operators. """ + from pyquil.quil import Program if calibrations is None: calibrations = Calibrations(isa=InstructionSetArchitecture)