Skip to content

Quantum Machine Learning

quanta.layer3.qml

quanta.layer3.qml -- Quantum Machine Learning (QML) module.

Provides quantum-enhanced classification and regression using parameterized quantum circuits (PQC) with classical optimizers.

Architecture
  • QuantumFeatureMap: Encodes classical data into quantum states
  • QuantumKernel: Quantum kernel for SVM-style classification
  • QuantumClassifier: End-to-end variational quantum classifier
Supported feature maps
  • ZZFeatureMap: Entangling feature map using ZZ interactions
  • AngleEncoding: Simple RY rotation encoding
  • AmplitudeEncoding: Log-depth amplitude encoding
Example

from quanta.layer3.qml import QuantumClassifier clf = QuantumClassifier(n_qubits=4, n_layers=2, feature_map="angle") clf.fit(X_train, y_train, epochs=50) predictions = clf.predict(X_test) print(f"Accuracy: {clf.score(X_test, y_test):.2%}")

Theory

Variational quantum classifiers use: 1. Feature map U_φ(x): Encodes input x into quantum state 2. Variational ansatz V(θ): Parameterized unitary 3. Measurement: Extracts class probabilities

Total unitary: |ψ⟩ = V(θ) · U_φ(x) · |0⟩ Loss: cross-entropy between measured probabilities and labels

QMLResult dataclass

Result of quantum machine learning training.

Attributes:

Name Type Description
accuracy float

Training accuracy.

loss_history list[float]

Loss values during training.

predictions ndarray

Last predictions made.

n_qubits int

Number of qubits used.

n_params int

Number of trainable parameters.

Source code in quanta/layer3/qml.py
@dataclass
class QMLResult:
    """Result of quantum machine learning training.

    Attributes:
        accuracy: Training accuracy.
        loss_history: Loss values during training.
        predictions: Last predictions made.
        n_qubits: Number of qubits used.
        n_params: Number of trainable parameters.
    """

    accuracy: float
    loss_history: list[float] = field(default_factory=list)
    predictions: np.ndarray = field(default_factory=lambda: np.array([]))
    n_qubits: int = 0
    n_params: int = 0

    def __repr__(self) -> str:
        return (
            f"QMLResult(accuracy={self.accuracy:.2%}, "
            f"qubits={self.n_qubits}, params={self.n_params})"
        )

QuantumClassifier

Variational Quantum Classifier (VQC).

End-to-end quantum classification pipeline
  1. Feature map encodes classical data → quantum state
  2. Variational ansatz parameterizes the classifier
  3. Measurement gives class probabilities
  4. Classical optimizer updates parameters
Example

clf = QuantumClassifier(n_qubits=4, n_layers=2) clf.fit(X_train, y_train, epochs=30) acc = clf.score(X_test, y_test) print(f"Accuracy: {acc:.2%}")

Source code in quanta/layer3/qml.py
class QuantumClassifier:
    """Variational Quantum Classifier (VQC).

    End-to-end quantum classification pipeline:
      1. Feature map encodes classical data → quantum state
      2. Variational ansatz parameterizes the classifier
      3. Measurement gives class probabilities
      4. Classical optimizer updates parameters

    Example:
        >>> clf = QuantumClassifier(n_qubits=4, n_layers=2)
        >>> clf.fit(X_train, y_train, epochs=30)
        >>> acc = clf.score(X_test, y_test)
        >>> print(f"Accuracy: {acc:.2%}")
    """

    def __init__(
        self,
        n_qubits: int = 4,
        n_layers: int = 2,
        feature_map: str = "angle",
        learning_rate: float = 0.1,
        seed: int | None = None,
    ) -> None:
        """Initialize quantum classifier.

        Args:
            n_qubits: Number of qubits.
            n_layers: Variational ansatz depth.
            feature_map: "angle", "zz", or "amplitude".
            learning_rate: Gradient descent step size.
            seed: Random seed.
        """
        self.n_qubits = n_qubits
        self.n_layers = n_layers
        self.feature_map = feature_map
        self.learning_rate = learning_rate
        self.rng = np.random.default_rng(seed)

        # 2 params per qubit per layer (RY + RZ)
        self.n_params = 2 * n_qubits * n_layers
        self.params = self.rng.uniform(
            -math.pi, math.pi, size=self.n_params,
        )
        self.loss_history: list[float] = []

    def _forward(self, x: np.ndarray) -> np.ndarray:
        """Forward pass: encode data + apply ansatz + measure.

        Returns probabilities for each computational basis state.
        """
        sim = create_simulator(self.n_qubits)

        # Feature encoding
        if self.feature_map == "zz":
            zz_feature_map(sim, x)
        elif self.feature_map == "amplitude":
            amplitude_encoding(sim, x)
        else:
            angle_encoding(sim, x)

        # Variational layers
        idx = 0
        for _ in range(self.n_layers):
            consumed = _variational_layer(sim, self.params, self.n_qubits, idx)
            idx += consumed

        # Measurement probabilities
        probs = np.abs(sim.state) ** 2
        return np.asarray(probs)

    def _predict_proba(self, x: np.ndarray) -> float:
        """Get probability of class 1 for a single sample.

        Uses parity of measurement: P(class=1) = Σ P(|i⟩) where popcount(i) is odd.
        """
        probs = self._forward(x)
        # Parity-based class probability
        p1 = sum(
            probs[i] for i in range(len(probs))
            if bin(i).count("1") % 2 == 1
        )
        return float(p1)

    def _loss(self, X: np.ndarray, y: np.ndarray) -> float:
        """Binary cross-entropy loss."""
        eps = 1e-12
        total = 0.0
        for xi, yi in zip(X, y, strict=False):
            p = np.clip(self._predict_proba(xi), eps, 1 - eps)
            total += -(yi * math.log(p) + (1 - yi) * math.log(1 - p))
        return total / len(X)

    def fit(
        self,
        X: np.ndarray,
        y: np.ndarray,
        epochs: int = 30,
    ) -> QMLResult:
        """Train the quantum classifier.

        Uses parameter-shift rule for quantum-native gradients.

        Args:
            X: Training data (n_samples, n_features).
            y: Binary labels (0 or 1).
            epochs: Number of training epochs.

        Returns:
            QMLResult with accuracy and loss history.
        """
        X = np.asarray(X, dtype=float)
        y = np.asarray(y, dtype=float)

        self.loss_history = []

        for epoch in range(epochs):
            # Compute loss
            loss = self._loss(X, y)
            self.loss_history.append(loss)

            # Parameter-shift gradients
            gradients = np.zeros(self.n_params)
            shift = math.pi / 2

            for i in range(self.n_params):
                # +shift
                self.params[i] += shift
                loss_plus = self._loss(X, y)
                self.params[i] -= shift

                # -shift
                self.params[i] -= shift
                loss_minus = self._loss(X, y)
                self.params[i] += shift

                gradients[i] = (loss_plus - loss_minus) / 2

            # Gradient descent
            self.params -= self.learning_rate * gradients

            # Early stopping
            if epoch > 5 and abs(self.loss_history[-1] - self.loss_history[-2]) < 1e-6:
                break

        # Final accuracy
        predictions = self.predict(X)
        accuracy = float(np.mean(predictions == y))

        return QMLResult(
            accuracy=accuracy,
            loss_history=self.loss_history,
            predictions=predictions,
            n_qubits=self.n_qubits,
            n_params=self.n_params,
        )

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels for samples.

        Args:
            X: Data matrix (n_samples, n_features).

        Returns:
            Array of predicted labels (0 or 1).
        """
        X = np.asarray(X, dtype=float)
        return np.array([
            1 if self._predict_proba(xi) > 0.5 else 0
            for xi in X
        ])

    def predict_proba(self, X: np.ndarray) -> np.ndarray:
        """Predict class probabilities.

        Args:
            X: Data matrix (n_samples, n_features).

        Returns:
            Array of (P(class=0), P(class=1)) per sample.
        """
        X = np.asarray(X, dtype=float)
        probas = np.array([self._predict_proba(xi) for xi in X])
        return np.column_stack([1 - probas, probas])

    def score(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute accuracy on test data.

        Args:
            X: Test data.
            y: True labels.

        Returns:
            Accuracy as a float.
        """
        predictions = self.predict(X)
        return float(np.mean(predictions == np.asarray(y)))
__init__
__init__(
    n_qubits: int = 4,
    n_layers: int = 2,
    feature_map: str = "angle",
    learning_rate: float = 0.1,
    seed: int | None = None,
) -> None

Initialize quantum classifier.

Parameters:

Name Type Description Default
n_qubits int

Number of qubits.

4
n_layers int

Variational ansatz depth.

2
feature_map str

"angle", "zz", or "amplitude".

'angle'
learning_rate float

Gradient descent step size.

0.1
seed int | None

Random seed.

None
Source code in quanta/layer3/qml.py
def __init__(
    self,
    n_qubits: int = 4,
    n_layers: int = 2,
    feature_map: str = "angle",
    learning_rate: float = 0.1,
    seed: int | None = None,
) -> None:
    """Initialize quantum classifier.

    Args:
        n_qubits: Number of qubits.
        n_layers: Variational ansatz depth.
        feature_map: "angle", "zz", or "amplitude".
        learning_rate: Gradient descent step size.
        seed: Random seed.
    """
    self.n_qubits = n_qubits
    self.n_layers = n_layers
    self.feature_map = feature_map
    self.learning_rate = learning_rate
    self.rng = np.random.default_rng(seed)

    # 2 params per qubit per layer (RY + RZ)
    self.n_params = 2 * n_qubits * n_layers
    self.params = self.rng.uniform(
        -math.pi, math.pi, size=self.n_params,
    )
    self.loss_history: list[float] = []
fit
fit(X: ndarray, y: ndarray, epochs: int = 30) -> QMLResult

Train the quantum classifier.

Uses parameter-shift rule for quantum-native gradients.

Parameters:

Name Type Description Default
X ndarray

Training data (n_samples, n_features).

required
y ndarray

Binary labels (0 or 1).

required
epochs int

Number of training epochs.

30

Returns:

Type Description
QMLResult

QMLResult with accuracy and loss history.

Source code in quanta/layer3/qml.py
def fit(
    self,
    X: np.ndarray,
    y: np.ndarray,
    epochs: int = 30,
) -> QMLResult:
    """Train the quantum classifier.

    Uses parameter-shift rule for quantum-native gradients.

    Args:
        X: Training data (n_samples, n_features).
        y: Binary labels (0 or 1).
        epochs: Number of training epochs.

    Returns:
        QMLResult with accuracy and loss history.
    """
    X = np.asarray(X, dtype=float)
    y = np.asarray(y, dtype=float)

    self.loss_history = []

    for epoch in range(epochs):
        # Compute loss
        loss = self._loss(X, y)
        self.loss_history.append(loss)

        # Parameter-shift gradients
        gradients = np.zeros(self.n_params)
        shift = math.pi / 2

        for i in range(self.n_params):
            # +shift
            self.params[i] += shift
            loss_plus = self._loss(X, y)
            self.params[i] -= shift

            # -shift
            self.params[i] -= shift
            loss_minus = self._loss(X, y)
            self.params[i] += shift

            gradients[i] = (loss_plus - loss_minus) / 2

        # Gradient descent
        self.params -= self.learning_rate * gradients

        # Early stopping
        if epoch > 5 and abs(self.loss_history[-1] - self.loss_history[-2]) < 1e-6:
            break

    # Final accuracy
    predictions = self.predict(X)
    accuracy = float(np.mean(predictions == y))

    return QMLResult(
        accuracy=accuracy,
        loss_history=self.loss_history,
        predictions=predictions,
        n_qubits=self.n_qubits,
        n_params=self.n_params,
    )
predict
predict(X: ndarray) -> np.ndarray

Predict class labels for samples.

Parameters:

Name Type Description Default
X ndarray

Data matrix (n_samples, n_features).

required

Returns:

Type Description
ndarray

Array of predicted labels (0 or 1).

Source code in quanta/layer3/qml.py
def predict(self, X: np.ndarray) -> np.ndarray:
    """Predict class labels for samples.

    Args:
        X: Data matrix (n_samples, n_features).

    Returns:
        Array of predicted labels (0 or 1).
    """
    X = np.asarray(X, dtype=float)
    return np.array([
        1 if self._predict_proba(xi) > 0.5 else 0
        for xi in X
    ])
predict_proba
predict_proba(X: ndarray) -> np.ndarray

Predict class probabilities.

Parameters:

Name Type Description Default
X ndarray

Data matrix (n_samples, n_features).

required

Returns:

Type Description
ndarray

Array of (P(class=0), P(class=1)) per sample.

Source code in quanta/layer3/qml.py
def predict_proba(self, X: np.ndarray) -> np.ndarray:
    """Predict class probabilities.

    Args:
        X: Data matrix (n_samples, n_features).

    Returns:
        Array of (P(class=0), P(class=1)) per sample.
    """
    X = np.asarray(X, dtype=float)
    probas = np.array([self._predict_proba(xi) for xi in X])
    return np.column_stack([1 - probas, probas])
score
score(X: ndarray, y: ndarray) -> float

Compute accuracy on test data.

Parameters:

Name Type Description Default
X ndarray

Test data.

required
y ndarray

True labels.

required

Returns:

Type Description
float

Accuracy as a float.

Source code in quanta/layer3/qml.py
def score(self, X: np.ndarray, y: np.ndarray) -> float:
    """Compute accuracy on test data.

    Args:
        X: Test data.
        y: True labels.

    Returns:
        Accuracy as a float.
    """
    predictions = self.predict(X)
    return float(np.mean(predictions == np.asarray(y)))

QuantumKernel

Quantum kernel for kernel-based classification.

Computes kernel matrix K(x_i, x_j) = |⟨φ(x_i)|φ(x_j)⟩|² using the ZZ feature map for quantum advantage.

Example

kernel = QuantumKernel(n_qubits=4, feature_map="zz") K = kernel.matrix(X_train)

Use with any kernel SVM
Source code in quanta/layer3/qml.py
class QuantumKernel:
    """Quantum kernel for kernel-based classification.

    Computes kernel matrix K(x_i, x_j) = |⟨φ(x_i)|φ(x_j)⟩|²
    using the ZZ feature map for quantum advantage.

    Example:
        >>> kernel = QuantumKernel(n_qubits=4, feature_map="zz")
        >>> K = kernel.matrix(X_train)
        >>> # Use with any kernel SVM
    """

    def __init__(
        self,
        n_qubits: int = 4,
        feature_map: str = "zz",
        reps: int = 2,
    ) -> None:
        """Initialize quantum kernel.

        Args:
            n_qubits: Number of qubits.
            feature_map: "zz" or "angle".
            reps: Feature map repetitions.
        """
        self.n_qubits = n_qubits
        self.feature_map = feature_map
        self.reps = reps

    def _encode(self, x: np.ndarray) -> np.ndarray:
        """Encode data into quantum state and return statevector."""
        sim = create_simulator(self.n_qubits)
        if self.feature_map == "zz":
            zz_feature_map(sim, x, reps=self.reps)
        else:
            angle_encoding(sim, x)
        return np.asarray(sim.state)

    def evaluate(self, x1: np.ndarray, x2: np.ndarray) -> float:
        """Compute kernel value K(x1, x2) = |⟨φ(x1)|φ(x2)⟩|²."""
        s1 = self._encode(x1)
        s2 = self._encode(x2)
        return float(abs(np.vdot(s1, s2)) ** 2)

    def matrix(self, X: np.ndarray) -> np.ndarray:
        """Compute kernel matrix for dataset X.

        Args:
            X: Data matrix (n_samples, n_features).

        Returns:
            (n_samples, n_samples) kernel matrix.
        """
        n = len(X)
        K = np.zeros((n, n))
        states = [self._encode(X[i]) for i in range(n)]
        for i in range(n):
            for j in range(i, n):
                K[i, j] = float(abs(np.vdot(states[i], states[j])) ** 2)
                K[j, i] = K[i, j]
        return K
__init__
__init__(
    n_qubits: int = 4,
    feature_map: str = "zz",
    reps: int = 2,
) -> None

Initialize quantum kernel.

Parameters:

Name Type Description Default
n_qubits int

Number of qubits.

4
feature_map str

"zz" or "angle".

'zz'
reps int

Feature map repetitions.

2
Source code in quanta/layer3/qml.py
def __init__(
    self,
    n_qubits: int = 4,
    feature_map: str = "zz",
    reps: int = 2,
) -> None:
    """Initialize quantum kernel.

    Args:
        n_qubits: Number of qubits.
        feature_map: "zz" or "angle".
        reps: Feature map repetitions.
    """
    self.n_qubits = n_qubits
    self.feature_map = feature_map
    self.reps = reps
evaluate
evaluate(x1: ndarray, x2: ndarray) -> float

Compute kernel value K(x1, x2) = |⟨φ(x1)|φ(x2)⟩|².

Source code in quanta/layer3/qml.py
def evaluate(self, x1: np.ndarray, x2: np.ndarray) -> float:
    """Compute kernel value K(x1, x2) = |⟨φ(x1)|φ(x2)⟩|²."""
    s1 = self._encode(x1)
    s2 = self._encode(x2)
    return float(abs(np.vdot(s1, s2)) ** 2)
matrix
matrix(X: ndarray) -> np.ndarray

Compute kernel matrix for dataset X.

Parameters:

Name Type Description Default
X ndarray

Data matrix (n_samples, n_features).

required

Returns:

Type Description
ndarray

(n_samples, n_samples) kernel matrix.

Source code in quanta/layer3/qml.py
def matrix(self, X: np.ndarray) -> np.ndarray:
    """Compute kernel matrix for dataset X.

    Args:
        X: Data matrix (n_samples, n_features).

    Returns:
        (n_samples, n_samples) kernel matrix.
    """
    n = len(X)
    K = np.zeros((n, n))
    states = [self._encode(X[i]) for i in range(n)]
    for i in range(n):
        for j in range(i, n):
            K[i, j] = float(abs(np.vdot(states[i], states[j])) ** 2)
            K[j, i] = K[i, j]
    return K

QuantumRegressor

Variational Quantum Regressor (VQR).

Continuous quantum regression pipeline
  1. Feature map encodes classical features into a quantum state.
  2. Variational ansatz parameterizes the circuit.
  3. Expectation value of Pauli-Z on readout qubit is measured.
  4. Trainable affine parameters (weight, bias) scale expectation in [-1, 1] to continuous target range: y_hat = weight * + bias.
  5. Parameter-shift gradients minimize Mean Squared Error (MSE) loss.
Example

reg = QuantumRegressor(n_qubits=4, n_layers=2) reg.fit(X_train, y_train, epochs=30) preds = reg.predict(X_test) print(f"R2 Score: {reg.score(X_test, y_test):.4f}")

Source code in quanta/layer3/qml.py
class QuantumRegressor:
    """Variational Quantum Regressor (VQR).

    Continuous quantum regression pipeline:
      1. Feature map encodes classical features into a quantum state.
      2. Variational ansatz parameterizes the circuit.
      3. Expectation value of Pauli-Z on readout qubit is measured.
      4. Trainable affine parameters (weight, bias) scale expectation in [-1, 1]
         to continuous target range: y_hat = weight * <Z> + bias.
      5. Parameter-shift gradients minimize Mean Squared Error (MSE) loss.

    Example:
        >>> reg = QuantumRegressor(n_qubits=4, n_layers=2)
        >>> reg.fit(X_train, y_train, epochs=30)
        >>> preds = reg.predict(X_test)
        >>> print(f"R2 Score: {reg.score(X_test, y_test):.4f}")
    """

    def __init__(
        self,
        n_qubits: int = 4,
        n_layers: int = 2,
        feature_map: str = "angle",
        learning_rate: float = 0.1,
        optimizer: str = "adam",
        seed: int | None = None,
    ) -> None:
        """Initialize quantum regressor.

        Args:
            n_qubits: Number of qubits.
            n_layers: Variational ansatz depth.
            feature_map: "angle", "zz", or "amplitude".
            learning_rate: Gradient descent step size.
            optimizer: "adam", "sgd", or "spsa".
            seed: Random seed.
        """
        self.n_qubits = n_qubits
        self.n_layers = n_layers
        self.feature_map = feature_map
        self.learning_rate = learning_rate
        self.optimizer = optimizer
        self.seed = seed
        self.rng = np.random.default_rng(seed)

        # 2 params per qubit per layer (RY + RZ)
        self.n_params = 2 * n_qubits * n_layers
        self.params = self.rng.uniform(
            -math.pi, math.pi, size=self.n_params,
        )
        self.weight = 1.0
        self.bias = 0.0
        self.loss_history: list[float] = []

    def _expectation_z(self, x: np.ndarray) -> float:
        """Evaluate <Z_0> expectation value for a single sample."""
        sim = create_simulator(self.n_qubits)

        if self.feature_map == "zz":
            zz_feature_map(sim, x)
        elif self.feature_map == "amplitude":
            amplitude_encoding(sim, x)
        else:
            angle_encoding(sim, x)

        idx = 0
        for _ in range(self.n_layers):
            consumed = _variational_layer(sim, self.params, self.n_qubits, idx)
            idx += consumed

        probs = np.abs(sim.state) ** 2
        # <Z> on qubit 0: (+1 for bit 0 == 0, -1 for bit 0 == 1)
        exp_z = sum(
            probs[i] if (i & 1) == 0 else -probs[i]
            for i in range(len(probs))
        )
        return float(exp_z)

    def _predict_single(self, x: np.ndarray) -> float:
        """Predict continuous target for a single sample."""
        exp_z = self._expectation_z(x)
        return float(self.weight * exp_z + self.bias)

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict continuous targets for samples.

        Args:
            X: Data matrix (n_samples, n_features).

        Returns:
            1D array of predicted continuous values.
        """
        X = np.asarray(X, dtype=float)
        return np.array([self._predict_single(xi) for xi in X], dtype=float)

    def _loss(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute Mean Squared Error (MSE) loss."""
        preds = self.predict(X)
        return float(np.mean((preds - y) ** 2))

    def score(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute R^2 (coefficient of determination) regression score.

        Args:
            X: Test data.
            y: True continuous targets.

        Returns:
            R^2 score as a float (best possible is 1.0).
        """
        X = np.asarray(X, dtype=float)
        y = np.asarray(y, dtype=float)
        preds = self.predict(X)
        ss_res = float(np.sum((y - preds) ** 2))
        ss_tot = float(np.sum((y - np.mean(y)) ** 2))
        if ss_tot < 1e-12:
            return 1.0 if ss_res < 1e-12 else 0.0
        return float(1.0 - ss_res / ss_tot)

    def fit(
        self,
        X: np.ndarray,
        y: np.ndarray,
        epochs: int = 30,
    ) -> RegressionResult:
        """Train the quantum regressor using parameter-shift rule and MSE loss.

        Args:
            X: Training features (n_samples, n_features).
            y: Continuous target values.
            epochs: Number of training epochs.

        Returns:
            RegressionResult with R^2, MSE, and loss history.
        """
        X = np.asarray(X, dtype=float)
        y = np.asarray(y, dtype=float)
        n_samples = len(X)
        if n_samples == 0:
            raise ValueError("Training dataset cannot be empty.")

        # Data-driven affine initialization
        y_mean = float(np.mean(y))
        y_std = float(np.std(y))
        self.bias = y_mean
        self.weight = y_std if y_std > 1e-4 else 1.0

        self.loss_history = []

        # Adam state
        m_params = np.zeros(self.n_params)
        v_params = np.zeros(self.n_params)
        m_w, v_w = 0.0, 0.0
        m_b, v_b = 0.0, 0.0
        beta1, beta2, eps = 0.9, 0.999, 1e-8

        shift = math.pi / 2

        for t, epoch in enumerate(range(epochs), start=1):
            # Current predictions and expectation values
            exp_vals = np.array([self._expectation_z(xi) for xi in X])
            preds = self.weight * exp_vals + self.bias
            residuals = preds - y
            loss = float(np.mean(residuals ** 2))
            self.loss_history.append(loss)

            grad_b = float(2.0 * np.mean(residuals))
            grad_w = float(2.0 * np.mean(residuals * exp_vals))

            if self.optimizer == "spsa":
                delta = self.rng.choice([-1.0, 1.0], size=self.n_params)
                ck = 0.1 / (epoch + 1) ** 0.101
                ak = self.learning_rate / (epoch + 1 + 10) ** 0.602

                self.params += ck * delta
                preds_plus = np.array([
                    self.weight * self._expectation_z(xi) + self.bias for xi in X
                ])
                loss_plus = float(np.mean((preds_plus - y) ** 2))

                self.params -= 2 * ck * delta
                preds_minus = np.array([
                    self.weight * self._expectation_z(xi) + self.bias for xi in X
                ])
                loss_minus = float(np.mean((preds_minus - y) ** 2))

                self.params += ck * delta
                grad_spsa = (loss_plus - loss_minus) / (2.0 * ck * delta)
                self.params -= ak * grad_spsa
                self.weight -= ak * grad_w
                self.bias -= ak * grad_b
            else:
                grad_params = np.zeros(self.n_params)
                for j in range(self.n_params):
                    self.params[j] += shift
                    exp_plus = np.array([self._expectation_z(xi) for xi in X])
                    self.params[j] -= 2 * shift
                    exp_minus = np.array([self._expectation_z(xi) for xi in X])
                    self.params[j] += shift

                    d_exp = (exp_plus - exp_minus) / 2.0
                    grad_params[j] = float(np.mean(2.0 * residuals * self.weight * d_exp))

                if self.optimizer == "adam":
                    m_params = beta1 * m_params + (1 - beta1) * grad_params
                    v_params = beta2 * v_params + (1 - beta2) * (grad_params ** 2)
                    m_hat = m_params / (1.0 - beta1 ** t)
                    v_hat = v_params / (1.0 - beta2 ** t)
                    self.params -= self.learning_rate * m_hat / (np.sqrt(v_hat) + eps)

                    m_w = beta1 * m_w + (1 - beta1) * grad_w
                    v_w = beta2 * v_w + (1 - beta2) * (grad_w ** 2)
                    mw_hat = m_w / (1.0 - beta1 ** t)
                    vw_hat = v_w / (1.0 - beta2 ** t)
                    self.weight -= self.learning_rate * mw_hat / (math.sqrt(vw_hat) + eps)

                    m_b = beta1 * m_b + (1 - beta1) * grad_b
                    v_b = beta2 * v_b + (1 - beta2) * (grad_b ** 2)
                    mb_hat = m_b / (1.0 - beta1 ** t)
                    vb_hat = v_b / (1.0 - beta2 ** t)
                    self.bias -= self.learning_rate * mb_hat / (math.sqrt(vb_hat) + eps)
                else:
                    self.params -= self.learning_rate * grad_params
                    self.weight -= self.learning_rate * grad_w
                    self.bias -= self.learning_rate * grad_b

            if epoch > 5 and abs(self.loss_history[-1] - self.loss_history[-2]) < 1e-7:
                break

        final_preds = self.predict(X)
        final_mse = float(np.mean((final_preds - y) ** 2))
        final_r2 = self.score(X, y)

        return RegressionResult(
            r2=final_r2,
            mse=final_mse,
            loss_history=self.loss_history,
            predictions=final_preds,
            n_qubits=self.n_qubits,
            n_params=self.n_params,
        )
__init__
__init__(
    n_qubits: int = 4,
    n_layers: int = 2,
    feature_map: str = "angle",
    learning_rate: float = 0.1,
    optimizer: str = "adam",
    seed: int | None = None,
) -> None

Initialize quantum regressor.

Parameters:

Name Type Description Default
n_qubits int

Number of qubits.

4
n_layers int

Variational ansatz depth.

2
feature_map str

"angle", "zz", or "amplitude".

'angle'
learning_rate float

Gradient descent step size.

0.1
optimizer str

"adam", "sgd", or "spsa".

'adam'
seed int | None

Random seed.

None
Source code in quanta/layer3/qml.py
def __init__(
    self,
    n_qubits: int = 4,
    n_layers: int = 2,
    feature_map: str = "angle",
    learning_rate: float = 0.1,
    optimizer: str = "adam",
    seed: int | None = None,
) -> None:
    """Initialize quantum regressor.

    Args:
        n_qubits: Number of qubits.
        n_layers: Variational ansatz depth.
        feature_map: "angle", "zz", or "amplitude".
        learning_rate: Gradient descent step size.
        optimizer: "adam", "sgd", or "spsa".
        seed: Random seed.
    """
    self.n_qubits = n_qubits
    self.n_layers = n_layers
    self.feature_map = feature_map
    self.learning_rate = learning_rate
    self.optimizer = optimizer
    self.seed = seed
    self.rng = np.random.default_rng(seed)

    # 2 params per qubit per layer (RY + RZ)
    self.n_params = 2 * n_qubits * n_layers
    self.params = self.rng.uniform(
        -math.pi, math.pi, size=self.n_params,
    )
    self.weight = 1.0
    self.bias = 0.0
    self.loss_history: list[float] = []
fit
fit(
    X: ndarray, y: ndarray, epochs: int = 30
) -> RegressionResult

Train the quantum regressor using parameter-shift rule and MSE loss.

Parameters:

Name Type Description Default
X ndarray

Training features (n_samples, n_features).

required
y ndarray

Continuous target values.

required
epochs int

Number of training epochs.

30

Returns:

Type Description
RegressionResult

RegressionResult with R^2, MSE, and loss history.

Source code in quanta/layer3/qml.py
def fit(
    self,
    X: np.ndarray,
    y: np.ndarray,
    epochs: int = 30,
) -> RegressionResult:
    """Train the quantum regressor using parameter-shift rule and MSE loss.

    Args:
        X: Training features (n_samples, n_features).
        y: Continuous target values.
        epochs: Number of training epochs.

    Returns:
        RegressionResult with R^2, MSE, and loss history.
    """
    X = np.asarray(X, dtype=float)
    y = np.asarray(y, dtype=float)
    n_samples = len(X)
    if n_samples == 0:
        raise ValueError("Training dataset cannot be empty.")

    # Data-driven affine initialization
    y_mean = float(np.mean(y))
    y_std = float(np.std(y))
    self.bias = y_mean
    self.weight = y_std if y_std > 1e-4 else 1.0

    self.loss_history = []

    # Adam state
    m_params = np.zeros(self.n_params)
    v_params = np.zeros(self.n_params)
    m_w, v_w = 0.0, 0.0
    m_b, v_b = 0.0, 0.0
    beta1, beta2, eps = 0.9, 0.999, 1e-8

    shift = math.pi / 2

    for t, epoch in enumerate(range(epochs), start=1):
        # Current predictions and expectation values
        exp_vals = np.array([self._expectation_z(xi) for xi in X])
        preds = self.weight * exp_vals + self.bias
        residuals = preds - y
        loss = float(np.mean(residuals ** 2))
        self.loss_history.append(loss)

        grad_b = float(2.0 * np.mean(residuals))
        grad_w = float(2.0 * np.mean(residuals * exp_vals))

        if self.optimizer == "spsa":
            delta = self.rng.choice([-1.0, 1.0], size=self.n_params)
            ck = 0.1 / (epoch + 1) ** 0.101
            ak = self.learning_rate / (epoch + 1 + 10) ** 0.602

            self.params += ck * delta
            preds_plus = np.array([
                self.weight * self._expectation_z(xi) + self.bias for xi in X
            ])
            loss_plus = float(np.mean((preds_plus - y) ** 2))

            self.params -= 2 * ck * delta
            preds_minus = np.array([
                self.weight * self._expectation_z(xi) + self.bias for xi in X
            ])
            loss_minus = float(np.mean((preds_minus - y) ** 2))

            self.params += ck * delta
            grad_spsa = (loss_plus - loss_minus) / (2.0 * ck * delta)
            self.params -= ak * grad_spsa
            self.weight -= ak * grad_w
            self.bias -= ak * grad_b
        else:
            grad_params = np.zeros(self.n_params)
            for j in range(self.n_params):
                self.params[j] += shift
                exp_plus = np.array([self._expectation_z(xi) for xi in X])
                self.params[j] -= 2 * shift
                exp_minus = np.array([self._expectation_z(xi) for xi in X])
                self.params[j] += shift

                d_exp = (exp_plus - exp_minus) / 2.0
                grad_params[j] = float(np.mean(2.0 * residuals * self.weight * d_exp))

            if self.optimizer == "adam":
                m_params = beta1 * m_params + (1 - beta1) * grad_params
                v_params = beta2 * v_params + (1 - beta2) * (grad_params ** 2)
                m_hat = m_params / (1.0 - beta1 ** t)
                v_hat = v_params / (1.0 - beta2 ** t)
                self.params -= self.learning_rate * m_hat / (np.sqrt(v_hat) + eps)

                m_w = beta1 * m_w + (1 - beta1) * grad_w
                v_w = beta2 * v_w + (1 - beta2) * (grad_w ** 2)
                mw_hat = m_w / (1.0 - beta1 ** t)
                vw_hat = v_w / (1.0 - beta2 ** t)
                self.weight -= self.learning_rate * mw_hat / (math.sqrt(vw_hat) + eps)

                m_b = beta1 * m_b + (1 - beta1) * grad_b
                v_b = beta2 * v_b + (1 - beta2) * (grad_b ** 2)
                mb_hat = m_b / (1.0 - beta1 ** t)
                vb_hat = v_b / (1.0 - beta2 ** t)
                self.bias -= self.learning_rate * mb_hat / (math.sqrt(vb_hat) + eps)
            else:
                self.params -= self.learning_rate * grad_params
                self.weight -= self.learning_rate * grad_w
                self.bias -= self.learning_rate * grad_b

        if epoch > 5 and abs(self.loss_history[-1] - self.loss_history[-2]) < 1e-7:
            break

    final_preds = self.predict(X)
    final_mse = float(np.mean((final_preds - y) ** 2))
    final_r2 = self.score(X, y)

    return RegressionResult(
        r2=final_r2,
        mse=final_mse,
        loss_history=self.loss_history,
        predictions=final_preds,
        n_qubits=self.n_qubits,
        n_params=self.n_params,
    )
predict
predict(X: ndarray) -> np.ndarray

Predict continuous targets for samples.

Parameters:

Name Type Description Default
X ndarray

Data matrix (n_samples, n_features).

required

Returns:

Type Description
ndarray

1D array of predicted continuous values.

Source code in quanta/layer3/qml.py
def predict(self, X: np.ndarray) -> np.ndarray:
    """Predict continuous targets for samples.

    Args:
        X: Data matrix (n_samples, n_features).

    Returns:
        1D array of predicted continuous values.
    """
    X = np.asarray(X, dtype=float)
    return np.array([self._predict_single(xi) for xi in X], dtype=float)
score
score(X: ndarray, y: ndarray) -> float

Compute R^2 (coefficient of determination) regression score.

Parameters:

Name Type Description Default
X ndarray

Test data.

required
y ndarray

True continuous targets.

required

Returns:

Type Description
float

R^2 score as a float (best possible is 1.0).

Source code in quanta/layer3/qml.py
def score(self, X: np.ndarray, y: np.ndarray) -> float:
    """Compute R^2 (coefficient of determination) regression score.

    Args:
        X: Test data.
        y: True continuous targets.

    Returns:
        R^2 score as a float (best possible is 1.0).
    """
    X = np.asarray(X, dtype=float)
    y = np.asarray(y, dtype=float)
    preds = self.predict(X)
    ss_res = float(np.sum((y - preds) ** 2))
    ss_tot = float(np.sum((y - np.mean(y)) ** 2))
    if ss_tot < 1e-12:
        return 1.0 if ss_res < 1e-12 else 0.0
    return float(1.0 - ss_res / ss_tot)

RegressionResult dataclass

Result of quantum machine learning regression training.

Attributes:

Name Type Description
r2 float

Coefficient of determination R^2 score.

mse float

Mean squared error on training data.

loss_history list[float]

Loss values during training.

predictions ndarray

Last predictions made.

n_qubits int

Number of qubits used.

n_params int

Number of trainable parameters.

Source code in quanta/layer3/qml.py
@dataclass
class RegressionResult:
    """Result of quantum machine learning regression training.

    Attributes:
        r2: Coefficient of determination R^2 score.
        mse: Mean squared error on training data.
        loss_history: Loss values during training.
        predictions: Last predictions made.
        n_qubits: Number of qubits used.
        n_params: Number of trainable parameters.
    """

    r2: float
    mse: float
    loss_history: list[float] = field(default_factory=list)
    predictions: np.ndarray = field(default_factory=lambda: np.array([]))
    n_qubits: int = 0
    n_params: int = 0

    def __repr__(self) -> str:
        return (
            f"RegressionResult(r2={self.r2:.4f}, mse={self.mse:.4f}, "
            f"qubits={self.n_qubits}, params={self.n_params})"
        )

amplitude_encoding

amplitude_encoding(
    sim: SimulatorBackend, x: ndarray
) -> None

Amplitude encoding: embed data directly into state amplitudes.

Encodes 2^n features into n qubits. Input is normalized to unit vector.

Parameters:

Name Type Description Default
sim SimulatorBackend

Simulator to apply encoding.

required
x ndarray

Feature vector (length must be ≤ 2^n_qubits).

required
Source code in quanta/layer3/qml.py
def amplitude_encoding(
    sim: SimulatorBackend,
    x: np.ndarray,
) -> None:
    """Amplitude encoding: embed data directly into state amplitudes.

    Encodes 2^n features into n qubits.
    Input is normalized to unit vector.

    Args:
        sim: Simulator to apply encoding.
        x: Feature vector (length must be ≤ 2^n_qubits).
    """
    n_qubits = sim.num_qubits
    dim = 2 ** n_qubits

    # Pad and normalize
    padded = np.zeros(dim)
    padded[:len(x)] = x
    norm = np.linalg.norm(padded)
    if norm > 1e-12:
        padded = padded / norm

    # Direct state preparation via public setter
    sim.state = padded.astype(complex)

angle_encoding

angle_encoding(
    sim: SimulatorBackend,
    x: ndarray,
    qubits: tuple[int, ...] | None = None,
) -> None

Encode features via RY rotations.

Each feature x_i → RY(π · x_i) on qubit i. Simple and effective for small feature spaces.

Parameters:

Name Type Description Default
sim SimulatorBackend

Simulator to apply encoding.

required
x ndarray

Feature vector (values in [0, 1]).

required
qubits tuple[int, ...] | None

Which qubits to use (default: first len(x)).

None
Source code in quanta/layer3/qml.py
def angle_encoding(
    sim: SimulatorBackend,
    x: np.ndarray,
    qubits: tuple[int, ...] | None = None,
) -> None:
    """Encode features via RY rotations.

    Each feature x_i → RY(π · x_i) on qubit i.
    Simple and effective for small feature spaces.

    Args:
        sim: Simulator to apply encoding.
        x: Feature vector (values in [0, 1]).
        qubits: Which qubits to use (default: first len(x)).
    """
    if qubits is None:
        qubits = tuple(range(len(x)))
    for i, q in enumerate(qubits):
        if i < len(x):
            sim.apply("RY", (q,), (math.pi * float(x[i]),))

zz_feature_map

zz_feature_map(
    sim: SimulatorBackend,
    x: ndarray,
    qubits: tuple[int, ...] | None = None,
    reps: int = 2,
) -> None

ZZ entangling feature map.

Encodes features with entanglement
  1. H on all qubits
  2. RZ(x_i) on each qubit
  3. RZZ(x_i * x_j) on adjacent pairs

Repeated reps times for expressiveness.

Parameters:

Name Type Description Default
sim SimulatorBackend

Simulator to apply encoding.

required
x ndarray

Feature vector.

required
qubits tuple[int, ...] | None

Which qubits to use.

None
reps int

Number of repetitions.

2
Source code in quanta/layer3/qml.py
def zz_feature_map(
    sim: SimulatorBackend,
    x: np.ndarray,
    qubits: tuple[int, ...] | None = None,
    reps: int = 2,
) -> None:
    """ZZ entangling feature map.

    Encodes features with entanglement:
      1. H on all qubits
      2. RZ(x_i) on each qubit
      3. RZZ(x_i * x_j) on adjacent pairs
    Repeated `reps` times for expressiveness.

    Args:
        sim: Simulator to apply encoding.
        x: Feature vector.
        qubits: Which qubits to use.
        reps: Number of repetitions.
    """
    n = len(x)
    if qubits is None:
        qubits = tuple(range(n))

    for _ in range(reps):
        # Hadamard layer
        for q in qubits[:n]:
            sim.apply("H", (q,))

        # Phase encoding
        for i, q in enumerate(qubits[:n]):
            sim.apply("RZ", (q,), (2.0 * float(x[i]),))

        # Entanglement encoding (ZZ interaction)
        for i in range(n - 1):
            q1, q2 = qubits[i], qubits[i + 1]
            phi = 2.0 * float(x[i]) * float(x[i + 1])
            sim.apply("CX", (q1, q2))
            sim.apply("RZ", (q2,), (phi,))
            sim.apply("CX", (q1, q2))