# Simulation¶

 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. trueq.Simulator.add_depolarizing Appends a noise source to this simulator that adds isotropic depolarizing noise to the simulation. trueq.Simulator.add_kraus Appends a noise source to this simulator that adds Kraus noise to the simulation. trueq.Simulator.add_overrotation Appends a noise source to this simulator that performs gate over/underrotation. trueq.Simulator.add_povm Appends measurement noise in the form of a positive operator-valued measurement (POVM). trueq.Simulator.add_prep Appends a state preparation description to this simulator that chooses which state to prepare whenever a Prep operator is encountered in a circuit. trueq.Simulator.add_readout_error Appends measurement classification error to this simulator, where errors are specified by confusion matrices. trueq.Simulator.add_relaxation Appends a noise source to this simulator that adds $$T_1$$ and $$T_2$$ relaxation noise to the simulation. trueq.Simulator.add_stochastic_pauli Appends a noise source to this simulator that introduces stochastic Pauli noise. trueq.Simulator.operator Returns the unitary or superoperator that results from simulating the given circuit. trueq.Simulator.run Updates the results attribute of each given circuit by simulating its final quantum state and sampling shots from it. trueq.Simulator.state Returns the quantum state that results from simulating the given circuit. trueq.simulation.simulator.CyclePropagator Wraps a function that mutates quantum states or operators by given circuit cycles.

## 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%

# initialize a simulator with depolarizing noise at rate 1%, and unitary
# overration by 5%

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

Results({'0': 97, '1': 3})

append_cycle_noise(propagation_fn, noise_only=False, is_prep=False, is_meas=False, dim=None)

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 is an instance of Tensor.

import trueq
import numpy as np

# create a circuit to test on
circuit = tq.Circuit([{0: tq.Prep()}, {0: tq.Gate.x}, {0: tq.Meas()}])

# 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()
results = sim.append_cycle_noise(fun).sample(circuit, 100)
print("Ideal example:", results)

# 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()
results = sim.append_cycle_noise(fun, is_prep=True).sample(circuit, 100)
print("Prep example: ", results)

# 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.9)*np.eye(2), np.sqrt(0.1)*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
results = sim.append_cycle_noise(fun, noise_only=True).sample(circuit, 100)
print("Kraus example:", results)

Ideal example: Results({'1': 100})
Prep example:  Results({'0': 51, '1': 49})
Kraus example: Results({'0': 8, '1': 92})

Parameters
• propagation_fn (function) – 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 propagator is applied before any other on each cycle.

• meas_noise (bool) – Whether this propagator is used to simulate measurement. This propagator is integrated with measurement sampling and is applied only at the last cycle.

• dim (None | int) – The Hilbert space dimension of each subsystem expected by this propagator. A value of None indicates that this propagator can propagate cycles of any dimension.

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)
print("Pure state: ", psi.mat())

# if we add depolarizing noise we get a density matrix
print("Density matrix: ", rho.mat())

# we can get outcome probabilities from the state
print("Pure state probabilities:     ", psi.probabilities())
print("Density matrix probabilities: ", rho.probabilities())

# and we can convert them to Result objects
print("Pure state as results:     ", psi.probabilities().to_results())
print("Density matrix as results: ", rho.probabilities().to_results())

Pure state:  [0.70710678+0.j 0.        +0.j 0.        +0.j 0.70710678+0.j]
Density matrix:  [[0.495025 +0.j 0.       +0.j 0.       +0.j 0.4851495+0.j]
[0.       +0.j 0.004975 +0.j 0.       +0.j 0.       +0.j]
[0.       +0.j 0.       +0.j 0.004975 +0.j 0.       +0.j]
[0.4851495+0.j 0.       +0.j 0.       +0.j 0.495025 +0.j]]
Pure state probabilities:      Tensor(<[(2,), ()] on labels [(0, 1)]>)
Density matrix probabilities:  Tensor(<[(2,), ()] on labels [(0, 1)]>)
Pure state as results:      Results({'00': 0.5000000000000001, '11': 0.5000000000000001})
Density matrix as results:  Results({'00': 0.49502500000000016, '01': 0.004975000000000002, '10': 0.004975000000000002, '11': 0.49502500000000016})

Parameters

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

Return type

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)
tq.visualization.plot_mat(u.mat())

# if we add depolarizing noise we get a superoperator
print("Super operator shape: ", s.mat().shape)

Super operator shape:  (16, 16)


Note

By default, this method skips Prep and Meas operators.

Parameters
Return type

OperatorTensor

sample(circuit, n_shots=50)

Samples ditstrings from the final state of the simulated circuit. In contrast to run(), this method does not update the results of the circuit.

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()

# instantiate a simulator with depolarizing and overrotation noise

# sample 100 shots from the final state of the circuit
print(sim.sample(circuit, 100))

Results({'00': 59, '01': 1, '10': 1, '11': 39})

Parameters
• circuit (Circuit) – A single circuit.

• n_shots (int | float("inf")) – The number of shots to sample. The final state of the circuit is simulated once and shots are drawn from the resulting probability distribution. Or, if this value is infinity-like (e.g. float("inf") or numpy.inf) then results are populated with the exact simulated probabilities.

Return type

Results

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

Updates the results attribute of each given circuit by simulating its final quantum state and sampling shots from it.

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(circuit.results)

# instantiate a simulator with depolarizing and overrotation noise

# we can also use run to evaluate circuit collections generated by protocols
# like CB, SRB, IRB, and XRB on a simulator:
circuits = tq.make_srb([0], [5, 50, 100], 30)
sim.run(circuits, n_shots=100)
circuits.plot.raw()

Results({'00': 47, '11': 53})

Parameters
• circuits (Circuit | Iterable) – A single circuit or an iterable of circuits.

• n_shots (int | float("inf")) – The number of shots to sample. The final state of the circuit is simulated once and shots are drawn from the resulting probability distribution. Or, if this value is infinity-like (e.g. float("inf") or numpy.inf) then results are populated with the exact simulated probabilities.

• 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, noisy_labels=None, noisy_gates=None)

Appends a noise source to this simulator that adds isotropic depolarizing noise to the simulation. By default, noise is added every time a gate is encountered, however, by specifying noisy_labels and/or noisy_gates, the user can design a noise profile which is gate- and/or qubit-dependent. 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. By default, 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

circuit = tq.Circuit([{(0,1): tq.Gate.cx, 2: tq.Gate.h}])

# make a simulator with depolarizing noise with p=0.02 acting at every
# location where a gate is applied
print(sim.sample(circuit, 1000))

# make a simulator where depolarizing noise acts only on locations where
# gates act on qubit 5
sim = tq.Simulator().add_depolarizing(0.04, noisy_labels = {5})
print(sim.sample(circuit, 1000))

Results({'000': 484, '001': 495, '010': 2, '011': 5, '100': 8, '101': 5, '110': 1})
Results({'000': 515, '001': 485})

Parameters
• p (float) – The depolarizing parameter, as defined in the equation above.

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

• noisy_labels (int | Iterable) – An integer or iterable of integers specifying which qubit labels are affected by this noise process. All of a gate’s labels must be a subset of the noisy labels in order for it to be impacted by the noise. By default, noisy_labels is None which causes all qubits to undergo this noise process.

• noisy_gates (Gate | Iterable) – A gate or iterable of gates which are affected by this noise process. By default, noisy_gates is None and all gates will be acted upon by this noise process.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_kraus(kraus_ops, noisy_labels=None, noisy_gates=None)

Appends a noise source to this simulator that adds Kraus noise to the simulation. By default, the noise is added 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.

By default, 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 (determined by the number of labels it acts on) can specify its own set of Kraus operators. If an applicable 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

circuit = tq.Circuit([{(0,1): tq.Gate.cx, 2: tq.Gate.h}])

# 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
print(sim.sample(circuit, 1000))

# 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
print(sim.sample(circuit, 1000))

# initialize a simulator which applies the above noise when a gate acts
# on qubit 0
print(sim.sample(circuit, 1000))

# initialize a simulator which applies the above noise when an X gate acts
# on qubit 2 or 3
sim=tq.Simulator()
print(sim.sample(circuit, 1000))

Results({'000': 485, '001': 515})
Results({'000': 517, '001': 480, '110': 1, '111': 2})
Results({'000': 484, '001': 516})
Results({'000': 513, '001': 487})

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.

• noisy_labels (int | Iterable) – An integer or iterable of integers specifying which qubit labels are affected by this noise process. All of a gate’s labels must be a subset of the noisy labels in order for it to be impacted by the noise. By default, noisy_labels is None which causes all qubits to undergo this noise process.

• noisy_gates (Gate | Iterable) – A gate or iterable of gates which are affected by this noise process. By default, noisy_gates is None and all gates will be acted upon by this noise process.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_overrotation(single_sys=0, multi_sys=0, noisy_labels=None, noisy_gates=None)

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

circuit = tq.Circuit([{(0,1): tq.Gate.cx, 2: tq.Gate.h}])

# initialize a simulator in which every single qubit gate is overrotated by
# epsilon=0.01
print(sim.sample(circuit, 1000))

# initialize a simulator in which every single qubit gate is overrotated by
# epsilon=0.02 and every multi-qubit gate by epsilon=0.04
print(sim.sample(circuit, 1000))

# initialize a simulator in which overrotations are applied whenever a gate
# acts on any qubits in {0, 2, 3}
sim = tq.Simulator()
print(sim.sample(circuit, 1000))

Results({'000': 521, '001': 479})
Results({'000': 496, '001': 504})
Results({'000': 492, '001': 508})

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 overrotatian.

• 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 overrotatian.

• noisy_labels (int | Iterable) – Which qubit labels are affected by this noise process. All of a gate’s labels must be a subset of the noisy labels in order for it to be impacted by the noise. By default, noisy_labels is None which causes all qubits to undergo this noise process.

• noisy_gates (Gate | Iterable) – A gate or iterable of gates which are affected by this noise process. By default, noisy_gates is None and all gates will be acted upon by this noise process.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_povm(povm)

Appends 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 qubit, 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 numpy as np

# Define a set of ideal POVM operators
ideal = np.array([[[1, 0], [0, 0]], [[0, 0], [0, 1]]])

# Define a unitary rotation about X by 5 degrees
U = tq.Gate.from_generators("X", 5).mat

# Define noisy POVM operators by rotating the ideal POVM operators by U
twisted = [U @ x @ U.conj().T for x in ideal]

# Specify how the operations defined above will impact the simulation
# In this instance, the ideal POVMs will act on every qubit, except for
# qubit 0, which will have the twisted POVM (rotation about X)
povm = tq.math.Tensor(
2, (2, 2),             # Number of POVMs, (dimensions of qubit subsystems)
spawn=ideal,           # Default measurement operation
value={(0,): twisted}, # Measurement operation on qubit 0
dtype=np.complex128    # Tensor is real by default; this specifies complex
)

# Initialize a simulator with the above POVM specifications.

# Initialize a circuit
circuit = tq.Circuit([{(0, ): tq.Gate.h}, {(0, 1): tq.Gate.cnot}])
circuit.measure_all()

# Run the circuit on the simulator
sim.sample(circuit, 100)

Results({'00': 48, '11': 52})

Parameters

povm (Tensor) – A tensor describing a POVM.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_prep(state)

Appends a state preparation description to this simulator that chooses which state to prepare whenever a 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 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.

import trueq as tq

circuit = tq.Circuit([{i: tq.Prep() for i in range(3)}])

# add 5% bitflip error to any qubit
print(sim.sample(circuit, float("inf")))

# add 2% bitflip error by default, but 5% for qubits 1 and 2
sim = tq.Simulator().add_prep({None: 0.02, 2: 0.05, 1: 0.05})
print(sim.sample(circuit, float("inf")))

# add specific density matrix to qubit 1, but 1% bitflip error by default
sim = tq.Simulator().add_prep({None: 0.01, 1: [[0.8, 0.1], [0.1, 0.2]]})
print(sim.sample(circuit, float("inf")))

Results({'000': 0.8573749999999999, '001': 0.045125, '010': 0.045125, '011': 0.0023750000000000004, '100': 0.045125, '101': 0.0023750000000000004, '110': 0.0023750000000000004, '111': 0.00012500000000000003})
Results({'000': 0.8844499999999998, '001': 0.04655, '010': 0.04655, '011': 0.0024500000000000004, '100': 0.01805, '101': 0.00095, '110': 0.00095, '111': 5e-05})
Results({'000': 0.78408, '001': 0.00792, '010': 0.19602, '011': 0.00198, '100': 0.00792, '101': 8e-05, '110': 0.00198, '111': 2e-05})


For state simulation, if a trueq.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, trueq.Prep objects are taken as the partial-trace-with-replacement channel $$\rho\mapsto\mathrm{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.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_readout_error(default_error=None, errors=None)

Appends measurement classification error to this simulator, where errors are specified by confusion matrices. For qubits, errors can instead be specified by a single misclassification probability (symmetric readout error), or a pair of readout errors (asymmetric readout error, where the probability of misclassifying $$|0\rangle$$ is different than $$|1\rangle$$).

Errors can differ on individual qubits by singling out their labels in the second argument, see the example below. Similarly, correlated errors can be added by specifying larger confusion matrices and the qubits they act on.

import trueq as tq

# make a test circuit that measures 4 qubits
circuit = tq.Circuit([{range(4): tq.Meas()}])

# all qubits get 1% readout error
print(sim.sample(circuit, 200))

# all qubits get 1% readout error, except qubit 1 gets 5% readout error
print(sim.sample(circuit, 200))

# 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>
print(sim.sample(circuit, 200))

# we specify correlated measurement errors on qubits (3, 2) with a 4x4
# confusion matrix (row/column order is 00, 01, 10, 11). all other qubits get
# 1% symmetric classification error.
confusion = [
[0.81, 0.72, 0.72, 0.64],
[0.09, 0.18, 0.08, 0.16],
[0.06, 0.08, 0.18, 0.16],
[0.04, 0.02, 0.02, 0.04]
]
print(sim.sample(circuit, 200))

# here we add asymmetric qutrit classification error to all three levels, where
# |0> is reported as a '0' 99% of the time, |1> is reported as a '1' 92% of the
# time, and |2> is reported as a '2' 80% of the time. off-diagonals are the
# probabilities of misclassification into specific outcomes corresponding to the
# row they are located in. we add this error specifically to qutrit 2, the rest
# of the qutrits have ideal measurements.
confusion = [[0.99, 0.03, 0.02], [0.005, 0.92, 0.18], [0.005, 0.05, 0.8]]
print(sim.sample(circuit, 200))

# classify qutrit measurements into binary outcomes. this situation arises, for
# example, in superconducting qubits when there is leakage into the third level
# of the anharmonic oscillator, but readout always reports a '0' or a '1'. we
# use a rectangular confusion matrix where |2> is classified as a '0' 70% of the
# time, and as a '1' 30% of the time.
confusion = [[0.99, 0.08, 0.7], [0.01, 0.92, 0.3]]
print(sim.sample(circuit, 200))

Results({'0000': 195, '0001': 1, '0100': 3, '1000': 1})
Results({'0000': 188, '0001': 3, '0010': 2, '0100': 6, '1000': 1})
Results({'0000': 186, '0001': 1, '0100': 11, '1000': 2})
Results({'0000': 159, '0010': 15, '0001': 14, '0011': 9, '0100': 2, '0101': 1})
Results({'0000': 200}, dim=3)
Results({'0000': 195, '0001': 2, '0010': 1, '0100': 1, '1000': 1})

Parameters
• default_error (numpy.ndarray-like | float | float) – Default readout error specificed as a confusion matrix to be applied to all qubits that don’t receive special errors in errors. For qubits, the confusion matrix can be replaced by a single number or pair of numbers for symmetric and asymmetric classification error, respectively. The default no error.

• errors (dict) – A dictionary mapping tuples of qubit labels to confusion matrices. For qubits, confusion matrices can be replaced by a single number or pair of numbers for symmetric and asymmetric classification error, respectively.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_relaxation(t1, t2, t_single, t_multi, excited_pop=0)

Appends a noise source to this simulator that adds $$T_1$$ and $$T_2$$ relaxation noise to the simulation. Specifically, for every cycle in a circuit, this noise source adds error to every qubit in the state defined by the Choi matrix:

$\begin{split}C = \begin{pmatrix} 1-p(1-e^{-t/T_1}) & 0 & 0 & e^{-t/T_2} \\ 0 & (1-p)e^{-t/T_1} & 0 & 0 \\ 0 & 0 & pe^{-t/T_1} & 0 \\ e^{-t/T_2} & 0 & 0 & 1-(1-p)e^{-t/T_1} \end{pmatrix}\end{split}$

where $$t$$ is the time of the longest gate in the cycle (i.e. either t_single or t_multi for an entire cycle).

import trueq

circuit = tq.Circuit([{(0,1): tq.Gate.cx, 2: tq.Gate.h}])

# Make a simulator with relaxation noise with t1=100e-6, t2=50e-6, single-qubit
# gate time 25e-9, two qubit gate time 100e-9 and an excited equilibrium
# population 0.01.
sim = tq.Simulator().add_relaxation(100e-6, 50e-6, 25e-9, 100e-9, 0.01)
print(sim.sample(circuit, 1000))

# Make a simulator with relaxation noise with t1=10us, single-qubit gate time
# 25ns and two qubit gate time 100ns. t2=5us on all qubits except qubit 0
# which has t2=2.5us.
t2 = {None: 5e-6, 0: 2.5e-6}
sim = tq.Simulator().add_relaxation(10e-6, t2, 25e-9, 100e-9)
# plot the final state
tq.visualization.plot_mat(sim.state(circuit).mat())

Results({'000': 488, '001': 512})

Parameters
• t1 (float | dict) – The $$T_1$$ time, i.e. the characteristic time of relaxation. These can vary from qubit to qubit, see the example above.

• t2 (float | dict) – The $$T_2$$ time, i.e. the characteristic time of dephasing. These can vary from qubit to qubit, see the example above.

• t_single (float) – The time for a single-qubit gate.

• t_multi (float) – The time for a multi-qubit gate.

• excited_pop (float | dict) – The excited state population at equilibrium.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

add_stochastic_pauli(px=0, py=0, pz=0, noisy_labels=None, noisy_gates=None)

Appends 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{split}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{split}$

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

By default, 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. However, noisy_labels and noisy_gates allow users to specify which gates and/or qubits are impacted by the noise. For example, if noisy_labels={0, 1} and noisy_gates={tq.Gate.x, tq.Gate.cz} then noise will only be applied in locations where an X gate acts on qubit 0 or 1 and where a CZ gate acts on qubits 0 and 1.

import trueq as tq

circuit = tq.Circuit([{(0,1): tq.Gate.cx, 2: tq.Gate.x, 3: tq.Gate.h}])

# initialize a simulator in which every location where a gate acts undergoes
# Pauli noise with px=0.01 and py=0.04
print(sim.sample(circuit, 1000))

# initialize a simulator where every X gate undergoes Pauli noise with
# py=pz=0 and px=0.04
print(sim.sample(circuit, 1000))

Results({'0000': 17, '0001': 25, '0010': 446, '0011': 422, '0100': 1, '0101': 2, '0110': 21, '0111': 17, '1001': 2, '1010': 24, '1011': 22, '1111': 1})
Results({'0000': 17, '0001': 17, '0010': 494, '0011': 472})

Parameters
• px (float) – The probability of an X error.

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

• pz (float) – The probability of a Z error.

• noisy_labels (int | Iterable) – An integer or iterable of integers specifying which qubit labels are affected by this noise process. All of a gate’s labels must be a subset of the noisy labels in order for it to be impacted by the noise. By default, noisy_labels is None which causes all qubits to undergo this noise process.

• noisy_gates (Gate | Iterable) – A gate or iterable of gates which are affected by this noise process. By default, noisy_gates is None and all gates will be acted upon by this noise process.

Return type

Simulator

Returns

This simulator instance so that add_*() calls can be chained.

## CyclePropagator¶

class trueq.simulation.simulator.CyclePropagator(propagation_fn, noise_only=False, dim=None)

Wraps a function that mutates quantum states or operators by given circuit cycles. This is a helper class for Simulator.

from trueq.simulation.simulator import CyclePropagator

# an ideal pure state simulator with no caching
def prop(cycle, state, cache):
for labels, gate in cycle:
state.apply_matrix(labels, gate.mat)
cycle_prop = CyclePropagator(prop)

Parameters
• propagation_fn (function) – 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. See also the documentation of noise-only.

• dim (None | int) – The Hilbert space dimension of each subsystem expected by this propagator. A value of None indicates that this propagator can propagate cycles of any dimension.

property dim

The Hilbert space dimension of each subsystem expected by this propagator.

Type

int

property noise_only

Whether this propagator only injects noise, as opposed to making an attempt to implement operations in a given cycle.

An example of a noise-only propagator is one that multiplies the state or superoperator by a depolarizing channel everytime a gate is encountered; it makes no attempt to actually simulate the gate.

An example of non-noisy-only propagator is an overrotation propagator that simulates the occurence of a gate $$G$$ with $$G^{1+\epsilon}$$; if $$\epsilon$$ is small, it does implement a gate close to $$G$$.

Type

bool