merlin.bridge.quantum_bridge module

Passive bridge between qubit statevectors and Merlin photonic computation spaces.

class merlin.bridge.quantum_bridge.QuantumBridge(n_photons, n_modes, *, qubit_groups=None, wires_order='little', computation_space=ComputationSpace.UNBUNCHED, normalize=True, device=None, dtype=torch.float32)

Bases: Module

Passive bridge between a qubit statevector (PyTorch tensor) and a Merlin QuantumLayer.

The bridge applies a fixed transition matrix that maps computational-basis amplitudes into the selected photonic computation space (Fock, unbunched, or dual-rail).

Args:

n_photons: Number of logical photons (equals len(qubit_groups)). n_modes: Total number of photonic modes that will be simulated downstream. qubit_groups: Logical grouping of qubits; [2, 1] means one photon is spread

over 2**2 modes and another over 2**1 modes.

wires_order: Endianness used to interpret computational basis strings. computation_space: Target photonic computation space. Accepts a

ComputationSpace enum or a string ("fock", "unbunched", "dual_rail").

normalize: Whether to L2-normalise input statevectors before applying the

transition matrix.

device: Optional device on which to place the output tensor. dtype: Real dtype that determines the corresponding complex dtype for amplitudes.

property basis_occupancies: tuple[tuple[int, ...], ...]

QLOQ occupancies indexed like the computational basis.

forward(psi)

Project a qubit statevector onto the selected photonic computation space.

Return type:

Tensor

Parameters

psitorch.Tensor

Input statevector with shape (2**n_qubits,) or (batch, 2**n_qubits). The tensor must reside on the bridge device.

Returns

torch.Tensor

Amplitudes ordered according to the computation-space enumeration. A 1D input returns a 1D tensor; batched inputs preserve the leading batch dimension.

Raises

TypeError

If psi is not a torch.Tensor.

ValueError

If the tensor shape or device is inconsistent with the bridge configuration.

property n_modes: int
property n_photons: int
property output_basis

Iterator over occupancies enumerating the selected computation space.

property output_size: int
qubit_to_fock_state(bitstring)

Convenience helper mirroring qubit_to_fock_state() with the bridge configuration.

Return type:

FockState

Parameters

bitstringstr

Computational basis string. Its length must equal sum(self.group_sizes).

Returns

perceval.BasicState

Photonic Fock state produced by the current qubit grouping convention.

transition_matrix()

Return the precomputed transition matrix.

Return type:

Tensor

Returns

torch.Tensor

Sparse COO tensor of shape (output_size, 2**n_qubits) mapping the qubit computational basis onto the selected photonic computation space.

Overview

The QuantumBridge provides a passive interface between PyTorch-compatible qubit statevector simulators (PennyLane, Qiskit, custom modules, …) and Merlin’s photonic quantum processors. It encodes computational-basis amplitudes into photonic spaces according to a one-photon-per-group scheme, enabling hybrid quantum machine learning workflows that combine qubit and photonic paradigms.

Key Features

  • Automatic State Conversion: Applies a predetermined transition matrix that maps qubit statevectors to photonic amplitudes ordered according to the selected computation space

  • Flexible Qubit Grouping: Supports arbitrary partitioning of qubits into groups for photonic encoding

  • Differentiable Pipeline: Maintains gradient flow from Merlin back to PennyLane for end-to-end training

  • Batch Processing: Handles batched inputs efficiently

  • Endianness Control: Supports both little-endian and big-endian qubit wire ordering

  • Inspectable Mapping: QuantumBridge.qubit_to_fock_state() reveals how individual bitstrings map to photonic occupancies for debugging or educational purposes

Architecture

The bridge operates in three stages:

  1. State Preparation: A PennyLane circuit or function generates a qubit statevector ψ ∈ ℂ^(2^n)

  2. Encoding: Each computational basis state |bitstring⟩ is mapped to a Fock state with one photon per qubit group

  3. Photonic Processing: The encoded superposition is fed to a Merlin QuantumLayer for photonic computation

Encoding Scheme

For a qubit system partitioned into groups of sizes [g₁, g₂, …, gₖ]:

  • Total qubits: n = g₁ + g₂ + … + gₖ

  • Total photonic modes: m = 2^g₁ + 2^g₂ + … + 2^gₖ

  • Total photons: k (one per group)

Each group of gᵢ qubits is encoded as one photon distributed across 2^gᵢ modes in a one-hot fashion.

Parameters

class merlin.bridge.quantum_bridge.QuantumBridge(*, qubit_groups, n_modes, n_photons, computation_space='unbunched', device=None, dtype=torch.float32, wires_order='little', normalize=True)
Parameters:
  • qubit_groups (list[int]) – List specifying the size of each qubit group. For example, [2, 2] splits 4 qubits into two groups of 2.

  • n_modes (int) – Total number of photonic modes (must equal Σ 2^group_size).

  • n_photons (int) – Number of photons in the photonic layer (must equal len(qubit_groups)).

  • computation_space (str) – Target computation space ("fock", "unbunched", or "dual_rail").

  • device (torch.device) – Target device for computation (default: None, infers from input).

  • dtype (torch.dtype) – Data type for real-valued tensors (default: torch.float32).

  • wires_order (str) – Qubit ordering convention - ‘little’ (LSB first) or ‘big’ (MSB first) (default: ‘little’).

  • normalize (bool) – Whether to normalize the input statevector (default: True).

Helper Functions

QuantumBridge.qubit_to_fock_state(bitstring)

Convenience helper mirroring qubit_to_fock_state() with the bridge configuration.

Return type:

FockState

Parameters

bitstringstr

Computational basis string. Its length must equal sum(self.group_sizes).

Returns

perceval.BasicState

Photonic Fock state produced by the current qubit grouping convention.

Converts a bitstring to a photonic BasicState using one-photon-per-group encoding.

param str qubit_state:

Binary string representing the qubit state (e.g., “1011”)

param list[int] group_sizes:

List of qubit group sizes

returns:

Perceval BasicState with one photon per group

rtype:

pcvl.BasicState

Usage Examples

Basic Example: Identity Circuit

This example demonstrates the bridge with a simple identity circuit, mapping basis states through the photonic layer:

import torch
import perceval as pcvl
from merlin import MeasurementStrategy, QuantumLayer
from merlin.bridge.quantum_bridge import ComputationSpace, QuantumBridge

# Create a simple identity circuit (m=4 modes, 2 photons)
circuit = pcvl.Circuit(4)
merlin_layer = QuantumLayer(
    input_size=0,
    circuit=circuit,
    n_photons=2,
    measurement_strategy=MeasurementStrategy.PROBABILITIES,
    no_bunching=True,
)

# Create the bridge
bridge = QuantumBridge(
    qubit_groups=[1, 1],  # Two 1-qubit groups
    n_modes=4,
    n_photons=2,
    wires_order='little',
    computation_space=ComputationSpace.UNBUNCHED,
    normalize=True,
)

# PennyLane-like state preparation module
class StatePrep(torch.nn.Module):
    def forward(self, _x):
        psi = torch.zeros(4, dtype=torch.complex64)
        psi[0] = 1.0 + 0.0j  # |00⟩ state
        return psi

state_prep = StatePrep()
model = torch.nn.Sequential(state_prep, bridge, merlin_layer)

dummy_input = torch.zeros(1, 1)
output = model(dummy_input)
print(f"Output shape: {output.shape}")
print(f"Output probabilities: {output}")

The bridge returns a complex amplitude tensor in the exact ordering expected by the QuantumLayer.

Hybrid Classification Task

A complete example showing hybrid qubit-photonic classification:

import torch
import torch.nn as nn
import pennylane as qml
import perceval as pcvl
from merlin import MeasurementStrategy, QuantumLayer
from merlin.bridge.quantum_bridge import ComputationSpace, QuantumBridge

class HybridQuantumClassifier(nn.Module):
    def __init__(self, n_qubits=3, n_classes=2):
        super().__init__()
        self.n_qubits = n_qubits

        # Classical pre-processing
        self.pre_net = nn.Sequential(
            nn.Linear(4, 8),
            nn.ReLU(),
            nn.Linear(8, n_qubits)
        )

        # PennyLane quantum circuit
        self.dev = qml.device('default.qubit', wires=n_qubits, shots=None)
        self.weights = nn.Parameter(torch.randn(n_qubits, 3))

        # Photonic processor
        m = 2 ** n_qubits  # One large group
        circuit = pcvl.Circuit(m)
        for i in range(m - 1):
            circuit.add(i, pcvl.BS())

        self.merlin_layer = QuantumLayer(
            input_size=0,
            circuit=circuit,
            n_photons=1,
            measurement_strategy=MeasurementStrategy.PROBABILITIES,
            no_bunching=True,
        )

        # Quantum bridge
        self.bridge = QuantumBridge(
            qubit_groups=[n_qubits],
            n_modes=m,
            n_photons=1,
            computation_space=ComputationSpace.UNBUNCHED,
            normalize=True,
        )

        # Classical post-processing
        self.post_net = nn.Linear(m, n_classes)

    def _quantum_state(self, x):
        @qml.qnode(self.dev, interface='torch', diff_method='backprop')
        def circuit(weights, features):
            qml.AngleEmbedding(features, wires=range(self.n_qubits))
            for i in range(self.n_qubits):
                qml.Rot(*weights[i], wires=i)
            for i in range(self.n_qubits - 1):
                qml.CNOT(wires=[i, i + 1])
            return qml.state()

        # Handle batching
        if x.ndim > 1:
            states = []
            for i in range(x.shape[0]):
                states.append(circuit(self.weights, x[i]))
            return torch.stack(states)
        return circuit(self.weights, x)

    def forward(self, x):
        # Classical preprocessing
        features = self.pre_net(x)
        psi = self._quantum_state(features)
        payload = self.bridge(psi)
        distribution = self.merlin_layer(payload)
        logits = self.post_net(distribution)
        return logits

# Usage
model = HybridQuantumClassifier(n_qubits=3, n_classes=2)
inputs = torch.randn(8, 4)  # Batch of 8 samples
outputs = model(inputs)
print(f"Classification outputs: {outputs.shape}")

Notes and Best Practices

Design Considerations

  • No Trainable Mapping: The bridge itself contains no trainable parameters. All variational behavior should be implemented in the PennyLane circuit.

  • Mode Requirements: The Merlin layer must be configured with m = Σ 2^group_size modes and n_photons = len(qubit_groups). No ancilla or post-selected modes are supported.

  • Differentiability: The bridge uses Merlin’s tensor superposition path to maintain gradient flow. Always call with apply_sampling=False (handled internally).

Performance Tips

  • Use normalize=False if your PennyLane circuit already outputs normalized states

  • For large qubit systems, consider using multiple smaller groups rather than one large group

  • Batch multiple samples together for better GPU utilization

See Also