Circuits and Gates#

Overview#

In TensorCircuit, a quantum circuit on \(n\) qubits – which supports both noiseless and noisy simulations via Monte Carlo trajectory methods – is created by the tc.Circuit(n) API. Here we show how to create basic circuits, apply gates to them, and compute various outputs.

Setup#

[1]:
import inspect
import numpy as np
import tensorcircuit as tc

K = tc.set_backend("tensorflow")

In TensorCircuit the default runtime datatype is complex64, but if higher precision is required this can be set as follows

[2]:
tc.set_dtype("complex128")
[2]:
('complex128', 'float64')

Basic Circuits and Outputs#

Quantum circuits can be constructed as follows.

[3]:
c = tc.Circuit(2)
c.h(0)
c.cnot(0, 1)
c.rx(1, theta=0.2)

Output: state

From this, various outputs can be computed.

The full wavefunction can be obtained via

[4]:
c.state()
[4]:
<tf.Tensor: shape=(4,), dtype=complex128, numpy=
array([0.70357418+0.j        , 0.        -0.07059289j,
       0.        -0.07059289j, 0.70357418+0.j        ])>

The full wavefunction can also be used to generate the reduced density matrix of a subset of the qubits

[5]:
# reduced density matrix for qubit 1
s = c.state()
tc.quantum.reduced_density_matrix(s, cut=[0])  # cut: list of qubit indices to trace out
[5]:
<tf.Tensor: shape=(2, 2), dtype=complex128, numpy=
array([[0.5+0.j, 0. +0.j],
       [0. +0.j, 0.5+0.j]])>

Amplitudes of individual basis vectors are computed by passing the corresponding bit-string value to the amplitude function. For example, the amplitude of the \(\vert{10}\rangle\) basis vector is computed by

[6]:
c.amplitude("10")
[6]:
<tf.Tensor: shape=(), dtype=complex128, numpy=-0.0705928857402556j>

The unitary matrix corresponding to the entire quantum circuit can also be output.

[7]:
c.matrix()
[7]:
<tf.Tensor: shape=(4, 4), dtype=complex128, numpy=
array([[ 0.70357418+0.j        ,  0.        -0.07059289j,
         0.70357418+0.j        ,  0.        -0.07059289j],
       [ 0.        -0.07059289j,  0.70357418+0.j        ,
         0.        -0.07059289j,  0.70357418+0.j        ],
       [ 0.        -0.07059289j,  0.70357418+0.j        ,
         0.        +0.07059289j, -0.70357418+0.j        ],
       [ 0.70357418+0.j        ,  0.        -0.07059289j,
        -0.70357418+0.j        ,  0.        +0.07059289j]])>

Output: measurement

Random samples corresponding to \(Z\)-measurements on all qubits can be generated using the following API, which will output a \((\text{bitstring}, \text{probability})\) tuple, comprising a binary string corresponding to the measurement outcomes of a Z measurement on all the qubits and the associated probability of obtaining that outcome. Z measurements on a subset of qubits can be performed with the measure command

[8]:
c.sample()
[8]:
(<tf.Tensor: shape=(2,), dtype=float64, numpy=array([1., 1.])>,
 <tf.Tensor: shape=(), dtype=float64, numpy=0.4950166615971341>)
[9]:
c.measure(0, with_prob=True)
[9]:
(<tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.])>,
 <tf.Tensor: shape=(), dtype=float64, numpy=0.5000000171142709>)
[10]:
c.measure(0, 1, with_prob=True)
[10]:
(<tf.Tensor: shape=(2,), dtype=float64, numpy=array([1., 1.])>,
 <tf.Tensor: shape=(), dtype=float64, numpy=0.4950166615971341>)

Output: expectation

Expectation values such as \(\langle X_0 \rangle\), \(\langle X_1 + Z_1\rangle\) or \(\langle Z_0 Z_1\rangle\) can be computed via the \({\sf expectation}\) method of a circuit object

[11]:
print(c.expectation([tc.gates.x(), [0]]))  # <X0>
print(c.expectation([tc.gates.x() + tc.gates.z(), [1]]))  # <X1 + Z1>
print(c.expectation([tc.gates.z(), [0]], [tc.gates.z(), [1]]))  # <Z0 Z1>
tf.Tensor(0j, shape=(), dtype=complex128)
tf.Tensor(0j, shape=(), dtype=complex128)
tf.Tensor((0.9800665437029109+0j), shape=(), dtype=complex128)
[12]:
# user-defined operator

c.expectation([np.array([[3, 2], [2, -3]]), [0]])
[12]:
<tf.Tensor: shape=(), dtype=complex128, numpy=0j>

While expectations of products of Pauli operators, e.g. \(\langle Z_0 X_1\rangle\) can be computed using c.expectation as above, TensorCircuit provides another way of computing such expressions which may be more convenient for longer Pauli strings, and longer Pauli strings can similarly be computed by providing lists of indices corresponding to the qubits that the \(X,Y,Z\) operators act on.

[13]:
c.expectation_ps(x=[1], z=[0])
[13]:
<tf.Tensor: shape=(), dtype=complex128, numpy=0j>

Built-in Gates#

TensorCircuit provides support for a wide variety of commonly encountered quantum gates. The full list is as below.

[14]:
for g in tc.Circuit.sgates:
    gf = getattr(tc.gates, g)
    print(g)
    print(tc.gates.matrix_for_gate(gf()))
i
[[1.+0.j 0.+0.j]
 [0.+0.j 1.+0.j]]
x
[[0.+0.j 1.+0.j]
 [1.+0.j 0.+0.j]]
y
[[0.+0.j 0.-1.j]
 [0.+1.j 0.+0.j]]
z
[[ 1.+0.j  0.+0.j]
 [ 0.+0.j -1.+0.j]]
h
[[ 0.70710678+0.j  0.70710678+0.j]
 [ 0.70710678+0.j -0.70710678+0.j]]
t
[[1.        +0.j         0.        +0.j        ]
 [0.        +0.j         0.70710678+0.70710678j]]
s
[[1.+0.j 0.+0.j]
 [0.+0.j 0.+1.j]]
td
[[1.        +0.j         0.        +0.j        ]
 [0.        +0.j         0.70710677-0.70710677j]]
sd
[[1.+0.j 0.+0.j]
 [0.+0.j 0.-1.j]]
wroot
[[ 0.70710678+0.j  -0.5       -0.5j]
 [ 0.5       -0.5j  0.70710678+0.j ]]
cnot
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j]]
cz
[[ 1.+0.j  0.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  1.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  1.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  0.+0.j -1.+0.j]]
swap
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
cy
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.-1.j]
 [0.+0.j 0.+0.j 0.+1.j 0.+0.j]]
iswap
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+1.j 0.+0.j]
 [0.+0.j 0.+1.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
ox
[[0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
oy
[[0.+0.j 0.-1.j 0.+0.j 0.+0.j]
 [0.+1.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
oz
[[ 1.+0.j  0.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j -1.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  1.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  0.+0.j  1.+0.j]]
toffoli
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j]]
fredkin
[[1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
[15]:
for g in tc.Circuit.vgates:
    print(g, inspect.signature(getattr(tc.gates, g).f))
r (theta: float = 0, alpha: float = 0, phi: float = 0) -> tensorcircuit.gates.Gate
cr (theta: float = 0, alpha: float = 0, phi: float = 0) -> tensorcircuit.gates.Gate
rx (theta: float = 0) -> tensorcircuit.gates.Gate
ry (theta: float = 0) -> tensorcircuit.gates.Gate
rz (theta: float = 0) -> tensorcircuit.gates.Gate
crx (*args: Any, **kws: Any) -> Any
cry (*args: Any, **kws: Any) -> Any
crz (*args: Any, **kws: Any) -> Any
orx (*args: Any, **kws: Any) -> Any
ory (*args: Any, **kws: Any) -> Any
orz (*args: Any, **kws: Any) -> Any
any (unitary: Any, name: str = 'any') -> tensorcircuit.gates.Gate
exp (unitary: Any, theta: float, name: str = 'none') -> tensorcircuit.gates.Gate
exp1 (unitary: Any, theta: float, name: str = 'none') -> tensorcircuit.gates.Gate

Also, we have built-in matrices as

[16]:
for name in dir(tc.gates):
    if name.endswith("_matrix"):
        print(name, ":\n", getattr(tc.gates, name))
_cnot_matrix :
 [[1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 0. 1.]
 [0. 0. 1. 0.]]
_cy_matrix :
 [[ 1.+0.j  0.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  1.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  0.+0.j -0.-1.j]
 [ 0.+0.j  0.+0.j  0.+1.j  0.+0.j]]
_cz_matrix :
 [[ 1.  0.  0.  0.]
 [ 0.  1.  0.  0.]
 [ 0.  0.  1.  0.]
 [ 0.  0.  0. -1.]]
_fredkin_matrix :
 [[1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 1.]]
_h_matrix :
 [[ 0.70710678  0.70710678]
 [ 0.70710678 -0.70710678]]
_i_matrix :
 [[1. 0.]
 [0. 1.]]
_ii_matrix :
 [[1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]]
_s_matrix :
 [[1.+0.j 0.+0.j]
 [0.+0.j 0.+1.j]]
_swap_matrix :
 [[1. 0. 0. 0.]
 [0. 0. 1. 0.]
 [0. 1. 0. 0.]
 [0. 0. 0. 1.]]
_t_matrix :
 [[1.        +0.j         0.        +0.j        ]
 [0.        +0.j         0.70710678+0.70710678j]]
_toffoli_matrix :
 [[1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 0. 1. 0.]]
_wroot_matrix :
 [[ 0.70710678+0.j  -0.5       -0.5j]
 [ 0.5       -0.5j  0.70710678+0.j ]]
_x_matrix :
 [[0. 1.]
 [1. 0.]]
_xx_matrix :
 [[0. 0. 0. 1.]
 [0. 0. 1. 0.]
 [0. 1. 0. 0.]
 [1. 0. 0. 0.]]
_y_matrix :
 [[ 0.+0.j -0.-1.j]
 [ 0.+1.j  0.+0.j]]
_yy_matrix :
 [[ 0.+0.j  0.-0.j  0.-0.j -1.+0.j]
 [ 0.+0.j  0.+0.j  1.-0.j  0.-0.j]
 [ 0.+0.j  1.-0.j  0.+0.j  0.-0.j]
 [-1.+0.j  0.+0.j  0.+0.j  0.+0.j]]
_z_matrix :
 [[ 1.  0.]
 [ 0. -1.]]
_zz_matrix :
 [[ 1.  0.  0.  0.]
 [ 0. -1.  0. -0.]
 [ 0.  0. -1. -0.]
 [ 0. -0. -0.  1.]]

Arbitrary unitaries. User-defined unitary gates may be implemented by specifying their matrix elements as an array. As an example, the unitary \(S = \begin{pmatrix} 1 & 0 \\ 0 & i\end{pmatrix}\) – which can also directly be added by calling c.s() can be implemented as

[17]:
c.unitary(0, unitary=np.array([[1, 0], [0, 1j]]), name="S")

# the optional name argument specifies how this gate is displayed when the circuit is output to \LaTeX

Exponential gates. Gates of the form \(e^{i\theta G}\) where matrix \(G\) satisfies \(G^2 = I\) admit a fast implementation via the exp1 command, e.g., the two-qubit gate \(e^{i\theta Z\otimes Z}\) acting on qubits \(0\) and \(1\)

[18]:
c.exp1(0, 1, theta=0.2, unitary=tc.gates._zz_matrix)

General exponential gates, where \(G^2\neq 1\) can be implemented via the exp command:

[19]:
c.exp(0, theta=0.2, unitary=np.array([[2, 0], [0, 1]]))

Non-unitary gates. TensorCircuit also supports the application of non-unitary gates, either by providing a non-unitary matrix as the argument to c.unitary or by supplying a complex angle \(\theta\) to an exponential gate.

[20]:
c.unitary(0, unitary=np.array([[1, 2], [2, 3]]), name="non_unitary")
c.exp1(0, theta=0.2 + 1j, unitary=tc.gates._x_matrix)

Note that the non-unitary gates will lead to an output state that is no longer normalized since normalization is often unnecessary and takes extra time which can be avoided.

Specifying the Input State and Composing Circuits#

By default, quantum circuits are applied to the initial all zero product state. Arbitrary initial states can be set by passing an array containing the input state amplitudes to the optional inputs argument of tc.Circuit. For example, the maximally entangled state \(\frac{\vert{00}\rangle+\vert{11}\rangle}{\sqrt{2}}\) can be input as follows.

[21]:
c1 = tc.Circuit(2, inputs=np.array([1, 0, 0, 1] / np.sqrt(2)))

Circuits that act on the same number of qubits can be composed together via the c.append() or c.prepend() commands. With c1 defined as above, we can create a new circuit c2 and then compose them together:

[22]:
c2 = tc.Circuit(2)
c2.cnot(1, 0)

c3 = c1.append(c2)
c3.state()
[22]:
<tf.Tensor: shape=(4,), dtype=complex128, numpy=array([0.70710678+0.j, 0.70710678+0.j, 0.        +0.j, 0.        +0.j])>

Circuit Transformation and Visualization#

tc.Circuit objects can be converted to and from Qiskit QuantumCircuit objects.

[23]:
c = tc.Circuit(2)
c.H(0)
c.cnot(1, 0)
cq = c.to_qiskit()
[24]:
c1 = tc.Circuit.from_qiskit(cq)
[25]:
# print the quantum circuit intermediate representation

c1.to_qir()
[25]:
[{'gatef': h,
  'gate': Gate(
      name: 'h',
      tensor:
          <tf.Tensor: shape=(2, 2), dtype=complex128, numpy=
          array([[ 0.70710677+0.j,  0.70710677+0.j],
                 [ 0.70710677+0.j, -0.70710677+0.j]])>,
      edges: [
          Edge('cnot'[3] -> 'h'[0] ),
          Edge('h'[1] -> 'qb-1'[0] )
      ]),
  'index': (0,),
  'name': 'h',
  'split': None,
  'mpo': False},
 {'gatef': cnot,
  'gate': Gate(
      name: 'cnot',
      tensor:
          <tf.Tensor: shape=(2, 2, 2, 2), dtype=complex128, numpy=
          array([[[[1.+0.j, 0.+0.j],
                   [0.+0.j, 0.+0.j]],

                  [[0.+0.j, 1.+0.j],
                   [0.+0.j, 0.+0.j]]],


                 [[[0.+0.j, 0.+0.j],
                   [0.+0.j, 1.+0.j]],

                  [[0.+0.j, 0.+0.j],
                   [1.+0.j, 0.+0.j]]]])>,
      edges: [
          Edge(Dangling Edge)[0],
          Edge(Dangling Edge)[1],
          Edge('cnot'[2] -> 'qb-2'[0] ),
          Edge('cnot'[3] -> 'h'[0] )
      ]),
  'index': (1, 0),
  'name': 'cnot',
  'split': None,
  'mpo': False}]

There are two ways to visualize quantum circuits generated in TensorCircuit. The first is to use c.tex() to get :nbsphinx-math:`Latex `quantikz commands.

[26]:
c.tex()
[26]:
'\\begin{quantikz}\n\\lstick{$\\ket{0}$}&\\gate{h} &\\targ{} &\\qw \\\\\n\\lstick{$\\ket{0}$}&\\qw &\\ctrl{-1} &\\qw \n\\end{quantikz}'

The second method uses the draw function from qiskit.

[27]:
c.draw(output="mpl")
[27]:
../_images/whitepaper_3-circuits-gates_53_0.png