Simulator

class trueq.Simulator

A (noisy) quantum simulator for Circuit objects that can be used to compute the final state(), the total effective operator(), or simply run() them to sample shots.

import trueq as tq

# initialize a simulator with no noise present
sim = tq.Simulator()

# initialize a simulator with depolarizing noise at rate 1%
sim = tq.Simulator().add_depolarizing(0.01)

# initialize a simulator with depolarizing noise at rate 1%, and unitary
# overration by 5%
sim = tq.Simulator().add_depolarizing(0.01).add_overrotation(0.05)

# make some circuits and populate their results with random shots
circuits = tq.make_srb([0], [4, 60])
sim.run(circuits, n_shots=100)
append_cycle_noise(propagation_fn, noise_only=False, is_prep=False, is_meas=False)

Appends a custom noise source to the simulator. A noise source is a function with arguments (cycle, state, cache) that takes a Cycle and uses it to modify a state, which an instance of Tensor.

import trueq
import numpy as np

# an ideal pure state simulator with no caching
def fun(cycle, state, cache):
    for labels, gate in cycle.gates.items():
        state.apply_matrix(labels, gate.mat)
sim = trueq.Simulator()
sim.append_cycle_noise(fun)

# an ideal simulator that prepares the |+> state every time a Prep is found
def fun(cycle, state, cache):
    for label in cycle.prep_labels:
        state.update((label,), [1 / np.sqrt(2), 1 / np.sqrt(2)])
sim = trueq.Simulator()
sim.append_cycle_noise(fun, is_prep=True)

# simulator that applies a Kraus map iid to all qubits (note that
# Simulator.add_kraus is built-in and does the same thing)
kraus_ops = [np.sqrt(0.99)*np.eye(2), np.sqrt(0.01)*np.array([[0,1],[1,0]])]
S = sum(np.kron(a, a.conj().T) for a in kraus_ops)
def fun(cycle, state, cache):
    for labels, gate in cycle.gates.items():
        for label in labels:
            state.apply_matrix((label,), S)
sim = trueq.Simulator()
# the function prop does not apply gates in a cycle, so we are careful to
# set noise_only to True
sim.append_cycle_noise(fun, noise_only=True)
Parameters
  • propagation_fn (propagation_fn) – A function with arguments (cycle, state, cache) that takes a Cycle and uses it to modify a state (an instance of Tensor). cache is a dictionary that is unique to this CyclePropagator that can be used in any way to cache computations.

  • noise_only (bool) – True iff this propagator only injects noise into the state, ie., it does not attempt to actually implement the cycle.

  • prep_noise (bool) – Whether this propagator is used as a state preparer. This is treated no different than any other propagator, except that it is guaranteed to run first every time a cycle is encountered, no matter when this propagator was added to the simulator object.

  • meas_noise (bool) – Whether this propagator is used to simulate measurement. This is treated no different than any other propagator, except that it is guaranteed to run first every time a cycle is encountered, no matter when this propagator was added to the simulator object.

state(circuit)

Returns the quantum state that results from simulating the given circuit.

If this simulator contains only unitary errors and pure state preparations, a pure state will be simulated and returned, otherwise, a density matrix will be simulated and returned. Unless this simulator contains a special preparation (see e.g. add_prep()), every qubit in the circuit will be prepared with the state \(|0\rangle\).

import trueq as tq

# make a circuit with two clock cycles
circuit = tq.Circuit([{(0, ): tq.Gate.h}, {(0, 1): tq.Gate.cnot}])

# simulate the circuit to find the final pure state
psi = tq.Simulator().state(circuit)
psi.mat()
# array([0.707, 0, 0, 0.707])

# if we add depolarizing noise we get a density matrix
rho = tq.Simulator().add_depolarizing(0.01).state(circuit)
rho.mat()
# array([[0.4525, 0.    , 0.    , 0.3645],
#        [0.    , 0.0475, 0.    , 0.    ],
#        [0.    , 0.    , 0.0475, 0.    ],
#        [0.3645, 0.    , 0.    , 0.4525]])

# we can get outcome probabilities from the state
psi.probabilities()
rho.probabilities()

# and we can convert them to Result objects
psi.probabilities().to_results()
rho.probabilities().to_results()
Parameters

circuit (Circuit) – A circuit to find the final state of.

Return type

trueq.simulator.StateTensor

operator(circuit, gates_only=True)

Returns the unitary or superoperator that results from simulating the given circuit.

If this simulator contains only unitary errors and pure state preparations, a unitary will be simulated and returned, otherwise, a superoperator will be simulated and returned.

import trueq as tq

# make a circuit with two clock cycles
circuit = tq.Circuit([{(0, ): tq.Gate.h}, {(0, 1): tq.Gate.cnot}])

# simulate the circuit to find the final unitary
u = tq.Simulator().operator(circuit)
u.mat()
# array([[ 0.70710678,  0.        ,  0.70710678,  0.        ],
         [ 0.        ,  0.70710678,  0.        ,  0.70710678],
         [ 0.        ,  0.70710678,  0.        , -0.70710678],
         [ 0.70710678,  0.        , -0.70710678,  0.        ]])

# if we add depolarizing noise we get a superoperator
s = tq.Simulator().add_depolarizing(0.01).operator(circuit)
s.mat().shape  # too big to meaningfully print here
# (16, 16)

Note

By default, this method skips Prep and Meas operators.

Parameters
  • circuit (Circuit) – A circuit to find the final operator of.

  • gates_only (bool) – Whether all Prep and Meas operators found in obj should be skipped during simulation.

Return type

trueq.simulator.OperatorTensor

run(circuits, n_shots=50, overwrite=None)

Simulates one or many Circuits and updates each of them to include results.

import trueq as tq

# make a circuit with two clock cycles and a measurement round
circuit = tq.Circuit([{(0, ): tq.Gate.h}, {(0, 1): tq.Gate.cnot}])
circuit.measure_all()

# initialize a simulator with no noise
sim = tq.Simulator()

# run the circuit on the simulator 100 times to populate the results
sim.run(circuit, n_shots=100)

# print the results of circuit
circuit.results
# Results({'00': 52, '11': 48})

# we can also use run to evaluate circuit collections generated by protocols
# like CB, SRB, IRB, and XRB on a simulator:

# generate a circuit collection for SRB
circuits = tq.make_srb([0], [5, 50, 100], 30)

# simulate circuits and enter results
sim.run(circuits, n_shots=100)
Parameters
  • circuits (Circuit | Iterable) – A single circuit, or an iterable of circuits.

  • overwrite (bool | None) – If False, a circuit that already has results will have new simulation results added to the old results. If True or None, old results will be erased and replaced with new results, though a warning will be raised in the latter case of None (default).

add_depolarizing(p, d=2)

Appends a noise source to this simulator that adds isotropic depolarizing noise to the simulation every time a gate is encountered. The noise map for a given gate is defined by

\[\mathcal{D}(\rho) = (1-p) \rho + p \text{Tr}(\rho) \mathcal{I} / d\]

where \(d=2\) for qubits. If, for example, a two-qubit gate is encountered, the tensor product of two one-qubit depolarizing channels will be simulated. Depolarizing noise is applied to every system being acted on by a gate in a given cycle. For example, if qubits (0,1) get a CNOT, and qubit 3 gets an X in a given cycle, then depolarizing noise is applied to only these three qubits, even if the preceeding cycle acted on qubit 2.

Note that this is noise only—it does not try to implement the cycle in question, but only adds noise to it.

import trueq

# make a simulator with depolarizing noise with p=0.02 acting at every
# location where a gate is applied
sim = tq.Simulator().add_depolarizing(0.02)
Parameters
  • p (float) – The depolarizing parameter, as defined in the equation above.

  • d (int) – The subsystem dimension, 2 for qubits.

add_kraus(kraus_ops)

Appends a noise source to this simulator that adds Kraus noise to the simulation every time a gate is encountered. This noise is gate-independent (though it may depend on size of the gate, described below) and the noise map is defined by

\[\mathcal{K}(\rho) = \sum_{i} K_i \rho K_i^\dagger\]

Note that this is noise only—it does not try to implement a given cycle in question, but only adds noise to it.

Kraus noise is applied to every system being acted on by a gate in a given cycle. For example, if qubits (0,1) get a CNOT, and qubit 3 gets an X in a given cycle, then Kraus noise is applied to only these three qubits, even if the preceeding cycle acted on qubit 2.

Optionally, every gate size (trueq.Gate.n_sys) can specify its own set of Kraus operators. If a gate is encountered whose size is not present in the collection of Kraus operators, then the single system (n_sys=1) Kraus operators are applied to every system that the gate acts on.

import trueq as tq
import numpy as np

# define qubit dephasing kraus operators
kraus_ops = [np.sqrt(0.99)*np.eye(2), np.sqrt(0.01)*np.diag([1,-1])]

# initialize a simulator in which qubit dephasing noise acts on every
# location where gates are applied
sim = tq.Simulator().add_kraus(kraus_ops)

# qubit dephasing for single qubit gates, but some other kraus channel for
# two-qubit gates
kraus_ops = {
    1: [np.sqrt(0.999) * np.eye(2), np.sqrt(0.001) * np.diag([1, -1])],
    2: [
        np.sqrt(0.99) * np.eye(4),
        np.sqrt(0.01) * np.fliplr(np.diag([1, 1, 1, 1]))
    ],
}

# initialize a simulator with the noise defined above acting at
# every location where a gate acts

sim=tq.Simulator().add_kraus(kraus_ops)
Parameters

kraus_ops – Either a list-like of square matrices, each having the dimension of a single system; or, a dictionary whose keys are n_sys and whose values are list-likes of square matrices, specifying the Kraus operators for gates with that value of n_sys.

add_overrotation(single_sys=0, multi_sys=0)

Appends a noise source to this simulator that performs gate over/underrotation. This is done by propagating by the matrix power \(U_{err}=U^{(1+\epsilon)}\) instead of the ideal gate \(U\).

import trueq as tq

# initialize a simulator in which every single qubit gate is overrotated by
# epsilon=0.01
sim = tq.Simulator().add_overrotation(single_sys=0.01)

# initialize a simulator in which every single qubit gate is overrotated by
# epsilon=0.02 and every multi-qubit gate by epsilon=0.04
sim = tq.Simulator().add_overrotation(single_sys=0.02, multi_sys=0.04)

# equivalently, we can initialize a simulator with an overrotation of
# epsilon=0.02 for single qubits and then add an overrotation of
# epsilon=0.04 for multi-qubit gates
sim1 = tq.Simulator().add_overrotation(single_sys=0.02)
sim1.add_overrotation(multi_sys=0.04)

# so that sim and sim1 apply the same noise
Parameters
  • single_sys (float) – The amount of over/underrotation to apply to gates that act on a single subsystem (ie. single-qubit). A value of 0 corresponds to no overrotation, a negative value corresponds to underrotation, and a positive value corresponds to overrototian.

  • multi_sys (float) – The amount of over/underrotation to apply to gates that act on a multiple subsystems (ie. multi-qubit). A value of 0 corresponds to no overrotation, a negative value corresponds to underrotation, and a positive value corresponds to overrototian.

add_povm(povm)

Adds a measurement noise in the form of a positive operator-valued measurement (POVM). This is entered as a Tensor with shape ((k,), (d, d)) where k is the number of outcomes per qudit, and d is the Hilbert state dimension, which must match the simulation. A sum over the k axis must produce the identity matrix for a probability preserving POVM.

import trueq as tq
import scipy as sp

# construct a POVM that is perfect on all qubits except qubit 0
# on this qubit, the measurement axis is off by 5 degrees about Y
ideal = sp.array([[[1,0], [0, 0]], [[0, 0], [0, 1]]])
U = tq.Gate.from_generators("X", 5).mat
twisted = [U @ x @ U.conj().T for x in ideal]
povm = tq.math.Tensor(
    2, (2, 2),
    spawn=ideal,
    value={(0,): twisted},
    dtype=sp.complex128
)

# simulate the results of a circuit using this POVM
sim = tq.Simulator().add_povm(povm)
circuit = tq.Circuit([{(0, ): tq.Gate.h}, {(0, 1): tq.Gate.cnot}])
circuit.measure_all()
sim.run(circuit, n_shots=10000)
circuit.results
# Results({'00': 5033, '01': 5, '10': 9, '11': 4953})
Parameters

povm (Tensor) – A tensor describing a POVM.

add_prep(state)

Adds a state preparation description to this simulator that chooses which state to prepare whenever a tq.Prep operator is encountered in a circuit.

A state to add can be specified in any of the following formats:

  • A number between 0 and 1, representing a bitflip error probability of the ideal preparation \(|0\rangle\)

  • A length-\(d\) vector, representing a pure state

  • A \(d\times d\) matrix, representing a density matrix

If only one of the above is provided, it is used every time a tq.Prep object is encountered. One can also specify that different subsystems get different errors by providing a dictionary mapping whose values are combination of the above formats; see the examples below.

# add 5% bitflip error to any qubit
Simulator().add_prep(0.05)

# add 2% bitflip error by default, but 5% for qubits 2 and 3
Simulator().add_prep({None: 0.02, 2: 0.05, 3: 0.05})

# add specific density matrix to qubit 1, but 1% bitflip error by default
Simulator().add_prep({None: 0.01, 1: [[0.8, 0.1], [0.1, 0.2]]})

For state simulation, if a tq.Prep operator is encountered on a subsystem that has previously been initialized, then the register is traced-out and the state is replaced with a new one. For operator simulation, tq.Prep objects are taken as the partial-trace-with-replacement channel \(\rho\mapsto \operator{Tr}_\text{i}(\rho)\otimes\rho'\) where \(i\) is the subsystem label being prepared, \(\rho\) is the old state on all subsystems, and \(\rho'\) is the new state being prepared on \(i\). Although, note that operator() skips preparation objects by default.

Parameters

state – A description of quantum states to initialize with.

add_readout_error(readout_error)

Adds qubit readout error to this simulator. For any single qubit, the readout error can be either a number, which is the probability of mischaracterizing a measurement bit, or a pair of numbers, which are the respective probabilities of mischaracterizing the \(|0\rangle\) and \(|1\rangle\) states. If different qubits are to receive different readout errors, then the input can be a dictionary as found in the examples below. The default error rate is 0 for qubits which are not specified.

import trueq as tq

# all qubits get 1% readout error
tq.Simulator().add_readout_error(0.01)

# all qubits get 1% readout error, except qubit 1 gets 5% readout error
tq.Simulator().add_readout_error({None: 0.01, 1: 0.05})

# all qubits get 1% readout error, except qubit 1 get 5% readout error, and
# qubit 3 get asymmetric readout error of 1% on |0> and 7% on |1>
tq.Simulator().add_readout_error({None: 0.01, 1: 0.05, 3: [0.01, 0.07]})
Parameters

readout_error (dict | list | float) – Specification of readout errors.

add_stochastic_pauli(px=0, py=0, pz=0)

Adds a noise source to this simulator that introduces stochastic Pauli noise. This is done by applying the following single-qubit Kraus operators to every qubit that is explicitly acted upon by a gate in a given cycle:

\[ \begin{align}\begin{aligned}K_1 &= \sqrt{1-p_x-p_y-p_z} \mathcal{I}\\K_2 &= \sqrt{p_x} \sigma_x\\K_3 &= \sqrt{p_y} \sigma_y\\K_4 &= \sqrt{p_z} \sigma_z\end{aligned}\end{align} \]

Note that this is noise only—it does not try to implement the cycle in question, but only adds noise to it.

This noise is applied to every system being acted on by a gate in a given cycle. For example, if qubits (0,1) get a CNOT, and qubit 3 gets an X in a given cycle, then stochastic Pauli noise is applied to only these three qubits, even if the preceeding cycle acted on qubit 2.

import trueq as tq

# initialize a simulator in which every location where a gate acts undergoes
# Pauli noise with px=0.01 and py=0.04
sim = tq.Simulator().add_stochastic_pauli(px=0.01, py=0.04)
Parameters
  • px (float) – The probability of an X error.

  • py (float) – The probability of a Y error.

  • pz – The probability of a Z error.