MerLin release 0.3 highlights

We are presenting the new features and changes introduced in MerLin 0.3.x.

0. Imports

[ ]:
import merlin as ml

1. New features

Here is a brief overview of the main new features.

1.1 Partial measurement

Partial measurement lets you measure only a subset of modes and keep the remaining modes as quantum state vectors. This is useful when you want classical probabilities for some modes while preserving quantum information in the rest. An example where this is useful is the QRNN algorithm that uses mid-circuit measurement to control other quantum operations. It is easy to implement with this feature: another QuantumLayer that takes for input the quantum state of the unmeasured modes and the measurements outputs is called after the original layer. It can then access all of the information needed to control the quantum state depending on the partial measure. The MerLin reproduction of this paper is available here. Another useful application of this new feature would be in a QCNN model where the pooling could be done with partial measurement. Those practical applications motivated the addition of this new feature.

Below is an example based on a 4-mode circuit. We measure modes [0, 1] out of 4 total modes with 2 photons in the Fock basis.

For more details, checkout the partial measurement documentation from which we present the following example.

[9]:
from merlin import CircuitBuilder, QuantumLayer
from merlin.core.computation_space import ComputationSpace
from merlin.measurement.strategies import MeasurementStrategy

# Minimal builder-based circuit definition.
builder = CircuitBuilder(n_modes=4)
builder.add_entangling_layer(trainable=True, name="U1")

# Partial measurement strategy (measure modes 0 and 1).
strategy = MeasurementStrategy.partial(
    modes=[0, 1],
    computation_space=ComputationSpace.FOCK,
)

layer = QuantumLayer(
    builder=builder,
    n_photons=2,
    measurement_strategy=strategy,
    return_object=True,
)

output = layer()

print(layer)
print(f" - Output type: {type(output)}")
print("--------- AMPLITUDES (unmeasured modes) ---------")
print(f" - Amplitudes = {output.amplitudes}")
print(f"\n - Amplitudes to tensor = {[amp.tensor for amp in output.amplitudes]}")
print(f"\n - Measured modes = {output.measured_modes}")
print(f"\n --------- PROBABILITIES (measured modes) ---------")
print(f"\n - Probabilities (tensor) = {output.probabilities}")
QuantumLayer(custom_circuit, modes=4, input_size=0, output_size=6)
 - Output type: <class 'merlin.core.partial_measurement.PartialMeasurement'>
--------- AMPLITUDES (unmeasured modes) ---------
 - Amplitudes = [StateVector(tensor=tensor([[ 0.0074-0.3472j,  0.3342+0.4957j, -0.1950-0.6957j]],
       grad_fn=<WhereBackward0>), n_modes=2, n_photons=2, _normalized=False), StateVector(tensor=tensor([[-0.7840+0.1447j,  0.6014-0.0526j]], grad_fn=<WhereBackward0>), n_modes=2, n_photons=1, _normalized=False), StateVector(tensor=tensor([[0.4434+0.8963j]], grad_fn=<WhereBackward0>), n_modes=2, n_photons=0, _normalized=False), StateVector(tensor=tensor([[-0.2238+0.7172j,  0.6204+0.2248j]], grad_fn=<WhereBackward0>), n_modes=2, n_photons=1, _normalized=False), StateVector(tensor=tensor([[0.5153-0.8570j]], grad_fn=<WhereBackward0>), n_modes=2, n_photons=0, _normalized=False), StateVector(tensor=tensor([[-0.9923-0.1241j]], grad_fn=<WhereBackward0>), n_modes=2, n_photons=0, _normalized=False)]

 - Amplitudes to tensor = [tensor([[ 0.0074-0.3472j,  0.3342+0.4957j, -0.1950-0.6957j]],
       grad_fn=<WhereBackward0>), tensor([[-0.7840+0.1447j,  0.6014-0.0526j]], grad_fn=<WhereBackward0>), tensor([[0.4434+0.8963j]], grad_fn=<WhereBackward0>), tensor([[-0.2238+0.7172j,  0.6204+0.2248j]], grad_fn=<WhereBackward0>), tensor([[0.5153-0.8570j]], grad_fn=<WhereBackward0>), tensor([[-0.9923-0.1241j]], grad_fn=<WhereBackward0>)]

 - Measured modes = (0, 1)

 --------- PROBABILITIES (measured modes) ---------

 - Probabilities (tensor) = tensor([[0.2409, 0.2107, 0.0586, 0.2046, 0.0394, 0.2459]],
       grad_fn=<StackBackward0>)

1.2 New measurement API

The MeasurementStrategy object is now redefined to be more expressive. Indeed, in this object, we now also define the following:

  • MeasurementStrategy: It selects how results are extracted from the quantum simulation or hardware backend.

  • grouping: LexGrouping and ModGrouping provide optional post-processing of outputs. It only works for partial measurement and probabilities.

Also, the deinition of a MeasurementStrategy is different. Enum-style and string access is deprecated and will be removed from MeasurementStrategy in v0.4. Here is the new way for each option.

  • Deprecated

    • Recommended replacement

  • MeasurementStrategy.PROBABILITIES

    • MeasurementStrategy.probs(computation_space=...)

  • MeasurementStrategy.MODE_EXPECTATIONS

    • MeasurementStrategy.mode_expectations(computation_space=...)

  • MeasurementStrategy.AMPLITUDES

    • MeasurementStrategy.amplitudes(computation_space=...)

  • MeasurementStrategy.NONE

    • MeasurementStrategy.amplitudes(computation_space=...)

  • "PROBABILITIES" (string)

    • MeasurementStrategy.probs(computation_space=...)

Lets define a QuantumLayer that uses a probabilities measurement in the full Fock basis and a LexGrouping strategy. We will define it using the old API and then, with the new one.

[10]:
# Define the circuit
circuit=ml.CircuitBuilder(n_modes=3)
circuit.add_entangling_layer()
circuit.add_angle_encoding([0,1])
circuit.add_entangling_layer()
[10]:
<merlin.builder.circuit_builder.CircuitBuilder at 0x13e3b46b0>

Old API, will be deprecated in v.0.4.x.

[ ]:
# import torch

# qlayer=ml.QuantumLayer(
#     input_size=2,
#     builder=circuit,n_photons=1,
#     measurement_strategy=ml.MeasurementStrategy.PROBABILITIES,
#     computation_space=ml.ComputationSpace.FOCK,
#     )
# qlayer_complete=torch.nn.Sequential(qlayer,ml.LexGrouping(3,2))

New API

[12]:
qlayer=ml.QuantumLayer(
    input_size=2,
    builder=circuit,n_photons=1,
    measurement_strategy=ml.MeasurementStrategy.probs(
        computation_space=ml.ComputationSpace.FOCK,
        grouping=ml.LexGrouping(3,2),
        ),
    )
qlayer.computation_space
[12]:
<ComputationSpace.FOCK: 'fock'>

2. Deprecations

The previous way of defining measurement strategy as presented earlier is deprecated (only a warning will be emitted).

There is another main deprecation.

2.1 The no_bunching flag is deprecated

The no_bunching flag used in many functions (QuantumLayer and kernels definitions) is deprecated and is removed since version 0.3.0. The new way of deciding to use the unbunched or full Fock computation space is with the ComputationSpace object in the MeasurementStrategy. Here we present the two ways to define a QuantumLayer with the equivalent of settng the no_bunching flag to True or False.

[ ]:
#Define the basic interferometer
circuit=ml.CircuitBuilder(n_modes=3)
circuit.add_entangling_layer()
circuit.add_angle_encoding([0,1])
circuit.add_entangling_layer()

Old flag, breaking change

[ ]:
# # Unbunched space
# qlayer=ml.QuantumLayer(
#     input_size=2,
#     builder=circuit,n_photons=1,
#     no_bunching=True
#     )

# # Full Fock space
# qlayer=ml.QuantumLayer(
#     input_size=2,
#     builder=circuit,n_photons=1,
#     no_bunching=False
#     )

Equivalents of the flag

[ ]:
#Equivalent of no_bunching=True
qlayer=ml.QuantumLayer(
    input_size=2,
    builder=circuit,n_photons=1,
    measurement_strategy=ml.MeasurementStrategy.probs(
        computation_space=ml.ComputationSpace.UNBUNCHED,
        ),
    )
print(qlayer.computation_space)

## Or, because the unbunched space is applied by default
qlayer=ml.QuantumLayer(
    input_size=2,
    builder=circuit,n_photons=1,
    )
print(qlayer.computation_space)

#Equivalent of no_bunching=False
qlayer=ml.QuantumLayer(
    input_size=2,
    builder=circuit,n_photons=1,
    measurement_strategy=ml.MeasurementStrategy.probs(
        computation_space=ml.ComputationSpace.FOCK,
        ),
    )
print(qlayer.computation_space)
ComputationSpace.UNBUNCHED
ComputationSpace.UNBUNCHED
ComputationSpace.FOCK