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:
objectConvert 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
phiis mapped toround(phi / phase_imprecision) * phase_imprecisionwith 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 followtorch.roundbehavior. For example,phi = pi / 8withphase_imprecision = pi / 4quantizes to0becauseround(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_imprecisionstep. When active, the effective sampled phase isround(phi / phase_imprecision) * phase_imprecision + epsilonwithepsilon ~ Uniform(-phase_error, phase_error). Ifphase_imprecisionis inactive, the sampled phase isphi + epsilon. Fresh samples are drawn only whento_tensor()is called withapply_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:
Initialization: User passes phase_imprecision and/or phase_error to CircuitConverter via QuantumLayer (Step 4: through InitializationContext → ComputationProcessFactory → ComputationProcess → CircuitConverter).
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.
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.
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).
Effective Phase: For a phase shifter commanded to phase
phi, the forward phase is:phiwhen both phase noises are inactive;round(phi / phase_imprecision) * phase_imprecisionwhen onlyphase_imprecisionis active;phi + epsilonwhen onlyphase_erroris active;round(phi / phase_imprecision) * phase_imprecision + epsilonwhen 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
dtypeis 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:
selffor method chaining.- Return type:
- Raises:
TypeError – If
devicetype 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:
phase_imprecision (deterministic, always applied): If configured, every phase is quantized to the nearest multiple of
phase_imprecisionusing a straight-through estimator. This usestorch.round(phase / phase_imprecision) * phase_imprecision: it is nearest-grid rounding, not truncation. Exact half-step ties followtorch.roundbehavior, sopi / 8with api / 4step quantizes to0. Gradients flow through the commanded phase, while the forward pass uses the quantized value. This is always active and does not requireapply_phase_error=True.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 ofinput_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_errorvalues during this conversion. This flag does not affect deterministicphase_imprecisionquantization, which is applied wheneverphase_imprecisionis 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:
- Raises:
ValueError – If the wrong number of input tensors is provided.
TypeError – If
input_paramsis 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).