Source code for uniqc.pytorch.quantum_layer

"""
QuantumLayer: PyTorch nn.Module for quantum circuits.

This module provides a PyTorch-compatible layer that wraps a parametric
quantum circuit, enabling gradient-based optimization via the parameter-shift
rule.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING

import numpy as np

try:
    import torch
    import torch.nn as nn

    TORCH_AVAILABLE = True
except ImportError:
    TORCH_AVAILABLE = False

    # Create placeholder classes for type hints
[docs] class nn: # type: ignore
[docs] class Module: pass
torch = None # type: ignore if TYPE_CHECKING: from uniqc.circuit_builder import Circuit __all__ = ["QuantumLayer"] if TORCH_AVAILABLE: class _QuantumFunction(torch.autograd.Function): """Custom autograd function using parameter-shift rule.""" @staticmethod def forward(ctx, params, circuit_template, expectation_fn, param_names, shift): ctx.save_for_backward(params) ctx.circuit_template = circuit_template ctx.expectation_fn = expectation_fn ctx.param_names = param_names ctx.shift = shift # Create bound circuit param_values = params.detach().numpy() bound_circuit = circuit_template.copy() for name, value in zip(param_names, param_values, strict=True): if name in bound_circuit._parameters: bound_circuit._parameters[name].bind(float(value)) result = expectation_fn(bound_circuit) return torch.tensor([result], dtype=params.dtype) @staticmethod def backward(ctx, grad_output): (params,) = ctx.saved_tensors circuit_template = ctx.circuit_template expectation_fn = ctx.expectation_fn param_names = ctx.param_names shift = ctx.shift param_values = params.detach().numpy() param_grads = [] for i, _name in enumerate(param_names): # Plus shift plus_circuit = circuit_template.copy() for j, n in enumerate(param_names): val = param_values[j] + (shift if j == i else 0.0) if n in plus_circuit._parameters: plus_circuit._parameters[n].bind(float(val)) exp_plus = expectation_fn(plus_circuit) # Minus shift minus_circuit = circuit_template.copy() for j, n in enumerate(param_names): val = param_values[j] - (shift if j == i else 0.0) if n in minus_circuit._parameters: minus_circuit._parameters[n].bind(float(val)) exp_minus = expectation_fn(minus_circuit) grad = 0.5 * (exp_plus - exp_minus) param_grads.append(grad) grad_params = torch.tensor(param_grads, dtype=params.dtype) * grad_output return grad_params, None, None, None, None class QuantumLayer(nn.Module): """PyTorch layer wrapping a parametric quantum circuit. Supports automatic differentiation via the parameter-shift rule for gradient-based optimization. Args: circuit: Parametric Circuit or template with _parameters expectation_fn: Function computing expectation from a bound circuit n_outputs: Number of output values (default: 1) init_params: Initial parameter values (optional) shift: Shift value for parameter-shift rule (default: π/2) Example: >>> @circuit_def(name="vqe", qregs={"q": 2}, params=["theta"]) ... def vqe_circuit(circ, q, theta): ... circ.ry(q[0], theta[0]) ... circ.cnot(q[0], q[1]) ... return circ >>> >>> qlayer = QuantumLayer( ... circuit=vqe_circuit.build_standalone(), ... expectation_fn=lambda c: simulate_and_measure(c) ... ) >>> optimizer = torch.optim.Adam(qlayer.parameters(), lr=0.01) """ def __init__( self, circuit: Circuit, expectation_fn: Callable[[Circuit], float], n_outputs: int = 1, init_params: torch.Tensor | None = None, shift: float = np.pi / 2, ): super().__init__() self._circuit_template = circuit self._expectation_fn = expectation_fn self._n_outputs = n_outputs self._shift = shift # Get parameter info from circuit if hasattr(circuit, "_parameters"): self._param_names = list(circuit._parameters.keys()) n_params = len(self._param_names) else: self._param_names = [] n_params = 0 # Initialize trainable parameters if init_params is not None: self.params = nn.Parameter(init_params.clone()) else: self.params = nn.Parameter(torch.randn(n_params) * 0.1) def forward(self, x: torch.Tensor | None = None) -> torch.Tensor: """Execute the quantum circuit and return expectation values. Args: x: Optional input tensor (for data encoding circuits) Returns: Tensor of expectation values """ return _QuantumFunction.apply( self.params, self._circuit_template, self._expectation_fn, self._param_names, self._shift, ) def extra_repr(self) -> str: return f"n_params={len(self._param_names)}, n_outputs={self._n_outputs}" else: # Placeholder when PyTorch is not available
[docs] class QuantumLayer: # type: ignore """Placeholder when PyTorch is not installed.""" def __init__(self, *args, **kwargs): raise ImportError("PyTorch is not installed. Install with: pip install unified-quantum[pytorch]")