Circuits

Here we summarise the catalogue circuit components and their underlying physical models.

Name

Method

Circuit Diagram

Modes

Beam splitter

bs

image0

2

Phase shifter

ps

image1

1

Permutation

perm

image2

2+

Switch

switch

image3

2

Loss

loss

image4

1+

Gate

gate

image5

1+

Haar random unitary

haar_random

image6

1+

Custom scattering matrix

custom

image7

1+

The following sections give more detail on each circuit component.

[105]:
from zpgenerator import *

Beam splitter

The beam splitter implements a unitary transformation on two modes that is defined by the matrix

\[\begin{split}\hat{U}_\text{bs} = \begin{pmatrix} \cos(\theta) & i\sin(\theta)\\ i\sin(\theta) & \cos(\theta) \end{pmatrix}.\end{split}\]

Other variants of the beam splitter, such as the Hadamard unitary, can be implemented by placing the appropriate phase shifters on the input and output ports. Alternatively, the circuit can be constructed using Perceval’s circuit components and then converted to ZPGenerator.

The parameters of this component are:

Examples

The beam splitter defaults to \(\theta=\pi/4\), indicating a 50:50 splitting ratio.

[106]:
c = Circuit.bs()
c.default_parameters
[106]:
{'angle': 0.7853981633974483}

All circuit elements have a time argument, even if they are time independent. They can be evaluated to a matrix in QuTiP Qobj format.

[107]:
c.evaluate(t=0)
[107]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = False $ \\ \left(\begin{matrix}0.707 & 0.707j\\0.707j & 0.707\\\end{matrix}\right)$

Phase shifter

The phase shifter implements a unitary transformation on one mode that is defined by the matrix

\[\hat{U}_\text{ps} = \begin{pmatrix} e^{i\phi} \end{pmatrix}.\]

This impact is only seen relative to the phase of light in other modes. The parameters of this component are:

Examples

By default, the phase is shifted by \(\pi\), which is equivalent to multiplying the amplitude of the light by -1.

[108]:
c = Circuit.ps()
c.default_parameters
[108]:
{'phase': 3.141592653589793}
[109]:
c.evaluate(t=0)
[109]:
Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra $ \\ \left(\begin{matrix}-1.0\\\end{matrix}\right)$

Permutation

The permutation unitary is used to swap modes without having them interact. This is used to organise the locations of the modes so that they can interact properly with other components. The permutation is provided as a list of unique non-negative integers and the order determines the new ordering of the modes. For two modes, the permutation is simply

\[\begin{split}\hat{U}_\text{perm} = \begin{pmatrix} 0 & 1\\ 1 & 0 \end{pmatrix}.\end{split}\]

Examples

This component has no parameters.

[110]:
c = Circuit.perm()
c.default_parameters
[110]:
{}

By default, it swaps two modes.

[111]:
c.evaluate(0)
[111]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True $ \\ \left(\begin{matrix}0.0 & 1.0\\1.0 & 0.0\\\end{matrix}\right)$

Providing a permutation as a list of integers, we can configure the reordering.

[112]:
c = Circuit.perm([1, 0, 4, 2, 3])
c.evaluate(0)
[112]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = False $ \\ \left(\begin{matrix}0.0 & 1.0 & 0.0 & 0.0 & 0.0\\1.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 1.0\\0.0 & 0.0 & 1.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 1.0 & 0.0\\\end{matrix}\right)$

Switch

The switch is a time-dynamic component used to actively change the spatial mode of light during its evolution. It is a Mach-Zehnder interferometer composed of beam splitters and a phase shifter. The only difference is that the phase of the phase shifter is controlled in time. The parameters of this component are all user-defined.

Examples

The switch can be instantiated with a function that determines the state of the switch over time. A value of 0 indicates the switch is fully off and all the light is transmitted in the same mode it enters, a value of 1 indicates the switch is fully on and the modes are swapped (with an appropriate phase factor). In addition to the function, it is necessary to supply an interval over which the function is applied to the switch and any default parameters needed to evaluate the function.

[113]:
c = Circuit.switch(function=lambda t, args: (t - args['start'])/args['end'],
                   interval=['start', 'end'],
                   parameters={'start': 0, 'end': 1})
c.default_parameters
[113]:
{'start': 0, 'end': 1}

By evaluating the circuit we can see that the unitary changes over time.

[114]:
c.evaluate(t=0)
[114]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True $ \\ \left(\begin{matrix}1.0 & 0.0\\0.0 & 1.0\\\end{matrix}\right)$
[115]:
c.evaluate(t=1)
[115]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True $ \\ \left(\begin{matrix}0.0 & -1.0\\-1.0 & 0.0\\\end{matrix}\right)$

Do demonstrate its impact, we can split a photon into two spatial modes and compare the intensity of the light as a function of time before and after the switch.

[116]:
source = Source.fock(1)
p = Processor() // source // c
source.plot_lifetime(start=0, end=5, label='source')
p.plot_lifetime(port=0, start=0, end=5, label='mode 0')
p.plot_lifetime(port=1, start=0, end=5, label='mode 1').show()
../_images/notebooks_circuits_catalogue_36_0.png

Loss

The loss component implements a non-unitary transformation on one mode that is defined by the matrix

\[\hat{S}_\text{loss} = \begin{pmatrix} \sqrt{\eta} \end{pmatrix}.\]

This is equivalent to a beam splitter (or linear) loss model where the amplitude of light dampened linearly. That is, a value of \(\eta=1/2\) will halve the average photon number of the light.

Examples

By default, the loss component does nothing.

[117]:
c = Circuit.loss()
c.default_parameters
[117]:
{'efficiency': 1}

By evaluating it, we can verify the underlying model.

[118]:
c.evaluate(t=0, parameters={'efficiency': 0.5})
[118]:
Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra $ \\ \left(\begin{matrix}0.707\\\end{matrix}\right)$

Using the ‘modes’ keyword, we can apply the same loss value to multiple modes simultaneously. We may also specify the default efficiency value when instantiating the object. This can be done through the ‘efficiency’ keyword or by passing a dictionary using the ‘parameters’ keyword.

[119]:
c = Circuit.loss(efficiency=0.5, modes=5)
c.evaluate(t=0)
[119]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True $ \\ \left(\begin{matrix}0.707 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.707 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.707 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.707 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.707\\\end{matrix}\right)$

Gate

The gate component implements a piece-wise time-independent non-unitary transformation on one mode that is defined by the matrix

\[\hat{S}_\text{gate}(t) = \begin{pmatrix} \sqrt{\eta(t)} \end{pmatrix}.\]

where \(\eta(t)=1\) for \(t\) within a specified interval of time and \(\eta(t)=0\) otherwise. This is equivalent to a switch where mode 1 is ignored and the function is a square pulse Like the switch, all parameters are user-defined.

Examples

When instantiating, we can specify parameters directly as strings in the interval list. Similar to the loss component, we can also specify the number of modes that are impacted by the gate.

[120]:
c = Circuit.gate(interval=['start', 'end'],
                 parameters={'start': 0, 'end': 1},
                 modes=3)
c.default_parameters
[120]:
{'start': 0, 'end': 1}

The value of the scattering matrix will depend on time.

[121]:
c.evaluate(t=-1)
[121]:
Quantum object: dims = [[3], [3]], shape = (3, 3), type = oper, isherm = True $ \\ \left(\begin{matrix}0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0\\\end{matrix}\right)$
[122]:
c.evaluate(t=0.5)
[122]:
Quantum object: dims = [[3], [3]], shape = (3, 3), type = oper, isherm = True $ \\ \left(\begin{matrix}1.0 & 0.0 & 0.0\\0.0 & 1.0 & 0.0\\0.0 & 0.0 & 1.0\\\end{matrix}\right)$
[123]:
c.evaluate(t=2)
[123]:
Quantum object: dims = [[3], [3]], shape = (3, 3), type = oper, isherm = True $ \\ \left(\begin{matrix}0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0\\\end{matrix}\right)$

We can use this to truncate emission from a source, either to model a realistic protocol or to analyse its behaviour.

[124]:
source = Source.fock(1)
p = Processor() // source // c
params = {'start': 0.5, 'end': 1.5}
source.plot_lifetime(start=0, end=5, label='source', parameters=params)
p.plot_lifetime(port=0, start=0, end=5, label='gated source', parameters=params).show()
../_images/notebooks_circuits_catalogue_56_0.png

Haar random

The Haar random component is simply a Haar random unitary transformation applied to a specified number of modes. The unitary matrix is generated randomly using the QuTiP function ‘rand_unitary_haar’. The Haar random component has no parameters.

Examples

We can specify the number of modes when creating the object.

[125]:
c = Circuit.haar_random(modes=5)
c.default_parameters
[125]:
{}

The circuit is not time dependent.

[126]:
c.is_time_dependent()
[126]:
False
[127]:
c.evaluate(t=0)
[127]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = False $ \\ \left(\begin{matrix}(0.475-0.204j) & (0.387-0.301j) & (0.371-0.197j) & (-0.476+0.222j) & (-0.197+0.037j)\\(0.088-0.455j) & (0.222+0.308j) & (-0.120+0.442j) & (0.013-0.012j) & (0.060+0.654j)\\(-0.168-0.544j) & (-0.103-0.296j) & (0.031+0.261j) & (-0.039-0.524j) & (-0.353-0.328j)\\(-0.403+0.084j) & (0.603-0.018j) & (-0.371+0.159j) & (0.005+0.364j) & (-0.350-0.220j)\\(0.025+0.153j) & (0.029-0.391j) & (-0.617-0.033j) & (-0.486-0.282j) & (0.316+0.162j)\\\end{matrix}\right)$

Custom

To make a custom circuit component, one can use the ‘custom’ method and specify the desired scattering matrix directly. Note that it doesn’t need to be a unitary matrix, but to obtain physically-meaningful results, it should at least be a scattering matrix with singular values between 0 and 1. For example, we can create a scattering matrix causing a loss of \(0.5^2=0.25\) on the second mode of a two-mode circuit.

[128]:
c = Circuit.custom(matrix=[[1.0, 0], [0, 0.5]])
c.evaluate(t=0)
[128]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True $ \\ \left(\begin{matrix}1.0 & 0.0\\0.0 & 0.500\\\end{matrix}\right)$

To ensure physically meaningful results, it is always recommended to build scattering matrices by combining diagonal loss matrices, as shown above, with unitary matrices.

[129]:
lossy_bs = Circuit.bs() // c
lossy_bs.evaluate(t=0)
[129]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = False $ \\ \left(\begin{matrix}0.707 & 0.354j\\0.707j & 0.354\\\end{matrix}\right)$

Parameters can be introduced by providing a function that returns a QuTiP Qobj, along with a corresponding dictionary of default parameters.

[130]:
from numpy import sqrt
c = Circuit.custom(matrix=lambda args: Qobj([[args['a'], 0], [0, args['b']]]) * Qobj([[1, 1], [1, -1]]) / sqrt(2),
                   parameters={'a': 1, 'b': 1})
c.evaluate(t=0, parameters={'a': 0.5, 'b': 0.8})
[130]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = False $ \\ \left(\begin{matrix}0.354 & 0.354\\0.566 & -0.566\\\end{matrix}\right)$

Conversion from Perceval

Perceval provides many tools for building photonic circuits and also has many catalogue circuits built to perform linear-optical gates. These circuits can be converted to ZPGenerator simply through their underlying unitary transformation. This conversion cannot account for any heralding or post-processing steps, nor non-unitary components in Perceval.

[131]:
from perceval import BS, catalog

Examples

We can easily create a Hadamard variation of the beam splitter using the class method of BS in Perceval.

[132]:
c = Circuit.from_perceval(BS.H())
c.evaluate(0)
[132]:
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True $ \\ \left(\begin{matrix}0.707 & 0.707\\0.707 & -0.707\\\end{matrix}\right)$

We can also import complicated circuits such as a 12-mode post-processed Toffoli (CCZ) circuit.

[133]:
c = Circuit.from_perceval(catalog['postprocessed ccz'].build_circuit())
c.evaluate(0)
[133]:
Quantum object: dims = [[12], [12]], shape = (12, 12), type = oper, isherm = False $ \\ \left(\begin{matrix}0.510 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.860 & 0.0 & 0.0\\0.0 & 0.510 & 0.0 & (0.321+0.556j) & 0.0 & \cdots & (-0.165-0.286j) & (-0.165+0.286j) & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.510 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.860 & 0.0\\0.0 & 0.0 & 0.0 & 0.510 & 0.0 & \cdots & 0.330 & (-0.165-0.286j) & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.510 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.860\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & (-0.165+0.286j) & 0.0 & 0.330 & 0.0 & \cdots & -0.510 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & (-0.165-0.286j) & 0.0 & (-0.165+0.286j) & 0.0 & \cdots & (-0.321+0.556j) & -0.510 & 0.0 & 0.0 & 0.0\\0.860 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & -0.510 & 0.0 & 0.0\\0.0 & 0.0 & 0.860 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & -0.510 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.860 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & -0.510\\\end{matrix}\right)$