Quantum Bridge (Qubit ↔ Merlin)

Overview

The Quantum Bridge lets you plug a qubit state-preparation module into a Merlin QuantumLayer by mapping computational basis states into photonic Fock states using a one-photon-per-group encoding. Any PyTorch-compatible noisy or noiseless simulator that outputs a complex statevector of size \(2^n\) can be placed upstream—PennyLane is a common choice but not a requirement.

Key ideas: - You provide a preconfigured QuantumLayer (the bridge is parameter-free). - You insert the bridge between a qubit-state module (e.g., a PennyLane QNode with interface="torch") that outputs a complex statevector of size \(2^n\) and the target QuantumLayer. - You partition the n qubits into groups via qubit_groups; [2, 1] means a two-qubit block encoded in four modes plus a dual-rail qubit, generalising dual-rail into the full QLOQ family. - n_modes must equal \(\sum_i 2^{k_i}\) and n_photons equals len(qubit_groups). - Select the target computation space (fock, unbunched, or dual_rail); the bridge builds the corresponding transition matrix and emits amplitudes already aligned with the photonic key ordering. - QuantumLayer can now consume the emitted tensor directly—no metadata plumbing or manual indexing required.

Minimal example

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

# Build a simple identity photonic circuit with m = sum(2**g) modes
qubit_groups = [1, 1]  # two groups of one qubit each → 2 photons, 4 modes
m = sum(2**g for g in qubit_groups)

circuit = pcvl.Circuit(m)
layer = QuantumLayer(
    input_size=0,
    circuit=circuit,
    n_photons=len(qubit_groups),
    measurement_strategy=MeasurementStrategy.probs(computation_space=ComputationSpace.UNBUNCHED),
    device=torch.device("cpu"),
    dtype=torch.float32,
)

class QubitStatePrep(torch.nn.Module):
    """Return a qubit statevector |01> (similar to what a PennyLane QNode would emit)."""

    def forward(self, _x: torch.Tensor) -> torch.Tensor:
        psi = torch.zeros(4, dtype=torch.complex64)
        psi[1] = 1 + 0j  # |01>
        return psi

state_prep = QubitStatePrep()
bridge = QuantumBridge(
    qubit_groups=qubit_groups,
    n_modes=m,
    n_photons=len(qubit_groups),
    wires_order="little",  # or "big"
    computation_space=ComputationSpace.UNBUNCHED,
    normalize=True,        # L2-normalize the input state
)

model = torch.nn.Sequential(state_prep, bridge, layer)

x = torch.zeros(1, 1)  # dummy input; state prep ignores it
y = model(x)           # probability distribution over photonic outcomes

The bridge left-multiplies the statevector by its precomputed transition matrix and emits amplitudes ordered exactly like QuantumLayer’s mapped_keys. Gradients then flow from y back through the photonic layer into the upstream qubit state preparation. Because nn.Sequential modules exchange a single argument, the bridge does not forward additional positional inputs; wrap bridge and layer in a custom module if you need to thread extra data alongside the statevector.

Note

The qubit_to_fock_state() helper exposes the exact bitstring → Fock-state mapping used internally. This is handy for spot-checking amplitudes or visualising how logical qubits populate photonic modes before running a full simulation.

Choosing the computation space

computation_space controls both the size and ordering of the emitted tensor:

  • ComputationSpace.DUAL_RAIL expects every qubit_groups entry to be 1 and produces \(2^n\) outcomes—one per logical basis state.

  • ComputationSpace.UNBUNCHED enumerates all configurations with at most one photon per mode (\(\binom{m}{n_{\text{photons}}}\) outcomes). The bridge populates only the subset consistent with its qubit groups; the remaining entries are zero.

  • ComputationSpace.FOCK generates the full Fock space with bunching allowed (\(\binom{m + n_{\text{photons}} - 1}{n_{\text{photons}}}\) outcomes). As with the unbunched space, amplitudes outside the logical subspace are zero but remain available to downstream Merlin components.

Convenience properties bridge.n_modes and bridge.n_photons expose the photonic settings that should be used when instantiating the receiving QuantumLayer.

Inspecting the QLOQ transformation

The bridge conceptually applies a transition matrix that maps the computational basis to the photonic QLOQ basis. Calling transition_matrix() returns this sparse operator immediately after bridge construction. The matrix has shape (output_dim, 2**n_qubits) and contains exactly one non-zero entry per computational basis column.

Devices and dtypes

  • The bridge output device follows the QuantumLayer device. Ensure the layer and bridge use the same device (CPU/CUDA).

  • The bridge converts input states to complex64 for float32 and to complex128 for float64. The emitted probability distribution keeps the QuantumLayer real dtype (float32/float64).

Constraints

  • No ancilla or postselected modes; total modes m must equal sum(2**group_size).

  • Number of photons equals the number of groups.

  • QuantumLayer must be provided; the bridge does not create circuits or perform validation against it.

API

class merlin.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).

Parameters:
  • n_photons (int) – Number of logical photons (equals len(qubit_groups)).

  • n_modes (int) – Total number of photonic modes that will be simulated downstream.

  • qubit_groups (Sequence[int] | None) – Logical grouping of qubits; [2, 1] means one photon is spread over 2**2 modes and another over 2**1 modes.

  • wires_order (Literal["little", "big"]) – Endianness used to interpret computational basis strings.

  • computation_space (ComputationSpace) – Target photonic computation space. Accepts a ComputationSpace enum value.

  • normalize (bool) – Whether to L2-normalise input statevectors before applying the transition matrix.

  • device (torch.device | None) – Optional device on which to place the output tensor.

  • dtype (torch.dtype) – Real dtype that determines the corresponding complex dtype for amplitudes.

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

QLOQ occupancies in computational-basis order.

Type:

tuple[tuple[int, …], …]

forward(psi)

Project a qubit statevector onto the selected photonic computation space.

Parameters:

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

Returns:

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

Return type:

torch.Tensor

Raises:
property n_modes: int

Total number of photonic modes.

Type:

int

property n_photons: int

Number of logical photons.

Type:

int

property output_basis

Occupancies enumerating the selected computation space.

Type:

Iterator[tuple[int, …]]

property output_size: int

Size of the selected photonic computation space.

Type:

int

qubit_to_fock_state(bitstring)

Convenience helper mirroring qubit_to_fock_state with the bridge configuration.

Parameters:

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

Returns:

Photonic Fock state produced by the current qubit grouping convention.

Return type:

pcvl.BasicState

Raises:

ValueError – If bitstring length is inconsistent with the bridge configuration.

transition_matrix()

Return the precomputed transition matrix.

Returns:

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

Return type:

torch.Tensor