Circuits
Here we summarise the catalogue circuit components and their underlying physical models.
Name |
Method |
Circuit Diagram |
Modes |
---|---|---|---|
Beam splitter |
|
2 |
|
Phase shifter |
|
1 |
|
Permutation |
|
2+ |
|
Switch |
|
2 |
|
Loss |
|
1+ |
|
Gate |
|
1+ |
|
Haar random unitary |
|
1+ |
|
Custom scattering matrix |
|
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
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]:
Phase shifter
The phase shifter implements a unitary transformation on one mode that is defined by the matrix
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]:
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
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]:
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]:
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]:
[115]:
c.evaluate(t=1)
[115]:
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()

Loss
The loss component implements a non-unitary transformation on one mode that is defined by the matrix
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]:
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]:
Gate
The gate component implements a piece-wise time-independent non-unitary transformation on one mode that is defined by the matrix
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]:
[122]:
c.evaluate(t=0.5)
[122]:
[123]:
c.evaluate(t=2)
[123]:
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()

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]:
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]:
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]:
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]:
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]:
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]: