QuantumLayer Essentials

The QuantumLayer is MerLin’s core building block for integrating quantum computation, as a single module, in a machine learning pipeline. It combines a Perceval photonic circuit (or experiment), optional classical parameters, and detector logic into a single differentiable module.

Overview

  • Autograd ready – QuantumLayer exposes a PyTorch Module interface, supports batching and differentiable forward passes, and plays nicely with optimisers or higher-level architectures.

  • Input encoding strategies - Pick a data encoding method: angle or amplitude encoding.. See Angle Encoding and Amplitude Encoding for more information.

  • Output measurement strategies – Select between probabilities, per-mode expectations, or raw amplitudes through MeasurementStrategy. The layer validates incompatible combinations (e.g. detectors with amplitude read-out). For more information on this and all possible output configurations, visit Measurement Strategy Guide. Grouping can be configured directly in the measurement strategy parameter. See Grouping Guide for more information.

  • Multiple construction paths – Build layers from the convenience simple() factory, a CircuitBuilder, a custom pcvl.Circuit or a fully specified pcvl.Experiment.

  • Detector awareness – Layers automatically derive detector transforms from the experiment, enabling threshold, PNR, or hybrid detection schemes.

  • Photon-loss aware – Experiments carrying a pcvl.NoiseModel trigger an automatic photon-loss transform so survival and loss outcomes share a single, normalised output distribution.

Initialisation recipes

QuantumLayer.simple()

The simple() helper generates a trainable interferometer with angle encoding that has input size + 1 modes. It is convenient for quick experiments, baselines or for machine learning experts without any prior knowledge in quantum machine learning.

import merlin as ML

layer = ML.QuantumLayer.simple(
    input_size=4,
    measurement_strategy=ML.MeasurementStrategy.probs(),
)

x = torch.rand(16, 4)
probs = layer(x)

CircuitBuilder

Use MerLin’s CircuitBuilder utilities to describe a circuit at a higher level. The builder maintains a record of the trainable parameters and the parameters used as layer inputs. A prefix-based naming scheme separates trainable parameters from those used as layer inputs. This is an ideal tool for quantum machine learning experts who do not have any experience with Perceval. More information in the CircuitBuilder API reference: CircuitBuilder

import torch
import merlin as ML

builder = ML.CircuitBuilder(n_modes=4)
builder.add_superpositions(depth=1)
builder.add_angle_encoding(modes=[0, 1], name="x")
builder.add_rotations(trainable=True, name="theta")

layer = ML.QuantumLayer(
    input_size=2,
    builder=builder,
    measurement_strategy=ML.MeasurementStrategy.probs(computation_space=ML.ComputationSpace.UNBUNCHED),
)

x = torch.rand(4, 2)
probs = layer(x)

Custom circuit

When you already have a pcvl.Circuit, provide the classical input layout and the trainable parameter prefixes explicitly. This initialization requires a good understanding of Perceval.

import perceval as pcvl
import torch
import merlin as ML

circuit = pcvl.Circuit(3)
circuit.add((0, 1), pcvl.BS())
circuit.add(0, pcvl.PS(pcvl.P("phi")))

layer = ML.QuantumLayer(
    input_size=1,
    circuit=circuit,
    input_parameters=["phi"],
    trainable_parameters=["theta"],
    input_state=[1, 0, 0],
    measurement_strategy=ML.MeasurementStrategy.probs(),
)

x = torch.linspace(0.0, 1.0, steps=8).unsqueeze(1)
probs = layer(x)

Note

input_state=[...] is accepted as a convenience input, but the layer stores it as a Perceval pcvl.BasicState (access the occupation vector via list(layer.input_state)). Tensor constructor states are removed; build a StateVector with from_tensor() when amplitude data must be passed as input_state. The same rule applies to set_input_state().

Experiment-driven

If you want to simulate a noise model or specify detectors characteristics, configure a pcvl.Experiment and pass it directly. The QuantumLayer inherits the circuit, detectors, and any photon-loss noise model you attached. This scheme is the one that gives the user the most options when utilizing a QuantumLayer.

import perceval as pcvl
import torch
import merlin as ML

circuit = pcvl.Circuit(2)
circuit.add((0, 1), pcvl.BS())

experiment = pcvl.Experiment(circuit)
experiment.detectors[0] = pcvl.Detector.threshold()
experiment.detectors[1] = pcvl.Detector.pnr()
experiment.noise = pcvl.NoiseModel(brightness=0.95, transmittance=0.9)

layer = ML.QuantumLayer(
    input_size=0,
    experiment=experiment,
    input_state=[1, 1],
    measurement_strategy=ML.MeasurementStrategy.probs(),
)

probs = layer()
detector_keys = layer.output_keys

Photon loss and detectors

  • Without an experiment, the layer defaults to ideal PNR detection on every mode, mirroring Perceval’s default behaviour.

  • experiment.noise = pcvl.NoiseModel(...) adds photon-loss sampling ahead of detector transforms. The resulting output_keys and output_size cover every survival/loss configuration implied by the noise model.

  • MeasurementStrategy.amplitudes() requires access to raw complex amplitudes and is therefore incompatible with custom detectors or photon-loss noise models. Active noise models raise a ValueError; custom detectors raise a RuntimeError. To emulate a detector pipeline while still inspecting amplitudes, run the layer without detectors and apply DetectorTransform manually to the resulting amplitudes.

  • Call output_keys() to inspect the classical outcomes produced by the detector transform.

Notes

  • input_state must match the number of circuit modes. When unspecified, the photons (denoted by n_photons) are evenly distributed across the modes (for instance, for dual-rail it defaults to [1,0,1,0,...]).

  • Both strong simulation (SLOS, which computes exact probabilities) and weak simulation (sampling) are supported. Sampling can be enabled using the shots and sampling_method parameters. See the SLOS: Strong Linear Optical Simulator for more information about strong and weak simulations.

  • The layer.parameters() method provides access to the trainable parameters (if any), just like any standard PyTorch layer.

  • Inspect layer.has_custom_noise_model and layer.output_keys to confirm whether photon loss is active and how it alters the output distribution.

Warning

Removed in version 0.4: The no_bunching flag is removed in version 0.4. Use MeasurementStrategy.probs(computation_space=ComputationSpace.UNBUNCHED) or MeasurementStrategy.probs(computation_space=ComputationSpace.FOCK) instead. See Migration guide.

Warning

Deprecated since version 0.4: The use of the computation_space argument in the QuantumLayer’s constructor is no longer supported as 0.4.0. Use the computation_space flag inside measurement_strategy instead. See Migration guide.

API Reference

class merlin.algorithms.layer.QuantumLayer(input_size=None, builder=None, circuit=None, experiment=None, input_state=None, n_photons=None, trainable_parameters=None, input_parameters=None, amplitude_encoding=False, measurement_strategy=None, return_object=False, noise=None, n_phase_error_samples=1, device=None, dtype=None)

Bases: MerlinModule

Quantum neural network layer with factory-based architecture.

This layer can be created either from a CircuitBuilder instance, a pre-compiled pcvl.Circuit, or an pcvl.Experiment.

detach_memristive_state(*, clear_history=False)

Detach the current memristive state without resetting its value.

This method is intended for manual truncated backpropagation through time. It cuts the autograd graph carried by the live recurrent memristive state, so future forward passes keep using the same numerical state values without backpropagating through earlier recurrence updates.

Parameters:

clear_history (bool) – Whether to replace each memristive history with only the detached current state. If False, the history length is preserved but stored tensors are detached. Default value is False.

Returns:

The layer is updated in place.

Return type:

None

export_config()

Export a standalone configuration for remote execution.

Returns:

Serializable layer configuration containing the resolved circuit, parameters, and input metadata.

Return type:

dict

forward(*input_parameters, shots=None, sampling_method=None, simultaneous_processes=None)

Forward pass through the quantum layer.

Encoding is inferred from the input type:

  • torch.Tensor (float): angle encoding (compatible with nn.Sequential)

  • torch.Tensor (complex): amplitude encoding

  • StateVector: amplitude encoding (preferred for quantum state injection)

Memristive State Updates

For layers with memristive elements, the state is updated after each forward pass according to the registered update rule. Gradient flow through the memristive recurrence is controlled by the detach_at_each_forward flag:

  • detach_at_each_forward=True (default): New states are detached, blocking gradients through the state recurrence. Earlier inputs receive zero gradients from memristive state chains. the entire accumulated state history.

Parameters:
  • input_parameters (torch.Tensor | merlin.core.state_vector.StateVector) – Input data. For angle encoding, pass float tensors. For amplitude encoding, pass a single StateVector or complex tensor.

  • shots (int | None) – Number of samples; if 0 or None, return exact amplitudes/probabilities.

  • sampling_method (str | None) – Sampling method, e.g. “multinomial”.

  • simultaneous_processes (int | None) – Batch size hint for parallel computation.

Returns:

Output after measurement mapping. Depending on the return_object argument and measurement strategy defined in the input, the output type will be different. Check the constructor for more details.

Return type:

torch.Tensor | PartialMeasurement | merlin.core.state_vector.StateVector | ProbabilityDistribution

Raises:
  • TypeError – If inputs mix torch.Tensor and StateVector, or if an unsupported input type is provided.

  • ValueError – If multiple StateVector inputs are provided.

  • RuntimeError – If batch size is inconsistent with memristive state (call reset(batch_size=N) to fix).

property has_custom_detectors: bool

Whether the wrapped experiment defines non-default detectors.

Type:

bool

memristive_history: list[list[torch.Tensor]]

Full history of memristive phase-shifter states since the last reset(), indexed by the memristive phase-shifters.

memristive_state: list[torch.Tensor]

Current state of each memristive phase-shifter.

property output_keys

Return the Fock basis associated with the layer outputs.

For g2 noise cases with photon loss/detectors, returns flattened keys matching the tensor output order. For other cases, returns keys with original structure.

property output_size: int

Number of values produced after measurement mapping.

Type:

int

prepare_parameters(input_parameters)

Prepare parameter list for circuit evaluation.

Return type:

list[Tensor]

processor: Any | None
reset(batch_size=1)

Resets the memristors to their initial state while clearing the history.

This also defines the allowed batch size to be ran per forward pass for circuits with memristive phase shifters.

Parameters:

batch_size (int) – Batch size that will be used in forward passes. Must be at least 1. Call this before each new batch to ensure memristive states are properly initialized.

Raises:

ValueError – If batch_size < 1.

Return type:

None

set_input_state(input_state)

Set the layer input state for subsequent evaluations.

Parameters:

input_state (merlin.core.state_vector.StateVector | pcvl.StateVector | pcvl.BasicState | tuple | list) – Input state to store on the layer and underlying computation process.

Raises:

ValueError – If torch.Tensor is passed as input_state.

Return type:

None

set_sampling_config(shots=None, sampling_method=None)

Deprecated: sampling configuration must be provided at call time in forward.

classmethod simple(cls, input_size, output_size=None, device=None, dtype=None, computation_space=ComputationSpace.UNBUNCHED)

Create a ready-to-train layer with a (input_size+1)-mode, ceil((input_size+1)/2)-photon architecture.

The circuit is assembled via CircuitBuilder with the following layout:

  1. A fully trainable entangling layer acting on all modes;

  2. A full input encoding layer spanning all encoded features;

  3. A fully trainable entangling layer acting on all modes.

Parameters:
  • input_size (int) – Size of the classical input vector. Must be 19 or lower.

  • output_size (int | None) – Optional classical output width.

  • device (torch.device | None) – Optional target device for tensors.

  • dtype (torch.dtype | None) – Optional tensor dtype.

  • computation_space (ComputationSpace | str) – Logical computation subspace; one of {"fock", "unbunched", "dual_rail"}.

Returns:

QuantumLayer configured with the described architecture.

Return type:

torch.nn.Module

to(*args, **kwargs)

Move the layer and auxiliary transforms to a new device or dtype.

Parameters:
Returns:

The updated layer instance.

Return type:

QuantumLayer

training: bool