Source code for uniqc.pytorch.tq_quantum_layer
"""TorchQuantumLayer: nn.Module with native PyTorch autograd via TorchQuantum.
Unlike QuantumLayer (parameter-shift rule), this layer gets gradients
for free through TorchQuantum's differentiable statevector simulation.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING
try:
import torch
import torch.nn as nn
from uniqc.simulator.torchquantum_simulator import TORCHQUANTUM_AVAILABLE, TorchQuantumSimulator
TORCH_AVAILABLE = True
except ImportError:
TORCH_AVAILABLE = False
TORCHQUANTUM_AVAILABLE = False
[docs]
class nn: # type: ignore
torch = None # type: ignore
if TYPE_CHECKING:
pass
__all__ = ["TorchQuantumLayer"]
if TORCH_AVAILABLE and TORCHQUANTUM_AVAILABLE:
class TorchQuantumLayer(nn.Module):
"""PyTorch layer using TorchQuantum for native autograd.
Takes a circuit builder callable that constructs opcodes from a
parameter tensor. Gradients propagate through PyTorch autograd
natively — no parameter-shift rule needed.
Args:
circuit_builder: Callable(params_tensor) -> (opcode_list, n_qubits,
param_overrides). Constructs the circuit with tensor parameters.
n_qubits: Number of qubits.
n_params: Number of trainable parameters.
hamiltonian: List of (pauli_string, coefficient) for expectation.
init_params: Initial parameter values (optional).
device: "cpu" or "cuda".
"""
def __init__(
self,
circuit_builder: Callable[[torch.Tensor], tuple],
n_qubits: int,
n_params: int,
hamiltonian: list[tuple[str, float]],
init_params: torch.Tensor | None = None,
device: str = "cpu",
):
super().__init__()
self.circuit_builder = circuit_builder
self.n_qubits = n_qubits
self.hamiltonian = hamiltonian
self._device = device
self._sim = TorchQuantumSimulator(n_wires=n_qubits, device=device)
if init_params is not None:
self.params = nn.Parameter(init_params.clone().to(device))
else:
self.params = nn.Parameter(torch.randn(n_params, device=device) * 0.1)
def forward(self, x: torch.Tensor | None = None) -> torch.Tensor:
"""Execute circuit and return expectation value.
Args:
x: Optional input tensor (for data encoding circuits).
Returns:
Differentiable scalar tensor with expectation value.
"""
params = self.params
if x is not None:
# Concatenate quantum params with encoded data
params = torch.cat([self.params, x.flatten()])
opcode_list, n_qubits, param_overrides = self.circuit_builder(params)
return self._sim.expectation(opcode_list, self.hamiltonian, param_overrides)
def extra_repr(self) -> str:
return f"n_qubits={self.n_qubits}, n_params={len(self.params)}"
else:
[docs]
class TorchQuantumLayer: # type: ignore
"""Placeholder when PyTorch/TorchQuantum is not installed."""
def __init__(self, *args, **kwargs):
raise ImportError(
"PyTorch and TorchQuantum are required. "
"Install with: pip install unified-quantum[pytorch] && "
'pip install "torchquantum @ '
'git+https://github.com/Agony5757/torchquantum.git@fix/optional-qiskit-deps"'
)