Linear Algebra

trueq.math.dekron(A, dim=2, force=False)

This takes a matrix which can be written as A = np.kron(a, b), and attempts to decompose it into (a, b) such that a and b are unitary.

This can also be used to find the closest unitary to a target matrix U in terms of the Frobenius norm:

dekron(U, dim=1, force=True)[1]
# This forces a decomposition into (a, b), where a.shape = (1, 1)
# this means that b is the same shape as the input U
Parameters
  • A (numpy.array) – A matrix to decompose into (a, b)

  • dim (int) – Dimension of the first submatrix a, dim=2 means a.shape = (2, 2)

  • force (bool) – If this is True, this forces a decomposition to the nearest possible (a, b) as defined by the Frobenous norm, where (a, b) are both unitary. If False then this will raise a DecompError if an exact decomposition is not found.

Returns

(a, b) - the matrices that attempt to make A = np.kron(a, b)

Return type

tuple

Raises

DecompError – if it cannot find a valid decomposition if force=False

trueq.math.split_gate(gate, labels=None, max_size=6)

Exhaustively attempts to factor the given trueq.Gate as a kronecker product of smaller gates. The output is a trueq.Cycle containing these factors and which subsystems they act on; this cycle implements a unitary equal to the given gate. If a trueq.Cycle is provided as input, then this procedure is performed on each of its gates, and single new factored cycle is returned.

Some worked examples:

# Generate a 3 qubit gate object that is the product of seperate single
# qubit gate operations. This is using the & shorthand for kronecker product.

gate = tq.Gate.x & tq.Gate.y & tq.Gate.z
# Gate({'XYZ': (1+0j)})

split_gate(gate=gate, labels=(0, 1, 2))
# Cycle((0,): Gate.x, (1,): Gate.y, (2,): Gate.z)

Another example involving 2 qubit operation between qubits (0, 2):

# Here we have a XX 90 Operation on qubits 0, 2, and a T gate on qubit 1
gate = tq.Gate.from_generators('XIX', 90) @ tq.Gate.from_generators('IZI', 45)

split_gate(gate)
# Cycle((0, 2): Gate(II, XX), (1,): Gate.t)

If a gate cannot be split, the returned Cycle will just contain the original gate with labels.

A tq.Cycle may also be provided instead of gate, if this is done, labels must be None, and the provided cycle will be altered in place instead of a new cycle being returned.

Note

This is an exponentially difficult operation as the number of qudits increases. Best and Worst case tested performance on a typical desktop computer, note that these are approximate times for single gate operations, and cycle split time should be the sum of all single gate split time present in them:

# of Qubits

Best

Worst

2 Qubits

0.5 ms

0.5 ms

3 Qubits

0.9 ms

1.2 ms

4 Qubits

1.4 ms

10 ms

5 Qubits

5.3 ms

250 ms

6 Qubits

0.03 s

22 sec

It is recommended to not attempt larger than 6 qubits or qudit equivalent.

Parameters
  • gate (trueq.Gate | trueq.Cycle) – The gate or cycle to be split into pieces if possible.

  • labels (tuple | list) – The given labels associated with provided gate. Should be omitted if the input is a cycle.

  • max_size (int) – Spliting will only be attempted if the number of subsystems is less than this value. If the number of subsystems exceed this, the original gate is returned unsplit.

Returns

A trueq.Cycle which contains the factored gates and associated labels.

Return type

trueq.Cycle

trueq.math.decompose_su4(given_gate, target_gate, max_depth=3, tol=1e-08)

Decompose a target 2 qubit trueq.Gate, into a layers of given 2 qubit trueq.Gate and \(SU(2)\otimes SU(2)\) operations.

decompose_su4(tq.Gate.cx, tq.Gate.swap)

This returns a trueq.Circuit using only 1 qubit unitaries and the provided given_gate to reconstruct the target_gate up to a global phase.

Parameters
  • target_gate (trueq.Gate) – The Gate to decompose into.

  • given_gate (trueq.Gate) – The Gate to use for the decomposition.

  • max_depth (int) – The maximum number of given_gates to use in the decompostion.

  • tol (float) – The tolerance for convergence as defined by process infidelity.

class trueq.math.QubitMode

Enumerates possible qubit gate decomposition modes. A decomposition mode of ZXZ, for example, results in decompositions of the form [("Z", angle1), ("X", angle2), ("Z", angle3)], where entries appear in chronological order, and nagles are specified in degrees

Some decoposition modes contain 5 rotation axes, such as ZXZXZ. These modes rely on identities of the form \(R_X(\theta)=R_Z(-\pi/2)R_X(\pi/2)R_Z(\pi-\theta)R_X(pi/2)R_Z(-pi/2)\). Therefore, these modes guarantee that the second and fourth rotation are always +90 degrees. This is useful when virtual gates are used for one rotation axis. If virtual Z gates are used, and the mode is ZXZXZ, then gates will be decomposed in the form [("Z", angle1), ("X", 90), ("Z", angle2), ("X", 90), ("Z", angle3)] and only one pulse shape is required to create any single qubit unitary.

property frame_change

A frame change matrix used when decomposing into this mode.

Type

numpy.ndarray

decompose(gate)

Decomposes the given gate into this mode, where the output format is a list of tuples of the form (pauli, angle).

>>> QubitMode.ZXZ.decompose(Gate([[0,-1j],[1j,0]]))
[("Z", 90), ("X", 180), ("Z", -90)]
Return type

list

validate_factories(factories)

Decides whether the given dictionary mapping single qubit Pauli strings to GateFactorys is sufficient to decompose this mode. It is checked that the factories are simple qubit rotations about the correct axes, and that factories that need to be able to rotate arbitrarily are able to do so. Here, “correct” means that the dictionary key is the same as the rotation axis of the factory.

In addition to these checks, the input is updated to contain a key of the form, ex “X90” if the mode is ZXZXZ, whose value is a trueq.NativeGate that performs an X90. This is because the given factories dictionary may have a fixed X90 factory, or a parameterized Xtheta factory, and we want to avoid performing the logic to figure this out during runtime.

Parameters

factories (dict) – dictionary mapping single qubit Pauli strings to GateFactorys.

Return type

bool

Raises

ValueError – If the factories are not sufficient to make arbitrary qubit gates in this mode.

trueq.math.kak(X)

Decompose an arbitrary 2 qubit unitary using the KAK decomposition.

Decomposition is of the form: np.kron(a0, a1) @ U @ np.kron(b0, b1) where U is a unitary with hamiltonian only containing II, XX, YY, ZZ terms

This returns (a0, a1), (b0, b1), and the generators for a Gate U

The original unitary can be reconstructed by:

A, B, k = kak(cnot)
gen = {p: v for p, v in zip(['XX', 'YY', 'ZZ'], k])}
np.kron(*A) @ tq.Gate.from_generators(gen).mat @ np.kron(*B)
Parameters

X (trueq.Gate) – A Gate object

Returns

(A, B, k), see above for a complete description

class trueq.math.Frame(frame)

A frame (that is, a spanning set) of a vector space.

# Make a frame with elements "U", "D" where U=[1, 0] and D=[0, 1]
b = Frame({"U": [1, 0], "D": [0, 1]})
Parameters

frame (dict) – a dictionary of frame elements.

shape(n_sys)

The shape of the vector space spanned by tensor products of Frame operators.

Parameters

n_sys (int) – the number of tensor factors.

Return type

tuple

labels(n_sys)

An iterator of basis element labels for the entire system. The order is the same as elements.

b = Frame({"U": [1, 0], "D": [0, 1])
list(b.labels(2))
["UU", "UD", "DU", "DD"]
Parameters

n_sys (int) – a positive integer specifying the number of systems under consideration.

Return type

map

get_element(label)

Returns the array corresponding to the given label or the kronecker product of the arrays for an iterable of labels.

b = Frame({"U": [1, 0], "D": [0, 1]})
b.get_element("D")
array([0, 1])

b = Frame({"U": [1, 0], "D": [0, 1])
b.get_element("DU")
array([0, 0, 1, 0])
Parameters

label (Iterable) – Which element to fetch.

Return type

numpy.ndarray

elements(n_sys=1)

An iterator of frame elements for multiple systems. The order is the same as basis_labels.

b = Frame({"U": [1, 0], "D": [0, 1])
list(b.elements(2))
[
    array([1, 0, 0, 0]),
    array([0, 1, 0, 0]),
    array([0, 0, 1, 0]),
    array([0, 0, 0, 1])
]
Parameters

n_sys (int) – a positive integer specifying the number of systems under consideration.

Return type

map

n_sys(arr)

Determine how many systems an array acts upon.

Parameters

arr (array-like) – An array with shape equal to the shape of this basis.

Return type

int

to_coeffs(arr)

Expands the given array in this frame, returning a 1D vector in the same order as basis_labels. This function is the inverse of from_coeffs().

Parameters

arr (array-like) – An array with shape consistent with the shape of this frame for some positive integer.

Return type

numpy.ndarray

from_coeffs(coeffs)

Takes a 1D array, interprets it as a list of coefficients with respect to this basis, and returns the array resulting from summing up these coefficients times their corresponding basis element. This function in the inverse of to_coeffs().

Parameters

coeffs (array-like) – A 1D array of coefficients.

Return type

numpy.ndarray

to_expansion(arr)

Expands the given array in this basis, returning a dictionary mapping basis_labels to coefficients.

Note

Only non-zero coefficients are normally included. However, in the case where arr is the zero array, instead of returning an empty dictionary, a dictionary with the first member basis_labels mapped to \(0\) is returned.

Parameters

arr (array-like) – An array with shape consistent with the shape of this basis for some positive integer.

Return type

dict

from_expansion(expansion)

Takes a dictionary mapping basis elements to coefficients and returns the array resulting from summing up these coefficients times their corresponding frame element. This function in the inverse of to_expansion().

Parameters

expansion (dict) – A dictionary mapping basis elements to coefficients.

Return type

numpy.ndarray

plot_arr(arr, ax=None)

Plots the given array as the coefficients in this basis/frame.

Parameters
  • arr (array-like) – An array with shape consistent with the shape of this basis for some positive integer or a list thereof.

  • ax (matplotlib.Axis) – An existing axis to plot on. If not given, a new figure will be created.

plot_superoperator(arr, ax=None)

Plots the superoperator that acts by conjugation by an array or a list of arrays on a state expanded in this frame.

Parameters
  • arr (array-like) – An array with shape consistent with the shape of this basis for some positive integer or a list thereof.

  • ax (matplotlib.Axis) – An existing axis to plot on. If not given, a new figure will be created.

superoperator(arr)

Returns the superoperator that acts by conjugation by an array or a list of arrays on a state expanded in this frame.

Parameters

arr (array-like) – An array with shape consistent with the shape of this basis for some positive integer or a list thereof.

Return type

numpy.ndarray

sparse_superoperator(arr)

Returns a sparse representation of the superoperator that acts by conjugation by an array or a list of arrays on a state expanded in this frame.

Parameters

arr (array-like) – An array with shape consistent with the shape of this basis for some positive integer or a list thereof.

Return type

dict

class trueq.math.OperatorTensor(dim=2, spawn=None, value=None, dtype=None)

Represents an operator (e.g. unitary) or superoperator (e.g. CPTP channel) on a multipartite qudit system. This is one of the main output formats of the Simulator. It differs from just a pure matrix, such as a numpy.ndarray, in that it also stores qudit label information, and attempts to store a sparse representation when the operator is not entangling between various subsystems.

Depending on the types of noise in your simulator, this operator could be a unitary or a superoperator. This will affect the dimension. You can check which case you have with OperatorTensor.is_superop. You can get the matrix representation with OperatorTensor.mat().

import trueq as tq

# construct a circuit that makes a cat state
circuit = tq.Circuit()
circuit.add_cycle({(0,): tq.Gate.h})
for idx in range(4):
    circuit.add_cycle({(idx, idx+1): tq.Gate.cnot})

# use a simulator to fetch the unitary of this circuit as an OperatorTensor
op = tq.Simulator().operator(circuit)

# grab the unitary itself
U = op.mat()

# get the superoperator matrix representation
op.upgrade()
S = op.mat()

What follows is information intended for users that want to do a bit more than get the matrix representation of an operator. This class has three main distinctions from its parent class Tensor:

  1. The datatype is numpy.complex128 by default

  2. The mat() and value() methods return a more convenient tensor order and shape by default.

  3. The upgrade() method, which transforms this operator into a superoperator mixed state by taking appropriate kroneckers and changing the input and output shapes from (d,) to (d,d). Certain methods automatically call upgrade(), for example, when a superoperator shaped matrix is given to apply_matrix().

import trueq as tq
import numpy as np

# make a unitary channel for a tensor product of qubits
psi = tq.math.OperatorTensor(2)

# we can manually upgrade it to a superoperator
rho = psi.upgrade()

# or we can automatically upgrade it to a superoperator by doing something that
# requires a superoperator. here, we apply something of superoperator size,
# rather than unitary size
psi = tq.math.OperatorTensor(2)
psi.apply_matrix((0,), np.eye(4))

# we can also create a superoperator on instatiation based on spawn/value shapes
rho = tq.math.OperatorTensor(2, spawn=np.eye(4))

Note

This class does not assume that the represented channel or anything applied to it is unitary, CP, TP, unital, etc.: it is just stores and manipulates numerical arrays.

Parameters
  • dim (int) – The Hilbert space dimension of a single subsystem.

  • spawn (dict) – The default operator to set a subsystem to when it’s label is referred to, but it does not exist yet. This spawn can be operator or superoperator. See also the constructor Tensor.

  • value – The initial value of this state, which can each be operator or superoperator. See also the constructor of Tensor.

  • dtype (type) – The datatype of this operator.

property is_superop

Whether this tensor represents a superoporator (as opposed to an operator).

Type

bool

property dim

The subsystem Hilbert space dimension that this operator acts on.

Type

int

upgrade()

If this operator represents a unitary, upgrades it to a tensor representing a superoperator. If this tensor already represents a superoperator, does nothing.

Returns

This instance.

Return type

OperatorTensor

value(order='composite-flat')

Returns a dictionary mapping tuples of subsystem labels to operators on those subsystems. Operators acting on n subsystems will have shape (d**n,d**n) or (d**(2*n),d**(2*n)) depending on the current value of is_superop.

Parameters

order (str) – Advanced users see Tensor.value().

Return type

dict

mat(order='composite-flat')

Returns the matrix form of this operator. The tensor order is the sorted list of all present labels. If n subsystems are present, this matrix will have shape (d**n,d**n) or (d**(2*n),d**(2*n)) depending on the current value of is_superop.

If this is a superoperator, then the basis used is the composite row-stacking basis. For example, suppose you wanted to apply this superoperator to a density matrix rho on 3 qubits that has shape (8, 8). Then one could use the following to get the output density matrix:

# op is this OperatorTensor
op.mat().dot(rho.flatten()).reshape(8, 8)

Note

Calling this method will create an array that is exponentially large in the number of subsystems, and causes any distinct subsystems found in value to be permanently merged; calling this method mutates the tensor’s internal representation.

Parameters

order (str) – Advanced users see Tensor.value().

Return type

numpy.ndarray

apply_matrix(labels, matrix)

Mutates this operator by applying the given square matrix to the given labels.

If acting on \(n\) subsystems, this matrix can be a \(d^n \times d^n\) unitary, or a \(d^{2n}\times d^{2n}\) superoparator. If this operator is currently a unitary and a superoperator is applied, upgrade() is called. If this state is currently a superoperator and a unitary is applied, the unitary is made into a superoperator before applying.

import trueq as tq
import numpy as np
t = tq.math.OperatorTensor(2)

# apply a unitary
t.apply_matrix((0, 1), tq.Gate.cnot.mat)

# apply a 1% bitflip superoperator
s = 0.99 * np.eye(4) + 0.01 * np.kron(tq.Gate.x, tq.Gate.x)
t.apply_matrix((1,), s)

The vectorization convention of superoperators is important. Mixed states are stored in a subsystem-wise row-stacked vectorization convention, and therefore superoperators need to use this convention too. See to_rowstack_super().

Parameters
  • labels (tuple) – Which subsystems to apply the matrix to.

  • matrix (numpy.ndarray-like) – A square matrix.

class trueq.math.StateTensor(dim=2, spawn=None, value=None)

Represents a pure or mixed state on a multipartite qudit system. This is one of the main output formats of the Simulator. It differs from just a pure matrix, such as a numpy.ndarray, in that it also stores qudit label information, and attempts to store a sparse representation when the state is not entangled between various subsystems.

Depending on the types of noise in your simulator, this state could be pure or mixed. This will affect the dimension. You can check which case you have with StateTensor.is_mixed. You can get the matrix representation with StateTensor.mat().

import trueq as tq

# construct a circuit that makes a cat state
circuit = tq.Circuit()
circuit.add_cycle({(0,): tq.Gate.h})
for idx in range(4):
    circuit.add_cycle({(idx, idx+1): tq.Gate.cnot})

# use a simulator to fetch the state of this circuit as a StateTensor
state = tq.Simulator().state(circuit)

# grab the 1D pure state vector
psi = state.mat()

# get the density matrix representation
state.upgrade()
rho = state.mat()

What follows is information intended for users that want to do a bit more than get the matrix representation of a state. This class has three main distinctions from its parent class Tensor:

  1. The datatype is numpy.complex128

  2. The mat() and value() methods return a more convenient tensor order and shape by default.

  3. The upgrade() method, which transforms this state from a pure state into a mixed state by taking appropriate outerproducts and changing the output shape from (d,) to (d,d). Certain methods automatically call upgrade(), for example, when a superoperator shaped matrix is given to apply_matrix().

import trueq as tq
import numpy as np

# make a pure state on a tensor product of qubits
psi = tq.math.StateTensor(2)

# we can manually upgrade it to a mixed state
rho = psi.upgrade()

# or we can automatically upgrade it to a mixed state by doing something that
# requires a mixed state. here, we apply something of superoperator size, rather
# than unitary size
psi = tq.math.StateTensor(2)
psi.apply_matrix((0,), np.eye(4))

# we can also create a mixed state on instatiation based on spawn/value shapes
rho = tq.math.StateTensor(2, spawn=[[1, 0], [0, 0]])

Note

This class does not assume that the represented state is positive or that it has unit trace, or that applied operators are unitary, CP , TP, unital, etc.: it just stores and manipulates numerical arrays.

Parameters
  • dim (int) – The Hilbert space dimension of a single subsystem.

  • spawn (dict) – The default state to set a subsystem to when it’s label is refered to, but it does not exist yet. This spawn can be pure or mixed. See also the constructor Tensor.

  • value – The initial value of this state, which can each be pure or mixed. See also the constructor of Tensor.

property is_mixed

Whether this state currently is a mixed state (as opposed to a pure state).

Type

bool

property dim

The subsystem Hilbert space dimension of this state.

Type

int

upgrade()

If this state represents a pure state, upgrades it to a mixed state by taking outer products. If this state is already a mixed state, does nothing.

Returns

This instance.

Return type

StateTensor

value(order='composite-group')

Returns a dictionary mapping tuples of subsystem labels to states on those subsystems. If this state is currently pure, then states will be 1D vectors, and if mixed, then states will be 2D density matrices.

Parameters

order (str) – Advanced users see Tensor.value().

Return type

dict

mat(order='composite-group')

If this state is currently pure, returns the pure state vector, otherwise returns the density matrix.

Note

Note that this property will create an array that is exponentially large in the number of subsystems, and causes any distinct subsystems found in value() to be permanently merged; calling this attribute mutates the tensor’s internal representation.

Parameters

order (str) – Advanced users see Tensor.value().

Return type

numpy.ndarray

apply_matrix(labels, matrix)

Mutates this state by applying the given square matrix to the given labels.

If acting on \(n\) subsystems, this matrix can be a \(d^n \times d^n\) unitary, or a \(d^{2n}\times d^{2n}\) superoparator. If this state is currently pure and a superoperator is applied, upgrade() is called. If this state is currently mixed and a unitary is applied, the unitary is made into a superoperator before applying.

import trueq as tq
import numpy as np
t = tq.math.StateTensor(2)

# apply a unitary
t.apply_matrix((0, 1), tq.Gate.cnot.mat)

# apply a 1% bitflip superoperator
s = 0.99 * np.eye(4) + 0.01 * np.kron(tq.Gate.x, tq.Gate.x)
t.apply_matrix((1,), s)

The vectorization convention of superoperators is important. Mixed states are stored in a subsystem-wise row-stacked vectorization convention, and therefore superoperators need to use this convention too. See to_rowstack_super().

Parameters
  • labels (tuple) – Which subsystems to apply the matrix to.

  • matrix (numpy.ndarray-like) – A square matrix.

marginalize(labels)

Removes the provided labels of this tensor by taking the partial trace. This will usually upgrade() a pure state to a mixed state.

The special case of no upgrade happens when this state is pure and all the labels being traced-out are known to be unentangled with other labels (this is known when every label tuple in the keys of value() is either a strict subset of the given labels, or does not intersect at all with the given labels).

In the case where all labels in the tensor are traced out, label (0,) is re-added so that the total trace of the state can be preserved.

Parameters

labels (tuple) – Which labels to remove. The order does not matter.

Returns

This instance.

Return type

StateTensor

probabilities(povm=None)

Computes the probabilities of all possible measurement outcomes. If a Positve Operator Valued Measurement (POVM) is not provided, then these are the probabilities of computational basis measurements.

import trueq.math as tqm
import numpy as np

# compute probability distribution of a qubit superposition
psi = tqm.StateTensor(2, value={(0,): np.array([1, -1j]) / np.sqrt(2)})
psi.probabilities()
# Tensor(<[(2,), ()] on labels [(0,)]>)
psi.probalities().value()
# {(0,): array([0.5, 0.5])}
psi.probabilities().sample(100)
# Results({"0": 46, "1": 54})

# define a POVM corresponding to readout error that has a 1% chance of
# flipping a |0> measurement and a 10% chance of flipping a |1> measurement
p00 = 0.99
p11 = 0.9
povm = [[[p00, 0], [0, 1-p11]], [[1-p00, 0], [0, p11]]]
povm = tqm.Tensor((2,), (2, 2), spawn=povm)
psi.probabilities(povm).value()
# {(0,): array([0.495, 0.005])}
Parameters

povm (Tensor) – A Tensor with shape ((k,), (d, d)) where d is the Hilbert space dimension, and (k,) is the number of possible outcomes per subsystem. Each slice [i,:,:] should be a positive matrix, and the sum over i should be the identity matrix, to be probability preserving.

Return type

dict

class trueq.math.Superop(mat)

Represents a quantum superoperator. A superoperator is a linear operation that maps density matrices to density matrices. The main utility of this class is converting between the various superoperator representations such as Kraus operators, Choi matrices, and Pauli transfer matrices.

Note

This class is in support of future features, and is not currently connected to other parts of the library. However, it is still fully functional as a stand-alone utility, and can be used, for instance, to generate inputs to the simulator method add_kraus.

Generally, instances of this class should be created by calling one of the static methods starting with from_. For example:

superop = Superop.from_kraus([np.eye(2)])
Parameters

mat (numpy.ndarray-like) – A superoperator matrix in the row-stacking basis.

property dim

The Hilbert space dimension that this superoperator acts on. For example, this is equal to 4 if this superoperator acts on \(4 \times 4\) density matrices.

Type

int

property choi_rank

The rank of the Choi matrix of this superoperator, i.e. the minimal number of Kraus operators required to describe this superoperator.

Type

int

property is_tp

Whether this superoperator is trace preserving (TP), i.e. whether the trace of output density matrices is always equal to the trace of input density matrices.

Type

bool

property is_unital

Whether this superoperator is unital, i.e. whether it maps the completely mixed state to the completely mixed state.

Type

bool

property is_cp

Whether this superoperator is completely positive (CP), i.e. whether this superoperator always outputs positive states when given positive states (even when this superoperator is tensored with an ancilla space).

Type

bool

property is_cptp

Whether this superoperator is completely positive and trace-preserving (CPTP). See also is_cp and is_tp.

Type

bool

static from_kraus(*ops)

Instantiates a new Superop from a given set of Kraus operators. A superoperator in the Kraus representation is a set of \(m\) operators \({K_0,\ldots,K_{m-1}}\) that act on a given density matrix by the formula \(\rho\mapsto\sum_{i=0}^{m-1}K_i \rho K^\dagger_i\). A superoperator admits a Kraus representation if and only if it is completely positive (see is_cp), but it is only trace preserving (see is_tp) if \(\sum_{k=0}^{m-1}K^\dagger_i K_i = \mathbb{I}\).

import numpy as np
import trueq as tq

# instantiate a depolarizing channel
p = 0.05
superop = tq.math.Superop.from_kraus(
    np.sqrt(1-p) * tq.Gate.id.mat,
    np.sqrt(p / 3) * tq.Gate.x.mat,
    np.sqrt(p / 3) * tq.Gate.y.mat,
    np.sqrt(p / 3) * tq.Gate.z.mat
)
Parameters

ops – A sequence or list of Kraus operators.

Return type

Superop

property kraus

Returns a Kraus representation of this superoperator; see from_kraus() for details about the representation. Note that Kraus representations are not unique, and this decomposition is only possible if the superoperator is CP (see is_cp).

Returns

A 3-D array ops of shape (n_ops, dim, dim) where ops[i,:,:] is the \(i^{th}\) Kraus operator.

Return type

numpy.ndarray

plot_kraus(n_columns=4, abs_max=None, axes=None)

Plots a Kraus representation of this superoperator; see from_kraus() for details about the representation. Note that Kraus representations are not unique.

Parameters
  • n_columns (int) – If new axes are made, how many columns of subplots to use.

  • abs_max (None | float) – The value to scale absolute values of the matrix by; the value at which plotted colors become fully opaque. By default, this is the largest absolute magnitude of the input matrix.

  • axes (Iterable | None) – An existing list of axes to plot on. They are created otherwise.

static from_unitary(u)

Instantiates a new Superop from a given unitary matrix. A unitary matrix \(U\) acts on a given matrix by the formula \(\rho\mapsto U\rho U^\dagger\).

import trueq as tq

# instantiate a CNOT as a superoperator
superop = tq.math.Superop.from_unitary(tq.Gate.cnot)
Parameters

u (numpy.ndarray) – A unitary matrix.

Return type

Superop

static from_function(fcn, dim)

Instantiates a new Superop from a python function.

import trueq as tq

def depolarizing(rho, p=0.05):
    d = rho.shape[0]
    return (1 - p) * rho + p * np.eye(d) * np.trace(rho) / d

# instantiate a qubit depolarizing channel
superop = tq.math.Superop.from_function(depolarizing, dim=2)

# instantiate a qutrit depolarizing channel
superop = tq.math.Superop.from_function(depolarizing, dim=3)
Parameters
  • fcn (function) – A function that takes a density matrix and returns a density matrix of the same shape.

  • dim (int) – The dimension of Hilbert space.

Return type

Superop

static from_ptm(ptm)

Instantiates a new Superop from a Pauli transfer matrix (PTM). A PTM represents how density matrices are transformed when expanded in the Pauli basis. In particular, suppose for \(n\) qubits, the Pauli matrices are denoted by \(\{P_i\}_{i=0}^{4^n-1}\), and sorted lexicographically. For example, for \(n=2\), then we have the ordering

\[\{P_i\}_{i=0}^{15} =\{II, IX, IY, IZ, XI, XX, XY, XZ, YI, YX, YY, YZ, ZI, ZX, ZY, ZZ\}.\]

Given any density matrix \(\rho\), we can expand it in the Pauli basis as \(\rho=\sum_{i=4^n-1} c_i P_i\). A PTM \(A\) performs the transformation \(c\mapsto A c\) where \(c\) is the vector \(c=(c_0,c_1,...,c_{4^n-1})\).

import numpy as np
import trueq as tq

# instantiate a qubit depolarizing channel
superop = tq.math.Superop.from_ptm(np.diag([0.97, 0.01, 0.01, 0.01]))
Parameters

ptm (numpy.ndarray) – A PTM. This must correspond to a dimension that is a power of 2.

Return type

Superop

property ptm

Returns the Pauli transfer matrix representation of this superoperator; see from_ptm() for details on this representation. Note that the dim of this superoperator must be a power of 2 for this to be valid.

Returns

The Pauli transfer matrix.

Return type

numpy.ndarray

plot_ptm(abs_max=None, ax=None)

Plots the Pauli transfer matrix representation of this superoperator; see from_ptm() for details on this representation.

Parameters
  • abs_max (None | float) – The value to scale absolute values of the matrix by; the value at which plotted colors become fully opaque. By default, this is the largest absolute magnitude of the input matrix.

  • ax (matplotlib.Axis | None) – An existing axis to plot on. One is created otherwise.

static from_choi(choi)

Instantiates a new Superop from a Choi matrix. If a channel \(\Lambda\) transforms a given density matrix as \(\rho\mapsto \Lambda(\rho)\), then the Choi matrix (in the row convention) is equal to \(\Lambda_r=\sum_{i,j=0}^{d-1}\Lambda(E_{i,j})\otimes E_{i,j}\) where \(d\) is the dimension, and \(E_{i,j}\) is the \(d\times d\) matrix of zeros with a 1 in the \((i,j)^\text{th}\) entry.

The Choi matrix is positive semidefinite if and only if the superoperator it represents is completely positive (see is_cp). Therefore, there is a correspondence between completely positive superoperators and density matrices (which must also be positive semidefinite) that is often called the Choi-Jamiolkowski isomorphism.

import numpy as np
import trueq as tq

# instantiate a random CPTP qubit channel
superop = tq.math.Superop.from_choi(tq.math.random_density(4))
Parameters

choi (numpy.ndarray) – A Choi matrix.

Return type

Superop

property choi

Returns the Choi matrix representation of this superoperator; see from_choi() for details on this representation.

Returns

The Choi matrix.

Return type

numpy.ndarray

plot_choi(abs_max=None, ax=None)

Plots the Choi representation of this superoperator; see from_choi() for details on this representation.

Parameters
  • abs_max (None | float) – The value to scale absolute values of the matrix by; the value at which plotted colors become fully opaque. By default, this is the largest absolute magnitude of the input matrix.

  • ax (matplotlib.Axis | None) – An existing axis to plot on. One is created elsewise.

static from_rowstack(mat)

Instantiates a new Superop from a Liouville superoperator matrix in the row-stacking basis. Note that since this is the native basis of the Superop class, this method is equivalent to the constructor.

This is the best representation for superopertors for simulation if density matrices are C-ordered (i.e. row-major, so that entire rows are contiguous in memory). This is because if our row-stacking superoperator matrix is \(M\) and our density matrix \(\rho\), then if we take the rows of \(\rho\) and stack them together into one column vector, denoted \(\operatorname{vec}(\rho)\), then the output density matrix is given by regular matrix multiplication, \(\operatorname{vec}^{-1}(M\operatorname{vec}(\rho))\), where \(\operatorname{vec}^{-1}\) is the unstacking operator.

import numpy as np
import trueq as tq

# instantiate the identity channel on a qutrit
superop = tq.math.Superop.from_rowstack(np.eye(9))
Parameters

mat (numpy.ndarray) – A superoperator in the row-stacking basis.

Return type

Superop

property rowstack

Returns the matrix representation of this superoperator in the row-stacking basis; see from_rowstack() for details on this representation.

Returns

The superoperator matrix.

Return type

numpy.ndarray

plot_rowstack(abs_max=None, ax=None)

Plots the matrix representation of this superoperator in the row-stacking basis; see from_rowstack() for details on this representation.

Parameters
  • abs_max (None | float) – The value to scale absolute values of the matrix by; the value at which plotted colors become fully opaque. By default, this is the largest absolute magnitude of the input matrix.

  • ax (matplotlib.Axis | None) – An existing axis to plot on. One is created elsewise.

static from_colstack(mat)

Instantiates a new Superop from a superoperator in the column-stacking basis.

This is the best representation for superopertors for simulation if density matrices are Fortran-ordered (i.e. column-major, so that entire columns are contiguous in memory). This is because if our column-stacking superoperator matrix is \(M\) and our density matrix \(\rho\), then if we take the columns of \(\rho\) and stack them together into one column vector, denoted \(\operatorname{vec}(\rho)\), then the output density matrix is given by regular matrix multiplication, \(\operatorname{vec}^{-1}(M\operatorname{vec}(\rho))\), where \(\operatorname{vec}^{-1}\) is the unstacking operator.

import numpy as np
import trueq as tq

# instantiate the identity channel on a qutrit
superop = tq.math.Superop.from_colstack(np.eye(9))
Parameters

mat (numpy.ndarray) – A superoperator in the column-stacking basis.

Return type

Superop

property colstack

Returns the matrix representation of this superoperator in the column-stacking basis; see from_colstack() for details on this representation.

Returns

The superoperator matrix.

Return type

numpy.ndarray

plot_colstack(abs_max=None, ax=None)

Plots the matrix representation of this superoperator in the col-stacking basis; see from_colstack() for details on this representation.

Parameters
  • abs_max (None | float) – The value to scale absolute values of the matrix by; the value at which plotted colors become fully opaque. By default, this is the largest absolute magnitude of the input matrix.

  • ax (matplotlib.Axis | None) – An existing axis to plot on. One is created elsewise.

apply(state)

Applies this superoperator to a given density matrix or pure state. The output is always a density matrix of shape (dim, dim).

# make a superoperator acting on two qutrits
s = tq.math.Superop(np.random.randn(2, 9, 9))
# apply to a pure state
s.apply([1, 0, 0])
Parameters

state (numpy.ndarray-like) – An input density matrix or pure state of the correct shape.

Return type

numpy.ndarray

class trueq.math.Tensor(output_shape, input_shape=None, spawn=None, value=None, dtype=None)

Represents a multipartite tensor on an arbitrary number of subsystems. Each subsystem owns a number of ordered wires (or indices), which can each be input or output wires (eg. lower and upper indices in Einstein’s notation). One important restriction is that all subsystems have the same wire description: the same number, dimensions, and order for input and output wires.

Here are some motivating cases for various output and input shapes output, input:

  • (d,), (): Pure states on a multipartite qudit system. Each subsystem has a single output wire of dimension \(d\) and no input wires (ie. a column vector). See StateTensor.

  • (d,), (): Categorical probability distributions on tensor product of dits. Each subsystem has a single output wire of dimension \(d\) and no input wires (ie. a column vector).

  • (d,d), (): Vectorized mixed states on a multipartite qudit system. Each subsystem has two output wires each with dimension \(d\) and no input wires. The first output wire represents the output wire of the unvectorized state, and the second output wire the input wire of the unvectorized state (this is the row-stacking convention). See StateTensor.

  • (d,), (d,): Unitaries acting on a multipartite qudit system. Each subsystem has a single output wire of dimension \(d\) and a single input wire of dimension \(d\) (ie. a square matrix). See OperatorTensor.

  • (d,d), (d,d): Superoperators acting on a multipartite qudit system. Each subsystem has two input wires and two output wires, all of dimension \(d\). The two input wires act on a vectorized density matrix (or contract against another superoperator in composition). See OperatorTensor.

A main feature of this class is the ability to efficiently left-multiply square matrices onto specific subsystems (apply_matrix()), which can be strictly smaller than all of the subsystems present in the tensor. Therefore this class can be used to maintain the state of a simulation, and is general enough to track pure states, mixed states, unitaries, and superoperators.

import trueq as tq
import numpy as np
t = tq.math.Tensor(2, 2, spawn=np.eye(2))
t.apply_matrix((0, 2), tq.Gate.cnot.mat)
t.apply_matrix((1,), tq.Gate.h.mat)
# U will be the matrix corresponding to the circuit {(0, 2): cnot, (1,): h}
U = t.mat().reshape(8, 8)

The internal storage format of this class is a dictionary mapping labels to numpy arrays, such as the following:

{
    (0,): [[0, 1], [1, 0]],
    (2, 3): [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]
}

In this case the label (0,) is storing an X gate, and the labels (2, 3) are storing a CNOT gate.

Parameters
  • output_shape (tuple | int) – The output wire dimension, or a tuple of output wire dimensions. Set to an empty tuple for no output wires.

  • input_shape (tuple | int) – The input wire dimension, or a tuple of input wire dimensions. Set to an empty tuple (or None) for no input wires.

  • spawn (numpy.ndarray-like) – The default value to assign to new subsystems. The size must match the provided input and output shapes. By default, this parameter is set to the tensor of all zeros, except the first element, which is 1.

  • value (dict) – The initial value of the tensor, set as a dictionary mapping tuples of subsystem indexes to tensors of the correct shape.

  • dtype – The datatype of the arrays, numpy.float64 by default.

property dtype

The data type of this tensor.

Type

type

property labels

Which subsystem labels of this tensor have a value.

Type

tuple

property n_sys

The number of subsystem labels with a value

Type

int

property shape

The output and input shape of each subsystem, eg. ((2, 2), (2, 2)). See the arguments input_shape and output_shape of the constructor of Tensor.

Type

tuple

property total_dim

The total dimension of this tensor; the total subsystem dimension to the power of the number of subsystems.

Type

int

property spawn

The arrary that is assigned to new subsystems by default.

Type

numpy.ndarray

conj(copy=True)

Takes the complex conjugate of each array in this tensor.

Parameters

copy (bool) – Whether to mutate the given tensor, or to create a copy and leave the original intact.

Return type

Tensor

adj(copy=True)

Takes the complex conjugate and transpose of each array in this tensor. Here, transpose refers to swapping the input shape with the output shape.

Parameters

copy (bool) – Whether to mutate the given tensor, or to create a copy and leave the original intact.

Return type

Tensor

transpose(output_perm, input_perm, copy=True)

Permutes wire dimensions of this tensor, possibly switching input wires with output wires.

# turn a column vector into a row vector
t = Tensor((2,))
# ((2,), ())
t.transpose([], [0]).shape
# ((), (2,))

# vectorize a matrix with row-stacking
t = Tensor((2,), (2,))
# ((2,), (2,))
t.transpose([0, 1], []).shape
# ((2, 2), ())

# vectorize a matrix with column-stacking
t = Tensor((2,), (3,))
# ((2,), (3,))
t.transpose([1, 0], []).shape
# ((3, 2), ())

# shuffle some wires
t = Tensor((2, 3), (5,))
t.transpose([2, 1], [0]).shape
# ((5, 3), (2,))
Parameters
  • output_perm (Iterable) – A list of indices referencing positions in the joint tuple shape=output_shape+input_shape. This value along with input_perm must uniquely refrence every integer 0, 1,..., len(shape).

  • output_perm – A list of indices referencing positions in the joint tuple output_shape+input_shape.

  • copy (bool) – Whether to mutate the given tensor, or to create a copy and leave the original intact.

Return type

Tensor

value(order='subsystem')

Returns the value of this tensor: a dictionary mapping tuples of subsystem labels to array values.

The argument of this method changes the order of indexes of the arrays. For simulation performance reasons, the internal storage arranges them subsystem-wise, so that all input wires for a given subsystem are contiguous, and likewise all output wires for a given subsystem are contiguous, and this is the default order of this method. This can optionally be changed so that indices are riffled over subsystems.

As an example, suppose the input shape is (2, 4) and the output shape is (5,) and that there are 3 subsystems in an array. Then the array will have shape (2, 4, 2, 4, 2, 4, 5, 5, 5) if the order is "subsystem" (default), or the array will have shape (2, 2, 2, 4, 4, 4, 5, 5, 5) if the order is "composite". As a convenience, an order of "composite-group" flattens each group of subsystems, giving a shape (8, 64, 125) in this example, and an order of "composite-flat" flattens all inputs and all outputs separately, giving a shape (512, 125) in this example.

Parameters

order (str) – Which order to arrange the wires, either "subsystem", "composite", "composite-group", or "composite-flat". Or, one of "”s”, ``"c", "cg", or cf”`` for short.

Return type

dict

mat(order='subsystem')

Returns the full matrix representation of this tensor, with subsystems ordered by sorted labels.

Note that this property will create an array that is exponentially large in the number of subsystems, and causes any distinct subsystems found in value() to be permanently merged; calling this attribute mutates the tensor’s internal representation.

The argument of this method changes the order of indexes of the arrays. For simulation performance reasons, the internal storage arranges them subsystem-wise, so that all input wires for a given subsystem are contiguous, and likewise all output wires for a given subsystem are contiguous, and this is the default order of this method. This can optionally be changed so that indices are riffled over subsystems.

As an example, suppose the input shape is (2, 4) and the output shape is (5,) and that there are 3 subsystems in an array. Then the array will have shape (2, 4, 2, 4, 2, 4, 5, 5, 5) if the order is "subsystem" (default), or the array will have shape (2, 2, 2, 4, 4, 4, 5, 5, 5) if the order is "composite". As a convenience, an order of "composite-group" flattens each group of subsystems, giving a shape (8, 64, 125) in this example, and an order of "composite-flat" flattens all inputs and all outputs separately, giving a shape (512, 125) in this example.

Parameters

order (str) – Which order to arrange the wires, either "subsystem", "composite", "composite-group", or "composite-flat". Or, one of "”s”, ``"c", "cg", or cf”`` for short.

Return type

numpy.ndarray

add_labels(labels)

Updates the number of subsystems to include labels provided, if they don’t already exist. If the labels are already present, this call doesn’t change anything.

Parameters

labels (list) – A list of labels to be added to this tensor.

Returns

This instance.

Return type

Tensor

upgrade()

Does nothing. This is to be used by subclasses that require a mutation of shape on a call to marginalize. See, for instance, StateTensor.upgrade() and OperatorTensor.upgrade().

Returns

This instance.

Return type

Tensor

marginalize(labels)

Removes the provided labels of this tensor through marginalization. This is an in-place operation that mutates the tensor. Here, marginalization is defined as the sum over all tensor indices of the provied labels.

import trueq as tq
import numpy as np

# Make a random probability distribution on length-4 bitstrings
S = np.random.rand(16)
p = tq.math.Tensor(2, value={(0, 1, 2, 3): S / S.sum()})

# Marginalize out two of the bitstrings
p.marginalize((1, 2))

In the case where labels is a superset of some label in value(), the entire entry is removed. However, its sum is still computed and multiplied onto some other entry so that the overall sum of the tensor is preserved. In the case where all labels in the tensor are marginalized, label (0,) is re-added so that the total sum of the tensor can be preserved.

Parameters

labels (tuple) – Which labels to remove. The order does not matter.

Returns

This instance.

Return type

Tensor

update(other)

Updates this tensor with some new values. If values are placed on labels that don’t exist, creates and sets them. If values are placed on labels that already exist, they are first removed by calling marginalize().

Parameters

other (dict | Tensor) – A dict like Tensor.value(), or another Tensor. In either case, the size of subsystems in other needs to match this tensor.

Returns

This instance.

Return type

Tensor

apply_matrix(labels, matrix)

Mutates this tensor by applying the given square matrix to the given labels.

The size of the matrix must be compatible with the output shape of this tensor, just as with regular matrix multiplication, \(AB\), the number of columns of \(A\) must match the number of rows of \(B\). The rule enforced here is that the number of rows of the given matrix, when reshaped into a square, must be equal to the product if this tensors output wire dimensions. Square matrices are enforced because we cannot change the output shape on only some of the subsystems.

import trueq as tq
import numpy as np
t = tq.math.Tensor((2, 3), (2,))
# we need a 6x6 since output shape is (2, 3)
t.apply_matrix((0,), np.random.randn(6, 6))
# we need a 36x36 since we are applying to 2 systems
t.apply_matrix((0, 3), np.random.randn(36, 36))
Parameters
  • labels (tuple) – Which subsystems to apply the matrix to.

  • matrix (numpy.ndarray-like) – A square matrix.

dot(rhs, dtype=None, overwrite_rhs=False)

Contracts this tensor with another tensor, returning a new tensor. The only shape restriction is that this tensor’s input shape must match the output tensor’s output shape. The returned tensor has input-output shapes (this.output_shape, rhs.input_shape); the inner shapes have been contracted against eachother.

import trueq.math as tqm
import numpy as np

lhs = tqm.Tensor(5, (2, 3), value={(0, 1): np.random.randn(25, 36)})
rhs = tqm.Tensor((2, 3), 4, value={(1, 5): np.random.randn(36, 16)})
product = lhs.dot(rhs)

product.shape
# ((5,), (4,))
product.labels
# (0, 1, 5)
Parameters
  • rhs (Tensor) – Another tensor with a compatible shape.

  • dtype (numpy.dtype) – The data type of the output tensor. If either of the inputs are complex, and the output is real, the real part is taken without warning.

  • overwrite_rhs (bool) – Whether to mutate rhs so that its final value is the product. Otherwise, a copy of rhs is made.

Return type

Tensor

sample(n_shots=1, labels=None)

Assumes that this tensor represents a categorical probability distribution over ditstrings and samples n_shots from it, or a marginalized version of it if labels are provided.

import trueq.math as tqm

probs = tqm.Tensor(2, value={(0,): [0, 1], (1, 2): [0, 0.5, 0, 0.5]})

probs.sample(100)
# Results({"101": 52, "111": 48})

probs.sample(100, labels=(0, 2))
# Results({"11": 100})
Parameters
  • n_shots (int) – The number of samples to draw from the distribution.

  • labels (iterable) – A list of labels to sample from.

Return type

Results

to_results(labels=None, clip=True)

Constructs a new Results whose ditstring values are equal to the corresponding matrix entries of this tensor.

import trueq.math as tqm

probs = tqm.Tensor(2, value={(1,): [0, 1], (0, 2): [0, 0.5, 0, 0.5]})

probs.to_results()
# Results({"011": 0.5, "111": 0.5})

probs.sample((1, 2))
# Results({"11": 1.0})
Parameters
  • labels (iterable) – A list of labels to slice, in case a subset of all labels present in this tensor is desired. If None, all labels present in the tensor are used (in sorted order).

  • clip (bool) – Whether or not to clip the values to the interval \([0, 1]\).

Return type

Results

trueq.math.embed_unitary(u, n_sys, extra_dims)

Returns a new unitary where extra dimensions have been added to every subsystem and the given unitary has been embedded onto the lowest energy levels. The returned unitary acts trivially on the extra levels.

# inject the CNOT gate into two qutrits
add_level(tq.Gate.cnot, 2, 1)
Parameters
  • u (numpy.ndarray-like) – The unitary matrix to embed.

  • n_sys (int) – The number of subsystems the unitary acts on.

  • extra_dims – The number of extra dimensions to add to each subsystem.

Return type

int

trueq.math.find_kak_equivalent(conf, gate, param_scaling=180, tol=0.01)

Finds a gate inside of a config which is equivalent to the target gate, up to single qubit operations.

Parameterized Gates are generally periodic by construction, but depending on how they are parameterized can have order of magnitude differences in periodicity of parameters. The param_scaling is an estimate of the periodicity of the gate parameters, IE: a gate defined by :math:r”exp^{-1j * phi sigma_x}”” will have a scale on the order of pi.

Parameters
  • conf (Config) – A Config object containing multi-qubit gates.

  • gate (Gate) – The target gate to find inside the config.

  • param_scaling (float) – Approx periodicity of gate parameters, see description above.

  • tol (float) – Tolerance to decide if convergence was successful, success is defined as the L1 distance between target KAK and found KAK is less than the tol.

trueq.math.int_base(x, n)

Returns the integer solution to base ** n == x. If no exact solution exists, then None is returned (including the cases where negative numbers are given, etc.).

This implementation uses a float as intermediate step and is therefore not suitable for huge values of x. Is does, however, check that the right answer was computed for large values of x.

int_base(16, 4)
# 4
logint(16, 2)
# 4
logint(16, 3)
# None
Parameters
  • x (int) – The positive integer to compute the logarithm of.

  • n – A positive integer; the power the base should be raised to get x.

Return type

int | None

Raises

RuntimeError – If an incorrect output is calculated.

trueq.math.int_log(x, base)

Returns the integer logarithm of x in the given base. If no exact solution exists, then None is returned (including cases where a negative numbers are given, etc.).

This implementation uses a float as intermediate step and is therefore not suitable for huge values of x. Is does, however, check that the right answer was computed for large values of x.

int_log(16, 2)
# 4
int_log(16, 4)
# 2
int_log(16, 3)
# None
Parameters
  • x (int) – The positive integer to compute the logarithm of.

  • base (int) – The base of the logarithm.

Return type

int | None

Raises

RuntimeError – If an incorrect output is calculated.

trueq.math.kron(*arrs)

Returns the Kronecker product of all supplied arrays (two or more). Note that this function does not work with numpy.complex128 data type.

Parameters

arrs – A sequence of numpy.ndarrays.

Return type

numpy.ndarray

trueq.math.proc_infidelity(A, B)

Calculates the process infidelity between given unitaries A, B.

Parameters
  • A (numpy.ndarray) – Unitary of shape (d, d)

  • B (numpy.ndarray) – Unitary of shape (d, d)

Return type

float

trueq.math.random_density(dim, rank=1)

Returns a random density matrix drawn from the trace normalized Ginibre ensemble of the given rank. By default, the rank is one so that this function returns random pure states.

Parameters
  • dim (int) – The dimension of the density matrix.

  • rank (int) – A positive integer specifying the rank of the output density matrix.

Return type

numpy.ndarray

trueq.math.random_unitary(dim)

Returns a Haar random unitary matrix of size (dim, dim).

Parameters

dim (int) – The size of the unitary matrix.

Return type

numpy.ndarray

trueq.math.reshuffle(mat, perm, dim=2)

Reshuffles the subsystems of a multi-partite matrix.

For example, we can reverse the direction of a CNOT matrix applied to qubits (0,1), so that the the control qubit becomes 1 instead of 0:

# Cnot on qubit (0, 1)
mat = [[1, 0, 0, 0],
       [0, 1, 0, 0],
       [0, 0, 0, 1],
       [0, 0, 1, 0]]

reshuffle(mat, [1, 0])
# array([[1, 0, 0, 0],
         [0, 0, 0, 1],
         [0, 0, 1, 0],
         [0, 1, 0, 0]])
Parameters
  • mat (numpy.ndarray) – An array-like object to be reshuffled, this must have an (n, n) shape, where n = dim ** len(perm).

  • perm (numpy.ndarray) – A list of indices to permute, this must contain all integers from 0 to the number of systems in the mat.

  • dim (int) – The dimension of the systems, 2 for qubits, 3 for qutrits, etc.

trueq.math.to_rowstack_super(A, B, d, n)

Returns the superoperator of the operation \(X\mapsto AXB^T\) in the subsystem-wise row stacking basis.

See https://arxiv.org/abs/1111.6950 Section V for details on subsystem-wise vs. composite stacking vectorization bases (though beware that this reference uses the column stacking convention).

Parameters
  • A – The left-multiplication operator, a square matrix.

  • B (numpy.ndarray) – The right-multiplication operator, a matrix with the same shape as A.

  • d (int) – The subsystem dimension.

  • n (int) – The number of subsystems.

Return type

numpy.ndarray