# 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,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 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.

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

`Circuit`

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

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 # 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) # 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})

- 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 qudit 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 qudits 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.

`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 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) # initialize a simulator which applies the above noise when a gate acts # on qubit 0 sim=tq.Simulator().add_kraus(kraus_ops, labels={0}) # initialize a simulator which applies the above noise when an X gate acts # on qubit 2 or 3 sim=tq.Simulator() sim.add_kraus(kraus_ops, noisy_labels={2, 3}, noisy_gates={tq.Gate.x})

- 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 qudit 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 qudits 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.

`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 # 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 # initialize a simulator in which overrotations are applied whenever a gate # acts on any qubits in {0, 2, 3} sim2 = tq.Simulator() sim2.add_overrotation(single_sys=0.02, multi_sys=0.01, noisy_labels={0, 2, 3})

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

noisy_labels(`int`

|`Iterable`

) – Which qudit 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 qudits 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.

`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 # 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. sim = tq.Simulator().add_povm(povm) # Initialize a circuit circuit = tq.Circuit([{(0, ): tq.Gate.h}, {(0, 1): tq.Gate.cnot}]) # Run the circuit on the simulator 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`

(default_error=None,errors=None)¶Adds measurment 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 qudits 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 # 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(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(0.01, {1: 0.05,3:[0.01,0.07]}) # we specify correlated measurement errors on qubits (3, 5) 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] ] tq.Simulator().add_readout_error( 0.01, {(3,5):confusion}) # 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]] tq.Simulator().add_readout_error(errors={2:confusion}) # 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]] tq.Simulator().add_readout_error(confusion)

- Parameters

default_error(`numpy.ndarray`

-like |`float`

|`float`

) – Default readout error specificed as a confusion matrix to be applied to all qudits 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. By default, the default error is no error.

errors(`dict`

) – A dictionary mapping tuples of qudit 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.

`add_stochastic_pauli`

(px=0,py=0,pz=0,noisy_labels=None,noisy_gates=None)¶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.

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 # 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) # initialize a simulator where every X gate undergoes Pauli noise with # py=pz=0 and px=0.04 sim = tq.Simulator().add_stochastic_pauli(px=0.04, noisy_gates=tq.Gate.x)

- Parameters

px(`float`

) – The probability of an X error.

py(`float`

) – The probability of a Y error.

pz– The probability of a Z error.

noisy_labels(`int`

|`Iterable`

) – An integer or iterable of integers specifying which qudit 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 qudits 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.