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_RAILexpects everyqubit_groupsentry to be 1 and produces \(2^n\) outcomes—one per logical basis state.ComputationSpace.UNBUNCHEDenumerates 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.FOCKgenerates 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:
ModulePassive 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 over2**2modes and another over2**1modes.wires_order (Literal["little", "big"]) – Endianness used to interpret computational basis strings.
computation_space (ComputationSpace) – Target photonic computation space. Accepts a
ComputationSpaceenum 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.
- 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:
- Raises:
TypeError – If
psiis not atorch.Tensor.ValueError – If the tensor shape or device is inconsistent with the bridge configuration.
- property output_basis
Occupancies enumerating the selected computation space.
- 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:
- Raises:
ValueError – If
bitstringlength 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: