Source code for pyqit.models.base.quantum_model
from abc import abstractmethod
import pennylane as qml
import pennylane.numpy as pnp
from skbase.utils.dependencies import _check_soft_dependencies
from pyqit.models.base.base import BaseModel
[docs]
class BaseQuantumModel(BaseModel):
"""Base class wiring a PennyLane QNode into either backend.
Subclasses build a QNode and call `register_qnode`, which handles the
pennylane/torch fork. The weight registry itself is `BaseModel`'s.
"""
_tags = {
"object_type": "model",
"is_quantum": True,
"model_type": "quantum",
}
def __init__(
self,
device="default.qubit",
shots=None,
diff_method="best",
):
super().__init__()
self.device = device
self.shots = shots
self.diff_method = diff_method
[docs]
def get_interface(self):
"""PennyLane QNode interface for the active backend."""
return "torch" if self.backend == "torch" else "autograd"
[docs]
def init_weights(self, weight_shapes: dict) -> dict:
"""Draw uniform ``[0, 1)`` starting weights from numpy's global RNG.
The range matches Qiskit ML's default ``initial_point`` and PennyLane's
template examples; uniform ``[0, 2pi)`` is the Haar-like regime where
gradients vanish (McClean et al. 2018).
Call this before building the device: a PennyLane device seeded
``"global"`` consumes numpy's RNG at construction by a device-dependent
amount, so weights drawn after it differ per device for the same seed.
Parameters
----------
weight_shapes : dict
Weight name to shape, as returned by an ansatz's
`get_weight_shapes`.
Returns
-------
dict
"""
return {
w: pnp.random.uniform(0, 1, size=s, requires_grad=True)
for w, s in weight_shapes.items()
}
[docs]
def register_qnode(
self, name: str, qnode: qml.QNode, weight_shapes: dict, weights=None
):
"""Wrap `qnode` for the active backend and store it under `name`.
Parameters
----------
name : str
Key under which the node's weights appear in `weights`.
qnode : qml.QNode
weight_shapes : dict
Weight name to shape, as returned by an ansatz's
`get_weight_shapes`.
weights : dict, optional
Starting weights from `init_weights`; drawn here when omitted, so
both backends start from the same point for the same seed.
"""
if weights is None:
weights = self.init_weights(weight_shapes)
if self.backend == "torch" and _check_soft_dependencies(
["torch"], severity="none"
):
import torch
init = {
w: torch.tensor(pnp.asarray(v), dtype=torch.get_default_dtype())
for w, v in weights.items()
}
torch_layer = qml.qnn.TorchLayer(qnode, weight_shapes, init_method=init)
setattr(self, name, torch_layer)
self._qnodes[name] = torch_layer
else:
setattr(self, name, qnode)
self._qnodes[name] = {"node": qnode, "weights": weights}
@abstractmethod
def _circuit(self, inputs, *flat_weights):
pass
[docs]
@abstractmethod
def forward(self, X):
"""Run the model on a batch and return its raw output."""
[docs]
def diff_methods(self, X) -> dict:
"""Differentiation method per QNode.
Parameters
----------
X : array-like
One prescaled batch; only its shape matters.
Returns
-------
dict
QNode name to method name.
"""
from pennylane.workflow import get_best_diff_method
X = qml.math.asarray(X, like=self.get_interface())
methods = {}
for name, node in self._qnodes.items():
qnode = self._qnode_of(node)
if qnode is None:
continue
prefix = f"{name}."
weights = {
k.removeprefix(prefix): v
for k, v in self.weights.items()
if k.startswith(prefix)
}
if qnode.diff_method != "best":
methods[name] = qnode.diff_method
else:
methods[name] = get_best_diff_method(qnode)(X, **weights)
return methods