PhotonicCircuit

The PhotonicCircuit is one of the key components of Lightworks. It is typically the main interaction point for users and allows for an algorithm to be encoded for implementation on a photonic quantum computing system.

Building Circuits

A circuit is initialized by calling the PhotonicCircuit object, and only requires the number of modes used in the circuit to be specified. For example, to create a circuit with 4 modes the required code would be:

circuit = lw.PhotonicCircuit(4)

Once a circuit has been created, it is then possible to view the number of modes with the n_modes attribute.

print(circuit.n_modes)
# Output: 4

There is also a number of additional attributes. U will provide the effective unitary of the circuit, this is altered by any loss elements included in the system. U_full will then provide the full unitary including any of the invisible loss modes included for loss elements.

We can then begin to add components to the circuit, the following are included as part of the PhotonicCircuit object and can be added with their corresponding methods.

PhotonicCircuit components

Component

Method

Options

Beam Splitter

bs

Modes, Reflectivity, Loss, Convention

Phase Shifter

ps

Mode, Phase shift, Loss

Mode Swaps

mode_swaps

Swaps, Element Loss

Loss Element

loss

Mode, Loss

Barriers

barrier

Modes

Below, a subsection is dedicated to each component, with more information about how each component can be used and their options.

Beam Splitter

A beam splitter is used to controllably couple two modes, with the exact nature of the coupling defined by the reflectivity of the splitter (also often referred to using an angle \(\theta\)).

To include a beam splitter in a circuit the bs method is used, at minimum it requires the first mode that the beam splitter will be placed on to be specified. By default, if the second mode isn’t specified then this will be set to the first mode + 1. So the following function calls will be equivalent:

circuit.bs(0)
circuit.bs(0, 1)

The reflectivity of the beam splitter can be adjusted by specifying the reflectivity option in the method. It should be provided as a decimal value, with the default value being 0.5. The beam splitter convention can also be adjusted if required, it defaults to ‘Rx’, with the available options being:

Beam splitter conventions

Convention

Unitary

Rx

\[\begin{split}\begin{bmatrix} \cos{\theta/2} & i\sin{\theta/2} \\ i\sin{\theta/2} & \cos{\theta/2} \\ \end{bmatrix}\end{split}\]

H

\[\begin{split}\begin{bmatrix} \cos{\theta/2} & \sin{\theta/2} \\ \sin{\theta/2} & -\cos{\theta/2} \\ \end{bmatrix}\end{split}\]

Where \(\theta = 2\cos^{-1}(\sqrt{\text{reflectivity}})\). As an example, if we wanted to add a beam splitter across modes 1 & 2, with reflectivity of 0.4 and using the ‘H’ convention, the function call would be:

circuit.bs(1, reflectivity = 0.4, convention = "H")

When using the emulator to simulate a circuit, it is also possible to include loss for components with the loss option, this will be included as a separate loss element on each of the two modes that the beam splitter acts over. If we therefore wanted to extend the function call above to include a beam splitter loss of 30% then it would look like:

circuit.bs(1, reflectivity = 0.4, convention = "H", loss = 0.3)

Warning

All losses in Lightworks should be provided as a decimal loss value, meaning loss = 0 corresponds to a ideal non-lossy component and loss = 1 will block all photons on a mode. It is also possible to specify loss in terms of dB using the included db_loss_to_decimal function of convert, for example, to include a 3 dB loss the following would be valid.

circuit.bs(1, reflectivity = 0.4, convention = "H", loss = 1 - lw.convert.db_loss_to_decimal(3))

Phase Shifter

A phase shifter acts to apply a phase to a single mode of the circuit. They are added to the circuit with the ps method, which requires the mode number it will act on and the phase shift that is to be applied. A phase shifter can also optionally introduce a loss on the mode if this is required. As an example of this, if we wanted to add a phase shift of 2 on mode 1 of the circuit, and include a 50% loss, then the method call should look like:

circuit.ps(1, 2, loss = 0.5)

Mode Swaps

The mode swaps component can be used to quickly rearrange the mode configuration of a circuit, without worrying about having to determine the exact sequence of swaps that would be required. To specify the mode swaps to be implemented a dictionary should be provided to the mode_swaps method, where the keys of the dictionary describe the initial modes and the values describe the final mode positions. If we were aiming to swaps modes 0 & 2 of a circuit, this might look like:

circuit.mode_swaps({0:2, 2:0})

It is also possible to implement more complex configurations though. For example, in a 4 mode circuit the following reconfiguration could be implemented:

circuit.mode_swaps({0:2, 2:3, 3:1, 1:0})

This would map 0 \(\rightarrow\) 2, 2 \(\rightarrow\) 3, 3 \(\rightarrow\) 1 and 1 \(\rightarrow\) 0.

Note

The provided mode swap dictionary must be complete, meaning all modes used in the swap (not necessarily all circuit modes) have an initial and final destination. This can be checked easily by comparing the keys and values of the swaps dictionary and confirming they are equivalent. An error will be raised if a provided set of swaps is incomplete.

Loss Element

A loss element is used to implement a dedicated source of loss to a mode of a photonic circuit. They are added through the loss method, which requires the mode to implement the loss on and the value of the loss as a decimal. It is important to note that adding loss elements to a circuit will introduce additional invisible modes to the circuit. This will increase runtime when simulating a circuit, so users should endeavour to minimize additional loss elements where possible.

To add a loss component on mode 1 of a circuit, with a value of 10%, the method call should look like:

circuit.loss(1, 0.1)

Barriers

The barrier method only affects how circuits are displayed by the visualization functions, and does not alter the actual functionality of the circuit. It is useful as it allows for different aspects of a circuit to be spatially separated, enabling a user to see which components correspond to a particular functionality they are trying to implement.

When adding a barrier, a list of the modes which it should be applied to is provided, alternatively if no arguments are provided to the function then it will be applied across all modes of the circuit.

# Apply to all circuit modes
circuit.barrier()

# Apply to modes 0, 2 & 4
circuit.barrier([0, 2, 4])

Visualization

Once a circuit has been created, the configuration can be viewed with the display method. To demonstrate this, we will create a new circuit which uses all of the components above.

circuit = lw.PhotonicCircuit(4)

circuit.bs(0, reflectivity = 0.4)
circuit.loss(0, 0.1)
circuit.barrier()
circuit.bs(2, loss = 0.2)
circuit.ps(0, 2)
circuit.mode_swaps({0:2,2:1,1:0})

The display method is then called on the circuit.

circuit.display()
../_images/circuit_display_demo.svg

From the circuit above, there is a few things to note. The first is that the beam splitter across modes 2 and 3 is not inline with the other beam splitter, this is directly as a result of the barrier method used in the circuit, which creates a horizontal distinction between them. It may be useful to recreate this circuit and remove the barrier so the effect of this can be seen. The other thing to note is that the loss elements, both those included with the beam splitter and the dedicated loss call are not shown. This is because we need to use the display_loss option to choose to show this. Implementing this option yields:

circuit.display(display_loss = True)
../_images/circuit_display_demo_loss.svg

For advanced users, rather than calling the display method of the PhotonicCircuit, it is instead possible to use the included Display object. Instead of displaying the circuit as soon as it is called, it will return the pyplot/drawing object, allowing this to be modified or saved to file directly.

Parameterization

Circuits in Lightworks support parameterization through a dedicated Parameter object included within the module. This allows for different values in the circuit to be modified after circuit creation, which can simplify the process of adjusting circuits. This Parameter object has a range of functionalities, which are detailed in the Parameter & ParameterDict section. To parametrize a circuit, we will start by defining a parameter, which we assign an initial value to and can optionally include a label which will be used when displaying.

parameter = lw.Parameter(0.5, label = "reflectivity")

We can then use this in a circuit, providing the parameter object in place of the normal value.

circuit = lw.PhotonicCircuit(4)

circuit.bs(0, reflectivity = parameter)
circuit.bs(2, reflectivity = parameter)
circuit.bs(1, reflectivity = parameter)

When then viewing this circuit with display, we will see that by default the parameter value is replaced by the provided label.

circuit.display()
../_images/circuit_parameter_demo1.svg

To instead view the circuit with the actual parameter values, we use show_parameter_values = True in the display method.

circuit.display(show_parameter_values = True)
../_images/circuit_parameter_demo2.svg

It is then possible to update the parameter value using the set method of the parameter. If this is updated to 0.3 it can be seen how this is then altered in the circuit visualization.

parameter.set(0.3)
circuit.display(show_parameter_values = True)
../_images/circuit_parameter_demo3.svg

This is the core functionality of Parameters in the circuit. It is also possible to store Parameters in the custom ParameterDict object, allowing for easy management and modification of parameters without having to assign each created Parameter to a distinct variable. It is recommended that this is utilized for any more than a couple of parameters. This is discussed further in the Parameter & ParameterDict section.

Note

Only certain aspects of a circuit can be parameterized, this includes phase shifts, beam splitter reflectivities and loss values. The mode number that elements are placed on cannot be parameterized. Some caution should also be used, as if the value of the parameter is altered some it becomes invalid for the quantity it is assigned to, then this will raise a compilation error when the circuit is attempted to be used.

Combining Circuits

Circuits also support addition, meaning it is possible to create sub-circuits with specific functionality and then combine them as required. There are two ways to do this.

The first is through the use of the + operator. This is simpler, but only supports the addition of equally sized circuits. An example of this is shown below, in which a circuit with a number of beam splitters is created and another with a number of phase shifters, these are then combined.

circuit_bs = lw.PhotonicCircuit(4)
circuit_bs.bs(0)
circuit_bs.bs(2)
circuit_bs.bs(1)

circuit_ps = lw.PhotonicCircuit(4)
circuit_ps.ps(0, 1)
circuit_ps.ps(2, 2)
circuit_ps.ps(1, 3)

new_circuit = circuit_bs + circuit_ps
new_circuit.display()
../_images/circuit_addition_demo1.svg

The other way to combine circuits is through the add method, which allows for a smaller circuit to be added to a larger circuit. With the add method, it is also possible to choose which mode of the larger circuit the smaller circuit starts on. For example, in the following we create a 5 and 3 mode circuit, comprised of beam splitters and phase shifters respectively, and choose for the circuit being added to start on mode 1 of the larger circuit.

circuit_bs = lw.PhotonicCircuit(5)
circuit_bs.bs(0)
circuit_bs.bs(2)
circuit_bs.bs(1)
circuit_bs.bs(3)

circuit_ps = lw.PhotonicCircuit(3)
circuit_ps.ps(0, 1)
circuit_ps.ps(1, 2)
circuit_ps.ps(2, 3)

circuit_bs.add(circuit_ps, 1)
circuit_bs.display()
../_images/circuit_addition_demo2.svg

It can also be seen how unlike when using + the add method does not return a new circuit, and instead adds it to the existing created circuit.

Note

The starting mode of the smaller circuit + the number of modes in the smaller circuit must be less than or equal to the number of modes in the larger circuit. For example, when adding a 4 mode circuit to a 6 mode circuit, then the 4 mode circuit could start on mode 0, 1 or 2.

When using add, it is also possible to choose to group all elements being added to the larger circuit into a single component. This has no functional effect on the circuit but can be useful when viewing the created circuit. A name can also be provided which is shown in the display. In this case we will choose to label the circuit as ‘phases’.

circuit_bs = lw.PhotonicCircuit(5)
circuit_bs.bs(0)
circuit_bs.bs(2)
circuit_bs.bs(1)
circuit_bs.bs(3)

circuit_ps = lw.PhotonicCircuit(3)
circuit_ps.ps(0, 1)
circuit_ps.ps(1, 2)
circuit_ps.ps(2, 3)

circuit_bs.add(circuit_ps, 1, group = True, name = "phases")
circuit_bs.display()
../_images/circuit_addition_demo3.svg

Heralding Integration

In photonic quantum computing, ancillary photons/modes are often used to realize particular entangled states, particularly in qubit paradigms. The Lightworks PhotonicCircuit supports the addition of these ancillary photons with the herald method, enabling heralding to be completed on a circuit without having to factor these modes being factored into the inputs and outputs of a circuit. This is supported for all simulation objects in the emulator.

As an example of this, in the following a herald is added on mode 2 of the circuit, requiring that 1 photon is input and output on this mode of the circuit. When the input and output mode are the same, only the input needs to be specified, but when they differ these both need to be specified. For example, herald(1, 2) and herald(1, 2, 2) are equivalent.

circuit = lw.PhotonicCircuit(4)
circuit.bs(0)
circuit.bs(1)
circuit.bs(2)

circuit.herald(1, 2)

circuit.display()
../_images/circuit_herald_demo.svg

It is also possible to include heralds as part of smaller circuits and then add them to a larger circuit. This enables small functional building blocks to be constructed and combined to create a larger transformation. When a sub-circuit has heralds added to it, this mode is not connected to the existing mode of the larger circuit, this means that for the example above, the sub-circuit would effectively have 3 modes. The effect of this can be seen below, in which the circuit above is added to a larger circuit.

main_circuit = lw.PhotonicCircuit(4)
main_circuit.add(circuit, 1)

main_circuit.display()
../_images/circuit_herald_demo2.svg

If a beam splitter is then added across modes 2 & 3, this will then effectively ignore the mode with the heralded photon.

main_circuit.bs(2, 3)

main_circuit.display()
../_images/circuit_herald_demo3.svg

Note

Despite the display showing 4 numbered modes, the circuit will now have 5 modes total. This will lead to an increase in job execution time, particularly if lots of heralded photons are used.

Other Functionality

Also included within the circuit, is a number of additional methods which enable a circuit to be modified as required. In the following, the functionality of each is briefly discussed:

lightworks.PhotonicCircuit.get_all_params()

This method will collect and return a list of all parameters used as part of a circuit.

circuit.get_all_params()

lightworks.PhotonicCircuit.copy()

Used to create an identical copy of a particular circuit. The freeze_parameters option can be used to decided whether any variables should remain assigned to parameters, or if they should just be assigned to the parameter value.

circuit.copy()
circuit.copy(freeze_parameters = True)

Warning

If you decide not to freeze the parameters of a circuit before copying then both the original and copy of the circuit will be affected by any parameter changes.

lightworks.PhotonicCircuit.unpack_groups()

Can be used to unpack any groups of components that have been added to a circuit into individual elements.

circuit.unpack_groups()

lightworks.PhotonicCircuit.compress_mode_swaps()

When this method is called it will look at any adjacent mode swaps in a circuit and compress them into a single element. This can be useful for reducing the footprint of a circuit.

circuit.compress_mode_swaps()

lightworks.PhotonicCircuit.remove_non_adjacent_bs()

This method can be used to convert any beam splitters that act across non-adjacent modes into a beam splitter on adjacent modes and a set of mode swaps across the circuit. This is useful as in photonic integrated circuit realizations of an interferometer the direct interaction between non-adjacent modes is not possible.

circuit.remove_non_adjacent_bs()