merlin.pcvl_pytorch.locirc_to_tensor module

class merlin.pcvl_pytorch.locirc_to_tensor.CircuitConverter(circuit, input_specs=None, memristive_metadata=None, dtype=torch.complex64, device=device(type='cpu'), phase_imprecision=0.0, phase_error=0.0)

Bases: object

Convert a parameterized Perceval circuit into a differentiable PyTorch unitary matrix.

This class converts Perceval quantum circuits into PyTorch tensors that can be used in neural network training with automatic differentiation. It supports batch processing for efficient training and handles various quantum components like beam splitters, phase shifters, and unitary operations.

Parameters:
  • circuit (pcvl.Circuit) – Perceval circuit to convert.

  • input_specs (list[str] | None) – Parameter name prefixes used to group parameters into input tensors.

  • dtype (torch.dtype) – Target tensor dtype.

  • device (torch.device) – Device used for tensor operations.

  • phase_imprecision (float) – Deterministic quantization step applied to every phase shifter before building the unitary. This models finite phase-setting resolution: a commanded phase phi is mapped to round(phi / phase_imprecision) * phase_imprecision with a straight-through estimator, so the forward pass uses the quantized value while the backward pass keeps the identity gradient through the commanded phase. This is nearest-grid rounding, not truncation. Exact half-step ties follow torch.round behavior. For example, phi = pi / 8 with phase_imprecision = pi / 4 quantizes to 0 because round(0.5) == 0. Default value is 0.0.

  • phase_error (float) – Stochastic uniform perturbation half-width in radians. This models random phase noise around the quantized phase after any phase_imprecision step. When active, the effective sampled phase is round(phi / phase_imprecision) * phase_imprecision + epsilon with epsilon ~ Uniform(-phase_error, phase_error). If phase_imprecision is inactive, the sampled phase is phi + epsilon. Fresh samples are drawn only when to_tensor() is called with apply_phase_error=True; otherwise the converter remains deterministic. Default value is 0.0.

Notes

Supported Components:
  • PS (Phase Shifter)

  • BS (Beam Splitter)

  • PERM (Permutation)

  • Unitary (Generic unitary matrix)

  • Barrier (no-op, removed during compilation)

Phase Noise Parameter Flow:

Phase noise parameters (phase_imprecision and phase_error) are configured at converter initialization and automatically applied during unitary generation. The flow is:

  1. Initialization: User passes phase_imprecision and/or phase_error to CircuitConverter via QuantumLayer (Step 4: through InitializationContext → ComputationProcessFactory → ComputationProcess → CircuitConverter).

  2. Compilation: During _compile_circuit(), constant phase shifters are marked as dynamic if phase_error > 0.0, ensuring fresh perturbations on each call. Quantization-only noise allows precomputation since it is deterministic.

  3. Conversion: Each call to to_tensor(*params, apply_phase_error=bool) applies both quantization (always, if configured) and perturbations (only if apply_phase_error=True). Monte Carlo sampling is done by calling to_tensor() multiple times with apply_phase_error=True and averaging the resulting probability distributions.

  4. Gradient Flow: Phase quantization uses straight-through estimators to preserve gradients to the commanded phase. Perturbations use torch.empty_like(phase) to ensure proper device/dtype handling and do NOT require gradients (they are stochastic noise, not learnable parameters).

  5. Effective Phase: For a phase shifter commanded to phase phi, the forward phase is:

    • phi when both phase noises are inactive;

    • round(phi / phase_imprecision) * phase_imprecision when only phase_imprecision is active;

    • phi + epsilon when only phase_error is active;

    • round(phi / phase_imprecision) * phase_imprecision + epsilon when both are active.

    The quantization uses nearest-grid rounding through torch.round(); it is not floor or truncation.

Example:

Basic usage with a single phase shifter:

>>> import torch
>>> import perceval as pcvl
>>> from merlin.pcvl_pytorch.locirc_to_tensor import CircuitConverter
>>>
>>> # Create a simple circuit with one phase shifter
>>> circuit = pcvl.Circuit(1) // pcvl.PS(pcvl.P("phi"))
>>>
>>> # Convert to PyTorch with gradient tracking
>>> converter = CircuitConverter(circuit, input_specs=["phi"])
>>> phi_params = torch.tensor([0.5], requires_grad=True)
>>> unitary = converter.to_tensor(phi_params)
>>> print(unitary.shape)  # torch.Size([1, 1])

Multiple parameters with grouping:

>>> # Circuit with multiple phase shifters
>>> circuit = (pcvl.Circuit(2)
...            // pcvl.PS(pcvl.P("theta1"))
...            // (1, pcvl.PS(pcvl.P("theta2"))))
>>>
>>> converter = CircuitConverter(circuit, input_specs=["theta"])
>>> theta_params = torch.tensor([0.1, 0.2], requires_grad=True)
>>> unitary = converter.to_tensor(theta_params)
>>> print(unitary.shape)  # torch.Size([2, 2])

Batch processing for training:

>>> # Batch of parameter values
>>> batch_params = torch.tensor([[0.1], [0.2], [0.3]], requires_grad=True)
>>> converter = CircuitConverter(circuit, input_specs=["phi"])
>>> batch_unitary = converter.to_tensor(batch_params)
>>> print(batch_unitary.shape)  # torch.Size([3, 1, 1])

Training integration:

>>> # Training loop with beam splitter
>>> circuit = pcvl.Circuit(2) // pcvl.BS.Rx(pcvl.P("theta"))
>>> converter = CircuitConverter(circuit, ["theta"])
>>> theta = torch.tensor([0.5], requires_grad=True)
>>> optimizer = torch.optim.Adam([theta], lr=0.01)
>>>
>>> for step in range(10):
...     optimizer.zero_grad()
...     unitary = converter.to_tensor(theta)
...     loss = some_loss_function(unitary)
...     loss.backward()
...     optimizer.step()
set_dtype(dtype)

Set the tensor data types for float and complex operations.

Parameters:

dtype (torch.dtype) – Target dtype (float32/complex64 or float64/complex128).

Raises:

TypeError – If dtype is not supported.

to(dtype, device)

Move the converter to a specific device and dtype.

Parameters:
  • dtype (torch.dtype) – Target tensor dtype (float32/complex64 or float64/complex128).

  • device (str | torch.device) – Target device (string or torch.device).

Returns:

self for method chaining.

Return type:

CircuitConverter

Raises:

TypeError – If device type is not supported.

to_tensor(*input_params, batch_size=None, apply_phase_error=False, memristive_current_state=None)

Convert the parameterized circuit to a PyTorch unitary tensor.

Phase Noise Processing:

This method applies configured phase noise to all phase shifters during unitary generation. The noise is applied in two stages:

  1. phase_imprecision (deterministic, always applied): If configured, every phase is quantized to the nearest multiple of phase_imprecision using a straight-through estimator. This uses torch.round(phase / phase_imprecision) * phase_imprecision: it is nearest-grid rounding, not truncation. Exact half-step ties follow torch.round behavior, so pi / 8 with a pi / 4 step quantizes to 0. Gradients flow through the commanded phase, while the forward pass uses the quantized value. This is always active and does not require apply_phase_error=True.

  2. phase_error (stochastic, controlled by apply_phase_error flag): If configured and apply_phase_error=True, fresh samples from Uniform(-phase_error, phase_error) are drawn and added to each phase after quantization. The samples respect the phase tensor’s device and dtype via torch.empty_like(). Each call with apply_phase_error=True produces a different unitary. For Monte Carlo averaging of probabilistic outputs, call this method multiple times with apply_phase_error=True, collect the resulting probability distributions, and average them.

Parameter Flow (see class Notes for full context): - layer_utils.classify_noise() → extracts phase settings to NoiseGroups - ComputationProcess.__init__() → stores phase settings from NoiseGroups - ComputationProcess._setup_computation_graphs() → passes to CircuitConverter - CircuitConverter.to_tensor() ← receives apply_phase_error flag each call

Parameters:
  • input_params (torch.Tensor) – Variable number of parameter tensors. Each tensor has shape (num_params,) or (batch_size, num_params) in the order of input_specs.

  • batch_size (int | None) – Explicit batch size. If None, it is inferred from the input tensors.

  • memristive_current_state (list[torch.Tensor] | None) – The memristive phase shifters current states. Defaults to None and will be treated as an empty list.

  • apply_phase_error (bool) – Whether to draw fresh stochastic perturbations for configured phase_error values during this conversion. This flag does not affect deterministic phase_imprecision quantization, which is applied whenever phase_imprecision is positive. The perturbation is added after quantization. Default value is False.

Returns:

Complex unitary tensor of shape (circuit.m, circuit.m) for a single sample or (batch_size, circuit.m, circuit.m) for batched inputs.

Return type:

torch.Tensor

Raises:
  • ValueError – If the wrong number of input tensors is provided.

  • TypeError – If input_params is not a list or tuple.

Note

CircuitConverter applies circuit phase noise directly to phase-shifter values before constructing the unitary tensor. phase_imprecision uses nearest-grid quantization with torch.round(phase / phase_imprecision) * phase_imprecision. It does not floor or truncate phases. Exact half-step ties follow torch.round behavior; for example, pi / 8 with a pi / 4 imprecision step quantizes to 0.

phase_error is added after any quantization and only when to_tensor(..., apply_phase_error=True) is used. The sampled effective phase is phase_quantized + epsilon with epsilon drawn from Uniform(-phase_error, phase_error).