| import math |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| from config import DROPOUT |
|
|
|
|
| class ClassicalControlLayer(nn.Module): |
| """ |
| Parameter-matched classical control for QuantumEvolutionLayer. |
| |
| Same interface (H -> (q_state, energy)) and the same surrounding |
| compress / expand / fusion_gate / correction / energy_head |
| architecture -- the only thing that differs is what sits between |
| compress and expand. QuantumEvolutionLayer runs a PennyLane circuit |
| there; this runs a small classical MLP operating at the same width |
| (n_qubits*3, matching the circuit's flat measurement-vector size). |
| |
| Used to isolate whether QuantumEvolutionLayer's benefit (if any) |
| comes from the quantum circuit's structure (superposition + |
| entanglement) specifically, or just from having *some* extra |
| nonlinear transform in that position of the pipeline. The quantum |
| circuit itself is a tiny fraction of QuantumEvolutionLayer's total |
| parameters (54 out of ~1.12M at dim=384, n_qubits=6, n_layers=3), |
| so keeping everything else identical and only swapping that one |
| small piece keeps total parameter count within a fraction of a |
| percent either way -- see the printed counts below. |
| """ |
|
|
| def __init__(self, dim, n_qubits=6, n_layers=3, verbose=False): |
|
|
| super().__init__() |
|
|
| self.n_qubits = n_qubits |
|
|
| self.compress = nn.Sequential( |
| nn.Linear(dim, dim), |
| nn.GELU(), |
| nn.Linear(dim, n_qubits), |
| ) |
|
|
| bottleneck_dim = n_qubits * 3 |
|
|
| |
| |
| |
| transform_layers = [nn.Linear(n_qubits, bottleneck_dim), nn.GELU()] |
|
|
| for _ in range(n_layers - 1): |
| transform_layers += [ |
| nn.Linear(bottleneck_dim, bottleneck_dim), |
| nn.GELU(), |
| ] |
|
|
| self.classical_transform = nn.Sequential(*transform_layers) |
|
|
| self.expand = nn.Sequential( |
| nn.Linear(bottleneck_dim, dim), |
| nn.GELU(), |
| nn.LayerNorm(dim), |
| nn.Dropout(DROPOUT), |
| ) |
|
|
| self.fusion_gate = nn.Sequential( |
| nn.Linear(dim * 2, dim), |
| nn.GELU(), |
| nn.Linear(dim, dim), |
| nn.Sigmoid(), |
| ) |
|
|
| self.correction = nn.Sequential( |
| nn.Linear(dim, dim), |
| nn.GELU(), |
| nn.Dropout(DROPOUT), |
| nn.Linear(dim, dim), |
| ) |
|
|
| |
| |
| |
| |
| |
| self.phase_gain = nn.Parameter(torch.tensor(1.0)) |
|
|
| self.energy_head = nn.Sequential( |
| nn.Linear(dim, dim), |
| nn.GELU(), |
| nn.LayerNorm(dim), |
| nn.Linear(dim, dim // 2), |
| nn.GELU(), |
| nn.Linear(dim // 2, 1), |
| ) |
|
|
| self.norm = nn.LayerNorm(dim) |
|
|
| if verbose: |
| total_params = sum(p.numel() for p in self.parameters()) |
| transform_params = sum( |
| p.numel() for p in self.classical_transform.parameters() |
| ) |
| print( |
| f"[ClassicalControlLayer] total parameters: {total_params:,} " |
| f"(classical_transform slot: {transform_params:,})" |
| ) |
|
|
| def forward(self, H): |
|
|
| B, K, D = H.shape |
|
|
| H_norm = F.normalize(H, dim=-1) |
|
|
| z = self.compress(H_norm.reshape(B * K, D)) |
|
|
| |
| |
| |
| |
| z = math.pi * torch.tanh(z) |
|
|
| measurements = self.classical_transform(z) |
|
|
| n = self.n_qubits |
|
|
| x_mean = measurements[:, :n].mean(dim=-1) |
| y_mean = measurements[:, n:2 * n].mean(dim=-1) |
|
|
| phase = self.phase_gain * torch.atan2(y_mean, x_mean) |
|
|
| q = self.expand(measurements) |
|
|
| H_flat = H.reshape(B * K, D) |
|
|
| gate = self.fusion_gate(torch.cat([H_flat, q], dim=-1)) |
|
|
| q = gate * q + (1 - gate) * H_flat |
|
|
| q = self.norm(q + self.correction(q)) |
|
|
| energy = self.energy_head(q) |
|
|
| energy = torch.tanh(energy).squeeze(-1) |
|
|
| q = q.reshape(B, K, D) |
|
|
| energy = energy.reshape(B, K) |
|
|
| phase = phase.reshape(B, K) |
|
|
| return q, energy, phase |