Layers#

A layer is a reusable block that models are assembled from. It holds its own weights and runs a batch through forward, and it can be classical or quantum. Nothing about a layer is specific to hybrid networks. BaseVQC is the embedding, ansatz and measurement block that VQCClassifier and VQCRegressor share, and QuantumLayer is that same block read as <Z> on every wire.

There are two ways to use a layer. A model class can build layers in its __init__ and run them in forward, which is how DressedQuantumClassifier is written. Or you can compose layers yourself in a pipeline and train them together with fit_mode="joint", where the loss after the last layer reaches every layer before it.

import pyqit
from pyqit.core import QuantumPipeline
from pyqit.models.layers import DenseClassifier, DenseLayer, QuantumLayer

hybrid = QuantumPipeline(
    [
        ("pre", DenseLayer(n_features=8, n_out=4, activation="tanh")),
        ("circuit", QuantumLayer(n_qubits=4, n_layers=2)),
        ("head", DenseClassifier(n_features=4)),
    ],
    fit_mode="joint",
)
history = pyqit.Trainer(max_epochs=20).fit(hybrid, dm)

Available layers#

Each layer’s page gives its inputs, its output shape and its weight names.

DenseLayer

Classical dense stage: activation(X @ weight.T + bias).

QuantumLayer

Quantum stage: an embedding, an ansatz, and <Z> read on every wire.

DenseClassifier

Classical head stage: one dense layer, then sigmoid or softmax.

Feature layers and heads#

A feature layer returns a (n_samples, width) array for the next layer to read. Its output is not a prediction, so it cannot be fit alone against labels. A head comes last. It returns predictions and has predict_step, which is what Trainer.predict calls for hard labels.

The pipeline prescales the input of every quantum layer for that layer’s embedding, so the layer in front of it needs no scaling of its own. Narrower input is zero-padded to the layer’s width and wider input raises.

What every layer provides#

forward(X, **custom_weights) runs the layer. weights is a flat dict keyed "<name>.<weight>", the same on both backends, and update_weights writes it back. Inside a pipeline the stage name goes in front of each key.

DenseLayer(n_features=8, n_out=4).weights
# {"dense.weight": ..., "dense.bias": ...}
hybrid.weights
# {"pre.dense.weight": ..., "circuit.main_circuit.weights": ..., ...}

A layer reads the backend once in its own __init__, as models do. Call set_backend() before building it.

Adding a layer#

Subclass BaseModel for a classical layer and register its weights with register_dense. Subclass BaseQuantumModel for a quantum one and use register_qnode. Set the object_type tag to "layer", and run the registered entries with execute_qnode inside forward(X, **custom_weights), passing custom_weights through. The training loops rely on that argument to route weights. A head also mixes in ClassifierMixin or RegressorMixin for predict_step.

A quantum layer built from an embedding and an ansatz can subclass BaseVQC and skip the circuit. It then implements _resolve_readout, which picks the measurement, and forward, which maps the raw output.

Implement get_test_params(). The layer suite finds the class by its tag and trains it in front of a DenseClassifier. It reads the input width from n_features or n_qubits and the output width from n_out or n_qubits, so a feature layer must expose those attributes. Heads are excluded from that test by name in test_all_layers.py. Then add the class name to the list above. See the contributing guide.

You subclass this and never instantiate it directly.

BaseVQC

An embedding, an ansatz, a measurement: the circuit the VQC models share.