Instructions

Instructions are the operations placed inside a Program. They describe how selected modes are prepared, transformed, measured, or affected by noise. In most examples, an instruction is attached to one or more modes with the pq.Q(...) | instruction syntax.

Preparations

Define the initial state or add state-vector components to the program.

Preparations
Gates

Apply transformations such as displacements, squeezings, beamsplitters, and interferometers.

Gates
Measurements

Extract samples or measurement results from selected modes.

Measurements
Channels

Model non-unitary effects such as loss and other noisy processes.

Channels

How instructions are used

A program is built by applying instructions to modes. The instruction itself describes the operation, while pq.Q(...) selects the modes on which the operation acts.

import numpy as np
import piquasso as pq

with pq.Program() as program:
   # Preparation
   pq.Q() | pq.Vacuum()

   # Gates
   pq.Q(0) | pq.Displacement(r=0.2)
   pq.Q(0, 1) | pq.Beamsplitter(theta=np.pi / 4)

   # Measurement
   pq.Q(all) | pq.ParticleNumberMeasurement()

simulator = pq.GaussianSimulator(d=2)
result = simulator.execute(program, shots=10)

print(result.samples)

Instruction categories

Preparations usually come first

A preparation instruction initializes the simulated state before gates, channels, or measurements are applied.

Gates transform the state

Gate instructions describe reversible transformations of the selected modes. Many common optical operations are represented as gates.

Channels model noise

Channel instructions describe non-unitary effects, such as losses. Their availability can depend on the chosen simulator.

Measurements usually come last

Measurement instructions produce classical information, such as samples. When shots are requested, the result contains measurement samples.

Simulator compatibility

Instructions are declarative: they describe the program to be executed. The chosen simulator determines which instructions can be evaluated efficiently and which state representation is used. For example, Gaussian circuits are naturally handled by Gaussian simulators, while many non-Gaussian operations require a Fock-space simulator. Boson Sampling-style programs are handled by the Sampling simulator.

Dynamic and adaptive programs

Piquasso programs do not have to be simple straight-line circuits. In supported simulators, measurements can appear before the end of the program, later instructions can be conditioned on previous measurement outcomes, and some instruction parameters can be resolved at runtime.

Mid-circuit measurements

Measure or postselect modes before the program ends, then continue the simulation on the remaining modes.

Conditional execution

Attach a condition to an instruction with Instruction.when(...) so it is applied only for selected measurement histories.

Runtime parameters

Use previous measurement outcomes to determine instruction parameters during execution.

Mid-circuit measurements

Mid-circuit measurements are useful when a measurement result should affect the state before later instructions are applied. A common example is heralding: one mode is measured or postselected, and the unmeasured modes continue in the post-measurement state.

import numpy as np
import piquasso as pq

with pq.Program() as program:
   pq.Q() | pq.Vacuum()

   # Entangle modes 0 and 1.
   pq.Q(0, 1) | pq.Squeezing2(r=np.log(1 + np.sqrt(2)))

   # Herald on detecting exactly one photon in mode 1.
   pq.Q(1) | pq.PostSelectPhotons((1,))

   # Continue the program on the heralded state.
   pq.Q(0, 2) | pq.Beamsplitter5050()

result = pq.PureFockSimulator(d=3).execute(program)

Conditional instruction execution

The Instruction.when(...) method makes an instruction conditional. The condition receives the previous measurement outcomes and decides whether the instruction should be applied in the current branch.

import numpy as np
import piquasso as pq

with pq.Program() as program:
   pq.Q(0, 1, 2) | pq.NumberState((1, 0, 0))
   pq.Q(0, 1) | pq.Beamsplitter(theta=np.pi / 4)

   # Measure mode 0 before the end of the program.
   pq.Q(0) | pq.ParticleNumberMeasurement()

   # If the last detected value was 0, route the remaining photon.
   pq.Q(1, 2) | pq.Beamsplitter(theta=np.pi / 2).when(
      lambda outcomes: outcomes[-1] == 0
   )

   pq.Q(1, 2) | pq.ParticleNumberMeasurement()

result = pq.PureFockSimulator(d=3).execute(program, shots=20)

Runtime-resolved parameters

Instruction parameters can also depend on earlier measurement outcomes. This is useful for adaptive programs where a later gate is calibrated from information obtained during the same execution.

import numpy as np
import piquasso as pq

program = pq.Program(
   instructions=[
      (
         pq.NumberState([0, 2]) + pq.NumberState([1, 1]) + pq.NumberState([2, 0])
      ) * np.sqrt(1 / 3),
      pq.ParticleNumberMeasurement().on_modes(1),
      pq.Squeezing(r=lambda x: 0.1 * x[-1] ** 2).on_modes(0),
   ]
)

result = pq.PureFockSimulator(d=2).execute(program, shots=10)

Using an expression string:

import numpy as np
import piquasso as pq

program = pq.Program(
   instructions=[
      (
         pq.NumberState([0, 2]) + pq.NumberState([1, 1]) + pq.NumberState([2, 0])
      ) * np.sqrt(1 / 3),
      pq.ParticleNumberMeasurement().on_modes(1),
      pq.Squeezing(r="0.1 * x[-1] ** 2").on_modes(0),
   ]
)

result = pq.PureFockSimulator(d=2).execute(program, shots=10)

Program construction styles

Most examples use the context-manager style, but the same program can also be created from an explicit instruction list. This can be convenient when programs are generated by another tool or assembled dynamically.

import numpy as np
import piquasso as pq

program = pq.Program(
   instructions=[
      pq.Vacuum(),
      pq.Squeezing(r=0.5).on_modes(0),
      pq.Beamsplitter(theta=np.pi / 4).on_modes(0, 1),
   ]
)

result = pq.GaussianSimulator(d=2).execute(program)