This is an automated email from the ASF dual-hosted git repository.

hcr pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git


The following commit(s) were added to refs/heads/main by this push:
     new d7cf2c3ee Align Iris amplitude encoding benchmark between PennyLane 
and QDP (#1088)
d7cf2c3ee is described below

commit d7cf2c3ee7b2f15d592e5969d99555f5d4893d9f
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Tue Feb 24 19:57:46 2026 +0800

    Align Iris amplitude encoding benchmark between PennyLane and QDP (#1088)
---
 .../benchmark/encoding_benchmarks/README.md        |  77 +++
 .../pennylane_baseline/iris_amplitude.py           | 349 +++++++++++++
 .../qdp_pipeline/iris_amplitude.py                 | 543 +++++++++++++++++++++
 3 files changed, 969 insertions(+)

diff --git a/qdp/qdp-python/benchmark/encoding_benchmarks/README.md 
b/qdp/qdp-python/benchmark/encoding_benchmarks/README.md
new file mode 100644
index 000000000..97c70ab36
--- /dev/null
+++ b/qdp/qdp-python/benchmark/encoding_benchmarks/README.md
@@ -0,0 +1,77 @@
+# Encoding benchmarks
+
+This directory is used to **compare a pure PennyLane baseline with a QDP 
pipeline on the full training loop**.
+Both scripts use the same dataset and the same variational model; **only the 
encoding step is different**.
+
+- **`pennylane_baseline/`**: pure PennyLane (sklearn / official Iris file → 
encoding → variational classifier).
+- **`qdp_pipeline/`**: same data and model, but encoding is done via the QDP 
`QuantumDataLoader` (amplitude).
+
+Run all commands from the `qdp-python` directory:
+
+```bash
+cd qdp/qdp-python
+```
+
+## Environment (one-time setup)
+
+```bash
+uv sync --group benchmark        # install PennyLane, torch(+CUDA), 
scikit-learn, etc.
+uv run maturin develop           # build QDP Python extension (qumat_qdp)
+```
+
+If your CUDA driver is not 12.6, adjust the PyTorch index URL in 
`pyproject.toml` according to the comments there.
+
+## Iris amplitude baseline (pure PennyLane)
+
+Pipeline: 2-class Iris (sklearn or official file) → L2 normalize → 
`get_angles` → variational classifier.
+
+```bash
+uv run python 
benchmark/encoding_benchmarks/pennylane_baseline/iris_amplitude.py
+```
+
+Common flags (only the key ones):
+
+- `--iters`: optimizer steps (default: 1500)
+- `--layers`: number of variational layers (default: 10)
+- `--lr`: learning rate (default: 0.08)
+- `--optimizer`: `adam` or `nesterov` (default: `adam`)
+- `--trials`: number of restarts; best test accuracy reported (default: 20)
+- `--data-file`: use the official Iris file instead of sklearn (2 features, 
75% train)
+
+Example (official file + Nesterov + short run):
+
+```bash
+uv run python 
benchmark/encoding_benchmarks/pennylane_baseline/iris_amplitude.py \
+  --data-file 
benchmark/encoding_benchmarks/pennylane_baseline/data/iris_classes1and2_scaled.txt
 \
+  --optimizer nesterov --lr 0.01 --layers 6 --trials 3 --iters 80 --early-stop 0
+```
+
+## Iris amplitude (QDP pipeline)
+
+Pipeline is identical to the baseline except for encoding:
+4-D vectors → QDP `QuantumDataLoader` (amplitude) → `StatePrep(state_vector)` 
→ same variational classifier.
+
+```bash
+uv run python benchmark/encoding_benchmarks/qdp_pipeline/iris_amplitude.py
+```
+
+The CLI mirrors the baseline, plus:
+
+- **QDP-specific flags**
+  - `--device-id`: QDP device id (default: 0)
+  - `--data-dir`: directory for temporary `.npy` files (default: system temp 
directory)
+
+Example (same settings as the baseline example, but with QDP encoding):
+
+```bash
+uv run python benchmark/encoding_benchmarks/qdp_pipeline/iris_amplitude.py \
+  --data-file 
benchmark/encoding_benchmarks/pennylane_baseline/data/iris_classes1and2_scaled.txt
 \
+  --optimizer nesterov --lr 0.01 --layers 6 --trials 3 --iters 80 --early-stop 0
+```
+
+To see the full list of options and defaults, append `--help`:
+
+```bash
+uv run python 
benchmark/encoding_benchmarks/pennylane_baseline/iris_amplitude.py --help
+uv run python benchmark/encoding_benchmarks/qdp_pipeline/iris_amplitude.py 
--help
+```
diff --git 
a/qdp/qdp-python/benchmark/encoding_benchmarks/pennylane_baseline/iris_amplitude.py
 
b/qdp/qdp-python/benchmark/encoding_benchmarks/pennylane_baseline/iris_amplitude.py
new file mode 100644
index 000000000..ee6b8f4d6
--- /dev/null
+++ 
b/qdp/qdp-python/benchmark/encoding_benchmarks/pennylane_baseline/iris_amplitude.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+PennyLane baseline: Iris (2-class), amplitude encoding, variational classifier.
+
+Aligned with: https://pennylane.ai/qml/demos/tutorial_variational_classifier
+
+Data sources (default: sklearn, not the official file):
+  - Default: sklearn.datasets.load_iris, classes 0 & 1, 4 features → scale → 
L2 norm → get_angles.
+  - Official file: pass --data-file <path>. File format: cols [f0, f1, f2, f3, 
label]; we use first 2 cols,
+    pad to 4, L2 norm. Bundled path: data/iris_classes1and2_scaled.txt (from 
XanaduAI/qml,
+    
https://raw.githubusercontent.com/XanaduAI/qml/master/_static/demonstration_assets/variational_classifier/data/iris_classes1and2_scaled.txt).
+  - Total samples: 100 (2-class Iris). Full Iris has 150 (3 classes).
+
+Pipeline: state prep (Möttönen angles) → Rot layers + CNOT → expval(PauliZ(0)) 
+ bias; square loss; Adam or Nesterov.
+"""
+
+from __future__ import annotations
+
+# --- Imports ---
+
+import argparse
+import time
+from typing import Any
+
+import numpy as np
+
+try:
+    import pennylane as qml
+    from pennylane import numpy as pnp
+    from pennylane.optimize import NesterovMomentumOptimizer, AdamOptimizer
+except ImportError as e:
+    raise SystemExit(
+        "PennyLane is required. Install with: uv sync --group benchmark"
+    ) from e
+
+try:
+    from sklearn.datasets import load_iris
+    from sklearn.preprocessing import StandardScaler
+except ImportError as e:
+    raise SystemExit(
+        "scikit-learn is required. Install with: uv sync --group benchmark"
+    ) from e
+
+
+NUM_QUBITS = 2
+
+
+# --- Encoding: 4-D vector → 5 angles (Möttönen et al.) ---
+def get_angles(x: np.ndarray) -> np.ndarray:
+    """State preparation angles from 4-D normalized vector (Möttönen et 
al.)."""
+    x = np.asarray(x, dtype=np.float64)
+    eps = 1e-12
+    beta0 = 2 * np.arcsin(np.sqrt(x[1] ** 2) / np.sqrt(x[0] ** 2 + x[1] ** 2 + 
eps))
+    beta1 = 2 * np.arcsin(np.sqrt(x[3] ** 2) / np.sqrt(x[2] ** 2 + x[3] ** 2 + 
eps))
+    beta2 = 2 * np.arcsin(np.linalg.norm(x[2:]) / (np.linalg.norm(x) + eps))
+    return np.array([beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2])
+
+
+# --- Circuit: amplitude state prep (RY/CNOT) + variational layer (Rot + CNOT) 
---
+def state_preparation(a, wires=(0, 1)):
+    """Amplitude encoding via rotation angles (tutorial / Möttönen et al.)."""
+    qml.RY(a[0], wires=wires[0])
+    qml.CNOT(wires=[wires[0], wires[1]])
+    qml.RY(a[1], wires=wires[1])
+    qml.CNOT(wires=[wires[0], wires[1]])
+    qml.RY(a[2], wires=wires[1])
+    qml.PauliX(wires=wires[0])
+    qml.CNOT(wires=[wires[0], wires[1]])
+    qml.RY(a[3], wires=wires[1])
+    qml.CNOT(wires=[wires[0], wires[1]])
+    qml.RY(a[4], wires=wires[1])
+    qml.PauliX(wires=wires[0])
+
+
+def layer(layer_weights, wires=(0, 1)):
+    """Rot on each wire + CNOT (tutorial Iris section)."""
+    for i, w in enumerate(wires):
+        qml.Rot(*layer_weights[i], wires=w)
+    qml.CNOT(wires=list(wires))
+
+
+# --- Data: official file (2 cols → pad 4 → L2 norm → get_angles). Source: 
XanaduAI/qml, see docstring. ---
+def load_iris_from_file(path: str) -> tuple[np.ndarray, np.ndarray]:
+    """
+    Load official-style Iris 2-class data: file with columns [f0, f1, f2, f3, 
label].
+    Uses first 2 columns only (as in tutorial), pad to 4, L2 normalize, 
get_angles.
+    Returns (features, Y) with features shape (n, 5), Y in {-1, 1}.
+    """
+    data = np.loadtxt(path, dtype=np.float64)
+    X = data[:, 0:2]  # first 2 features only (tutorial convention)
+    Y = data[:, -1]  # labels already ±1
+    padding = np.ones((len(X), 2)) * 0.1
+    X_pad = np.c_[X, padding]
+    norm = np.sqrt(np.sum(X_pad**2, axis=-1)) + 1e-12
+    X_norm = (X_pad.T / norm).T
+    features = np.array([get_angles(x) for x in X_norm])
+    return features, Y
+
+
+# --- Data: sklearn Iris classes 0 & 1, 4 features → scale → L2 norm → 
get_angles ---
+def load_iris_binary(seed: int = 42) -> tuple[np.ndarray, np.ndarray]:
+    """
+    Iris classes 0 and 1 only. Uses all 4 features: scale → L2 norm → 
get_angles.
+    Returns (features, Y) with features shape (n, 5), Y in {-1, 1}.
+    Data source: sklearn.datasets.load_iris.
+    """
+    X_raw, y = load_iris(return_X_y=True)
+    mask = (y == 0) | (y == 1)
+    X = np.asarray(X_raw[mask], dtype=np.float64)
+    y = y[mask]
+    X = StandardScaler().fit_transform(X)
+    norm = np.sqrt(np.sum(X**2, axis=-1)) + 1e-12
+    X_norm = (X.T / norm).T
+    features = np.array([get_angles(x) for x in X_norm])
+    Y = np.array(y, dtype=np.float64) * 2 - 1  # {0,1} → {-1,1}
+    return features, Y
+
+
+# --- Training: build circuit, split data, optimize, evaluate ---
+def run_training(
+    features: np.ndarray,
+    Y: np.ndarray,
+    *,
+    num_layers: int,
+    iterations: int,
+    batch_size: int,
+    lr: float,
+    seed: int,
+    test_size: float = 0.25,
+    optimizer: str = "adam",
+    early_stop_target: float | None = 0.9,
+) -> dict[str, Any]:
+    """Train classifier: circuit + bias, square loss, batched. Optional early 
stop when test acc ≥ target."""
+    dev = qml.device("default.qubit", wires=NUM_QUBITS)
+
+    # Circuit: state_prep(angles) → layers of Rot+CNOT → expval(PauliZ(0))
+    @qml.qnode(dev, interface="autograd", diff_method="backprop")
+    def circuit(weights, angles):
+        state_preparation(angles, wires=(0, 1))
+        for lw in weights:
+            layer(lw, wires=(0, 1))
+        return qml.expval(qml.PauliZ(0))
+
+    def model(weights, bias, angles):
+        return circuit(weights, angles) + bias
+
+    def cost(weights, bias, X_batch, Y_batch):
+        preds = model(weights, bias, X_batch.T)
+        return pnp.mean((Y_batch - preds) ** 2)
+
+    # Train/val split (seed-driven)
+    n = len(Y)
+    np.random.seed(seed)
+    try:
+        pnp.random.seed(seed)
+    except Exception:
+        pass
+    rng = np.random.default_rng(seed)
+    idx = rng.permutation(n)
+    n_train = int(n * (1 - test_size))
+    feats_train = pnp.array(features[idx[:n_train]])
+    Y_train = pnp.array(Y[idx[:n_train]])
+    feats_test = features[idx[n_train:]]
+    Y_test = Y[idx[n_train:]]
+
+    # Weights and optimizer
+    weights_init = 0.01 * pnp.random.randn(
+        num_layers, NUM_QUBITS, 3, requires_grad=True
+    )
+    bias_init = pnp.array(0.0, requires_grad=True)
+    if optimizer == "adam":
+        opt = AdamOptimizer(lr)
+    else:
+        opt = NesterovMomentumOptimizer(lr)
+
+    # Compile (first run)
+    t0 = time.perf_counter()
+    _ = circuit(weights_init, feats_train[0])
+    _ = cost(weights_init, bias_init, feats_train[:1], Y_train[:1])
+    compile_sec = time.perf_counter() - t0
+
+    # Optimize (batched steps; optional early stop every 100 steps)
+    t0 = time.perf_counter()
+    weights, bias = weights_init, bias_init
+    steps_done = 0
+    for step in range(iterations):
+        batch_idx = rng.integers(0, n_train, size=(batch_size,))
+        fb = feats_train[batch_idx]
+        yb = Y_train[batch_idx]
+        out = opt.step(cost, weights, bias, fb, yb)
+        weights, bias = out[0], out[1]
+        steps_done += 1
+        if early_stop_target is not None and (step + 1) % 100 == 0:
+            pred_test_now = np.sign(
+                np.array(model(weights, bias, pnp.array(feats_test).T))
+            ).flatten()
+            test_acc_now = float(
+                np.mean(np.abs(pred_test_now - np.array(Y_test)) < 1e-5)
+            )
+            if test_acc_now >= early_stop_target:
+                break
+    train_sec = time.perf_counter() - t0
+
+    # Metrics (train/test accuracy)
+    pred_train = np.sign(np.array(model(weights, bias, 
feats_train.T))).flatten()
+    pred_test = np.sign(
+        np.array(model(weights, bias, pnp.array(feats_test).T))
+    ).flatten()
+    Y_train_np = np.array(Y_train)
+    Y_test_np = np.array(Y_test)
+    train_acc = float(np.mean(np.abs(pred_train - Y_train_np) < 1e-5))
+    test_acc = float(np.mean(np.abs(pred_test - Y_test_np) < 1e-5))
+
+    return {
+        "compile_time_sec": compile_sec,
+        "train_time_sec": train_sec,
+        "train_accuracy": train_acc,
+        "test_accuracy": test_acc,
+        "n_train": n_train,
+        "n_test": len(Y_test),
+        "epochs": steps_done,
+        "samples_per_sec": (steps_done * batch_size) / train_sec
+        if train_sec > 0
+        else 0.0,
+    }
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(
+        description="PennyLane Iris amplitude encoding baseline (2-class)"
+    )
+    parser.add_argument(
+        "--iters",
+        type=int,
+        default=1500,
+        help="Max optimizer steps per run (default: 1500)",
+    )
+    parser.add_argument(
+        "--batch-size", type=int, default=5, help="Batch size (default: 5)"
+    )
+    parser.add_argument(
+        "--layers", type=int, default=10, help="Variational layers (default: 
10)"
+    )
+    parser.add_argument(
+        "--lr", type=float, default=0.08, help="Learning rate (default: 0.08 
for Adam)"
+    )
+    parser.add_argument(
+        "--test-size",
+        type=float,
+        default=0.1,
+        help="Test fraction (default: 0.1); ignored if --data-file set",
+    )
+    parser.add_argument("--seed", type=int, default=0, help="Random seed 
(default: 0)")
+    parser.add_argument(
+        "--optimizer",
+        type=str,
+        default="adam",
+        choices=("adam", "nesterov"),
+        help="Optimizer (default: adam)",
+    )
+    parser.add_argument(
+        "--trials",
+        type=int,
+        default=20,
+        help="Number of restarts; best test acc reported (default: 20)",
+    )
+    parser.add_argument(
+        "--early-stop",
+        type=float,
+        default=0.9,
+        help="Stop run when test acc >= this (default: 0.9; 0 = off)",
+    )
+    parser.add_argument(
+        "--data-file",
+        type=str,
+        default=None,
+        help="Path to official Iris file (cols: f0,f1,f2,f3,label). If set, 
use first 2 cols + 75%% train.",
+    )
+    args = parser.parse_args()
+
+    # Data source: official file (when --data-file set) or sklearn Iris 
(default)
+    if args.data_file:
+        features, Y = load_iris_from_file(args.data_file)
+        test_size = 0.25  # tutorial uses 75% train
+        data_src = f"official file (2 features): {args.data_file}"
+    else:
+        features, Y = load_iris_binary(seed=args.seed)
+        test_size = args.test_size
+        data_src = "sklearn load_iris, classes 0 & 1, 4 features"
+    n = len(Y)
+    print("Iris amplitude baseline (PennyLane) — 2-class variational 
classifier")
+    print(
+        f"  Data: {data_src} → L2 norm → get_angles  (n={n}; 2-class Iris = 
100 samples)"
+    )
+    print(
+        f"  Iters: {args.iters}, batch_size: {args.batch_size}, layers: 
{args.layers}, lr: {args.lr}, optimizer: {args.optimizer}"
+    )
+
+    results: list[dict[str, Any]] = []
+    for t in range(args.trials):
+        r = run_training(
+            features,
+            Y,
+            num_layers=args.layers,
+            iterations=args.iters,
+            batch_size=args.batch_size,
+            lr=args.lr,
+            seed=args.seed + t,
+            test_size=test_size,
+            optimizer=args.optimizer,
+            early_stop_target=args.early_stop if args.early_stop > 0 else None,
+        )
+        results.append(r)
+        print(f"\n  Trial {t + 1}:")
+        print(f"    Compile:   {r['compile_time_sec']:.4f} s")
+        print(f"    Train:     {r['train_time_sec']:.4f} s")
+        print(f"    Train acc: {r['train_accuracy']:.4f}  (n={r['n_train']})")
+        print(f"    Test acc:  {r['test_accuracy']:.4f}  (n={r['n_test']})")
+        print(f"    Throughput: {r['samples_per_sec']:.1f} samples/s")
+
+    if args.trials > 1:
+        test_accs = sorted(r["test_accuracy"] for r in results)
+        best = test_accs[-1]
+        mid = args.trials // 2
+        print(
+            f"\n  Best test accuracy:  {best:.4f}  (median: 
{test_accs[mid]:.4f}, min: {test_accs[0]:.4f}, max: {test_accs[-1]:.4f})"
+        )
+        if best >= 0.9:
+            print("  → Target ≥0.9 achieved.")
+
+
+if __name__ == "__main__":
+    main()
diff --git 
a/qdp/qdp-python/benchmark/encoding_benchmarks/qdp_pipeline/iris_amplitude.py 
b/qdp/qdp-python/benchmark/encoding_benchmarks/qdp_pipeline/iris_amplitude.py
new file mode 100644
index 000000000..f1e2bf8bf
--- /dev/null
+++ 
b/qdp/qdp-python/benchmark/encoding_benchmarks/qdp_pipeline/iris_amplitude.py
@@ -0,0 +1,543 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+QDP pipeline: Iris (2-class), same data and training as baseline; only 
encoding differs.
+
+Aligned with baseline: 
https://pennylane.ai/qml/demos/tutorial_variational_classifier
+
+Data sources (default: sklearn, not the official file):
+  - Default: sklearn.datasets.load_iris, classes 0 & 1, 4 features → scale → 
L2 norm → 4-D vectors for QDP.
+  - Official file: pass --data-file <path>. File format: cols [f0, f1, f2, f3, 
label]; we use first 2 cols,
+    pad to 4, L2 norm. Bundled path: 
../pennylane_baseline/data/iris_classes1and2_scaled.txt (from XanaduAI/qml,
+    see baseline docstring URL).
+  - Total samples: 100 (2-class Iris). Full Iris has 150 (3 classes).
+
+Only difference from baseline: encoding. Here we use QDP (QuantumDataLoader + 
amplitude) → StatePrep(encoded);
+baseline uses get_angles → state_preparation(angles). Rest: same circuit (Rot 
+ CNOT), loss, optimizer, CLI.
+"""
+
+from __future__ import annotations
+
+# --- Imports ---
+
+import argparse
+import os
+import tempfile
+import time
+from typing import Any
+
+import numpy as np
+
+try:
+    import pennylane as qml
+    from pennylane import numpy as pnp
+    from pennylane.optimize import NesterovMomentumOptimizer, AdamOptimizer
+except ImportError as e:
+    raise SystemExit(
+        "PennyLane is required. Install with: uv sync --group benchmark"
+    ) from e
+
+try:
+    from sklearn.datasets import load_iris
+    from sklearn.preprocessing import StandardScaler
+except ImportError as e:
+    raise SystemExit(
+        "scikit-learn is required. Install with: uv sync --group benchmark"
+    ) from e
+
+from qumat_qdp import QuantumDataLoader
+import torch
+
+
+NUM_QUBITS = 2
+STATE_DIM = 2**NUM_QUBITS  # 4
+
+
+# --- Circuit: variational layer (Rot + CNOT); state prep is 
StatePrep(encoded) in training ---
+def layer(layer_weights, wires=(0, 1)):
+    """Rot on each wire + CNOT (tutorial Iris section)."""
+    for i, w in enumerate(wires):
+        qml.Rot(*layer_weights[i], wires=w)
+    qml.CNOT(wires=list(wires))
+
+
+# --- Data: official file (2 cols → pad 4 → L2 norm). Source: XanaduAI/qml, 
see docstring. ---
+def load_iris_4d_from_file(path: str) -> tuple[np.ndarray, np.ndarray]:
+    """
+    Load official-style Iris 2-class data; return 4-D normalized vectors for 
QDP.
+    File cols [f0, f1, f2, f3, label]. We use first 2 cols only, pad to 4, L2 
norm.
+    Returns (X_norm, Y) with X_norm (n, 4), Y in {-1, 1}.
+    """
+    data = np.loadtxt(path, dtype=np.float64)
+    X = data[:, 0:2]
+    Y = np.asarray(data[:, -1], dtype=np.float64)
+    # Official Xanadu file uses ±1; if file has 0/1, convert to ±1
+    if np.unique(Y).size <= 2 and 0 in np.unique(Y):
+        Y = Y * 2 - 1
+    padding = np.ones((len(X), 2)) * 0.1
+    X_pad = np.c_[X, padding]
+    norm = np.sqrt(np.sum(X_pad**2, axis=-1)) + 1e-12
+    X_norm = (X_pad.T / norm).T
+    return X_norm, Y
+
+
+# --- Data: sklearn Iris classes 0 & 1, 4 features → scale → L2 norm. Source: 
sklearn.datasets.load_iris. ---
+def load_iris_binary_4d(seed: int = 42) -> tuple[np.ndarray, np.ndarray]:
+    """
+    Iris classes 0 and 1 only. Uses all 4 features: scale → L2 norm.
+    Returns (X_norm, Y) with X_norm (n, 4), Y in {-1, 1}.
+    """
+    X_raw, y = load_iris(return_X_y=True)
+    mask = (y == 0) | (y == 1)
+    X = np.asarray(X_raw[mask], dtype=np.float64)
+    y = y[mask]
+    X = StandardScaler().fit_transform(X)
+    norm = np.sqrt(np.sum(X**2, axis=-1)) + 1e-12
+    X_norm = (X.T / norm).T
+    Y = np.array(y, dtype=np.float64) * 2 - 1
+    return X_norm, Y
+
+
+# --- Encoding: QDP (QuantumDataLoader + amplitude); 4-D → GPU tensor ---
+def encode_via_qdp(
+    X_norm: np.ndarray,
+    batch_size: int,
+    device_id: int = 0,
+    data_dir: str | None = None,
+    filename: str = "iris_4d.npy",
+) -> torch.Tensor:
+    """QDP: save 4-D vectors to .npy, run QuantumDataLoader (amplitude), 
return encoded (n, 4) on GPU."""
+    n, dim = X_norm.shape
+    if dim != STATE_DIM:
+        raise ValueError(
+            f"X_norm must have {STATE_DIM} features for 2 qubits, got {dim}"
+        )
+    if data_dir is None:
+        data_dir = tempfile.gettempdir()
+    os.makedirs(data_dir, exist_ok=True)
+    path = os.path.join(data_dir, filename)
+    np.save(path, X_norm.astype(np.float64))
+    total_batches = (n + batch_size - 1) // batch_size
+    loader = (
+        QuantumDataLoader(device_id=device_id)
+        .qubits(NUM_QUBITS)
+        .encoding("amplitude")
+        .batches(total_batches, size=batch_size)
+        .source_file(path)
+    )
+    batches = []
+    for qt in loader:
+        t = torch.from_dlpack(qt)
+        batches.append(t)  # keep on GPU
+    return torch.cat(batches, dim=0)[:n].clone()
+
+
+# --- Training: StatePrep(encoded) + Rot layers, square loss, optional early 
stop ---
+def run_training(
+    encoded_train: torch.Tensor | np.ndarray,
+    encoded_test: torch.Tensor | np.ndarray,
+    Y_train: np.ndarray,
+    Y_test: np.ndarray,
+    *,
+    num_layers: int,
+    iterations: int,
+    batch_size: int,
+    lr: float,
+    seed: int,
+    early_stop_target: float | None = None,
+    optimizer: str = "nesterov",
+) -> dict[str, Any]:
+    """Train variational classifier: StatePrep(encoded) + Rot layers + bias, 
square loss, batched.
+    If encoded_* are on GPU and lightning.gpu is available, training runs on 
GPU; otherwise on CPU.
+    When early_stop_target is set, evaluate test acc every 100 steps and stop 
when >= target."""
+    n_train = len(Y_train)
+    np.random.seed(seed)
+    rng = np.random.default_rng(seed)
+
+    # Prefer Lightning GPU when encoded data is on GPU
+    use_gpu = isinstance(encoded_train, torch.Tensor) and encoded_train.is_cuda
+    dev_qml = None
+    if use_gpu:
+        try:
+            dev_qml = qml.device("lightning.gpu", wires=NUM_QUBITS)
+        except Exception:
+            use_gpu = False
+    if not use_gpu or dev_qml is None:
+        dev_qml = qml.device("default.qubit", wires=NUM_QUBITS)
+        use_gpu = False
+        if isinstance(encoded_train, torch.Tensor):
+            encoded_train = encoded_train.cpu().numpy()
+        if isinstance(encoded_test, torch.Tensor):
+            encoded_test = encoded_test.cpu().numpy()
+
+    if use_gpu:
+        return _run_training_gpu(
+            encoded_train,
+            encoded_test,
+            Y_train,
+            Y_test,
+            dev_qml=dev_qml,
+            num_layers=num_layers,
+            iterations=iterations,
+            batch_size=batch_size,
+            lr=lr,
+            seed=seed,
+            n_train=n_train,
+            rng=rng,
+            early_stop_target=early_stop_target,
+        )
+    return _run_training_cpu(
+        encoded_train,
+        encoded_test,
+        Y_train,
+        Y_test,
+        dev_qml=dev_qml,
+        num_layers=num_layers,
+        iterations=iterations,
+        batch_size=batch_size,
+        lr=lr,
+        seed=seed,
+        n_train=n_train,
+        rng=rng,
+        qml_device="cpu",
+        early_stop_target=early_stop_target,
+        optimizer=optimizer,
+    )
+
+
+def _run_training_cpu(
+    encoded_train: np.ndarray,
+    encoded_test: np.ndarray,
+    Y_train: np.ndarray,
+    Y_test: np.ndarray,
+    *,
+    dev_qml: Any,
+    num_layers: int,
+    iterations: int,
+    batch_size: int,
+    lr: float,
+    seed: int,
+    n_train: int,
+    rng: np.random.Generator,
+    qml_device: str = "cpu",
+    early_stop_target: float | None = None,
+    optimizer: str = "nesterov",
+) -> dict[str, Any]:
+    """CPU path: default.qubit + autograd + Nesterov or Adam. Optional early 
stop every 100 steps."""
+    try:
+        pnp.random.seed(seed)
+    except Exception:
+        pass
+    feats_train = pnp.array(encoded_train)
+    feats_test = encoded_test
+    Y_train_pnp = pnp.array(Y_train)
+    Y_test_np = np.asarray(Y_test)
+
+    @qml.qnode(dev_qml, interface="autograd", diff_method="backprop")
+    def circuit(weights, state_vector):
+        qml.StatePrep(state_vector, wires=(0, 1))
+        for lw in weights:
+            layer(lw, wires=(0, 1))
+        return qml.expval(qml.PauliZ(0))
+
+    def model(weights, bias, state_batch):
+        return circuit(weights, state_batch) + bias
+
+    def cost(weights, bias, X_batch, Y_batch):
+        preds = model(weights, bias, X_batch)
+        return pnp.mean((Y_batch - preds) ** 2)
+
+    weights_init = 0.01 * pnp.random.randn(
+        num_layers, NUM_QUBITS, 3, requires_grad=True
+    )
+    bias_init = pnp.array(0.0, requires_grad=True)
+    opt = AdamOptimizer(lr) if optimizer == "adam" else 
NesterovMomentumOptimizer(lr)
+
+    t0 = time.perf_counter()
+    _ = circuit(weights_init, feats_train[0])
+    _ = cost(weights_init, bias_init, feats_train[:1], Y_train_pnp[:1])
+    compile_sec = time.perf_counter() - t0
+
+    t0 = time.perf_counter()
+    weights, bias = weights_init, bias_init
+    steps_done = 0
+    for step in range(iterations):
+        batch_idx = rng.integers(0, n_train, size=(batch_size,))
+        fb = feats_train[batch_idx]
+        yb = Y_train_pnp[batch_idx]
+        out = opt.step(cost, weights, bias, fb, yb)
+        weights, bias = out[0], out[1]
+        steps_done += 1
+        if early_stop_target is not None and (step + 1) % 100 == 0:
+            pred_test_now = np.sign(
+                np.array(model(weights, bias, pnp.array(feats_test)))
+            ).flatten()
+            test_acc_now = float(np.mean(np.abs(pred_test_now - Y_test_np) < 
1e-5))
+            if test_acc_now >= early_stop_target:
+                break
+    train_sec = time.perf_counter() - t0
+
+    pred_train = np.sign(np.array(model(weights, bias, feats_train))).flatten()
+    pred_test = np.sign(np.array(model(weights, bias, 
pnp.array(feats_test)))).flatten()
+    Y_train_np = np.array(Y_train_pnp)
+    train_acc = float(np.mean(np.abs(pred_train - Y_train_np) < 1e-5))
+    test_acc = float(np.mean(np.abs(pred_test - Y_test_np) < 1e-5))
+
+    return {
+        "compile_time_sec": compile_sec,
+        "train_time_sec": train_sec,
+        "train_accuracy": train_acc,
+        "test_accuracy": test_acc,
+        "n_train": n_train,
+        "n_test": len(Y_test),
+        "epochs": steps_done,
+        "samples_per_sec": (steps_done * batch_size) / train_sec
+        if train_sec > 0
+        else 0.0,
+        "qml_device": qml_device,
+    }
+
+
+def _run_training_gpu(
+    encoded_train: torch.Tensor,
+    encoded_test: torch.Tensor,
+    Y_train: np.ndarray,
+    Y_test: np.ndarray,
+    *,
+    dev_qml: Any,
+    num_layers: int,
+    iterations: int,
+    batch_size: int,
+    lr: float,
+    seed: int,
+    n_train: int,
+    rng: np.random.Generator,
+    early_stop_target: float | None = None,
+) -> dict[str, Any]:
+    """GPU path: lightning.gpu + PyTorch interface, data stays on GPU. 
Optional early stop every 100 steps."""
+    device = encoded_train.device
+    dtype = encoded_train.dtype
+    Y_train_t = torch.tensor(Y_train, dtype=dtype, device=device)
+    Y_test_t = torch.tensor(Y_test, dtype=dtype, device=device)
+
+    @qml.qnode(dev_qml, interface="torch", diff_method="adjoint")
+    def circuit(weights, state_vector):
+        qml.StatePrep(state_vector, wires=(0, 1))
+        for lw in weights:
+            layer(lw, wires=(0, 1))
+        return qml.expval(qml.PauliZ(0))
+
+    def model(weights, bias, state_batch):
+        return circuit(weights, state_batch) + bias
+
+    def cost(weights, bias, X_batch, Y_batch):
+        preds = model(weights, bias, X_batch)
+        return torch.mean((Y_batch - preds) ** 2)
+
+    torch.manual_seed(seed)
+    weights = 0.01 * torch.randn(
+        num_layers, NUM_QUBITS, 3, device=device, dtype=dtype, 
requires_grad=True
+    )
+    bias = torch.tensor(0.0, device=device, dtype=dtype, requires_grad=True)
+    opt = torch.optim.SGD([weights, bias], lr=lr, momentum=0.9, nesterov=True)
+
+    t0 = time.perf_counter()
+    _ = circuit(weights, encoded_train[0:1])
+    _ = cost(weights, bias, encoded_train[:1], Y_train_t[:1])
+    compile_sec = time.perf_counter() - t0
+
+    t0 = time.perf_counter()
+    steps_done = 0
+    for step in range(iterations):
+        opt.zero_grad()
+        batch_idx = rng.integers(0, n_train, size=(batch_size,))
+        fb = encoded_train[batch_idx]
+        yb = Y_train_t[batch_idx]
+        loss = cost(weights, bias, fb, yb)
+        loss.backward()
+        opt.step()
+        steps_done += 1
+        if early_stop_target is not None and (step + 1) % 100 == 0:
+            with torch.no_grad():
+                pred_test_now = torch.sign(model(weights, bias, 
encoded_test)).flatten()
+                test_acc_now = (
+                    (pred_test_now - 
Y_test_t).abs().lt(1e-5).float().mean().item()
+                )
+            if test_acc_now >= early_stop_target:
+                break
+    train_sec = time.perf_counter() - t0
+
+    with torch.no_grad():
+        pred_train = torch.sign(model(weights, bias, encoded_train)).flatten()
+        pred_test = torch.sign(model(weights, bias, encoded_test)).flatten()
+    train_acc = (pred_train - Y_train_t).abs().lt(1e-5).float().mean().item()
+    test_acc = (pred_test - Y_test_t).abs().lt(1e-5).float().mean().item()
+
+    return {
+        "compile_time_sec": compile_sec,
+        "train_time_sec": train_sec,
+        "train_accuracy": float(train_acc),
+        "test_accuracy": float(test_acc),
+        "n_train": n_train,
+        "n_test": len(Y_test),
+        "epochs": steps_done,
+        "samples_per_sec": (steps_done * batch_size) / train_sec
+        if train_sec > 0
+        else 0.0,
+        "qml_device": "cuda",
+    }
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(
+        description="QDP Iris amplitude encoding pipeline (2-class, same 
training as baseline)"
+    )
+    parser.add_argument(
+        "--iters",
+        type=int,
+        default=1500,
+        help="Max optimizer steps per run (default: 1500)",
+    )
+    parser.add_argument(
+        "--batch-size", type=int, default=5, help="Batch size (default: 5)"
+    )
+    parser.add_argument(
+        "--layers", type=int, default=10, help="Variational layers (default: 
10)"
+    )
+    parser.add_argument(
+        "--lr", type=float, default=0.08, help="Learning rate (default: 0.08)"
+    )
+    parser.add_argument(
+        "--test-size",
+        type=float,
+        default=0.1,
+        help="Test fraction (default: 0.1); ignored if --data-file set",
+    )
+    parser.add_argument("--seed", type=int, default=0, help="Random seed 
(default: 0)")
+    parser.add_argument(
+        "--optimizer",
+        type=str,
+        default="adam",
+        choices=("adam", "nesterov"),
+        help="Optimizer for CPU (default: adam); GPU uses SGD+Nesterov",
+    )
+    parser.add_argument(
+        "--trials",
+        type=int,
+        default=20,
+        help="Number of restarts; best test acc reported (default: 20)",
+    )
+    parser.add_argument(
+        "--early-stop",
+        type=float,
+        default=0.9,
+        help="Stop run when test acc >= this (default: 0.9; 0 = off)",
+    )
+    parser.add_argument(
+        "--data-file",
+        type=str,
+        default=None,
+        help="Path to official Iris file (cols: f0,f1,f2,f3,label). If set, 
use first 2 cols + 75%% train.",
+    )
+    parser.add_argument(
+        "--device-id", type=int, default=0, help="QDP device (default: 0)"
+    )
+    parser.add_argument(
+        "--data-dir", type=str, default=None, help="Dir for .npy files 
(default: temp)"
+    )
+    args = parser.parse_args()
+
+    # Data source: official file (when --data-file set) or sklearn Iris 
(default)
+    if args.data_file:
+        X_norm, Y = load_iris_4d_from_file(args.data_file)
+        test_size = 0.25  # tutorial uses 75% train
+        data_src = f"official file (2 features): {args.data_file}"
+    else:
+        X_norm, Y = load_iris_binary_4d(seed=args.seed)
+        test_size = args.test_size
+        data_src = "sklearn load_iris, classes 0 & 1, 4 features"
+    n = len(Y)
+    rng = np.random.default_rng(args.seed)
+    idx = rng.permutation(n)
+    n_train = int(n * (1 - test_size))
+    train_idx, test_idx = idx[:n_train], idx[n_train:]
+    X_train_4d = X_norm[train_idx]
+    X_test_4d = X_norm[test_idx]
+    Y_train = Y[train_idx]
+    Y_test = Y[test_idx]
+
+    # QDP encoding: 4-D → amplitude-encoded state vectors
+    encoded_train = encode_via_qdp(
+        X_train_4d,
+        batch_size=args.batch_size,
+        device_id=args.device_id,
+        data_dir=args.data_dir,
+        filename="iris_4d_train.npy",
+    )
+    encoded_test = encode_via_qdp(
+        X_test_4d,
+        batch_size=args.batch_size,
+        device_id=args.device_id,
+        data_dir=args.data_dir,
+        filename="iris_4d_test.npy",
+    )
+
+    print("Iris amplitude (QDP encoding) — 2-class variational classifier")
+    print(f"  Data: {data_src} → QDP amplitude  (n={n}; 2-class Iris = 100 
samples)")
+    print(
+        f"  Iters: {args.iters}, batch_size: {args.batch_size}, layers: 
{args.layers}, lr: {args.lr}, optimizer: {args.optimizer}"
+    )
+
+    results: list[dict[str, Any]] = []
+    early_stop = args.early_stop if args.early_stop > 0 else None
+    for t in range(args.trials):
+        r = run_training(
+            encoded_train,
+            encoded_test,
+            Y_train,
+            Y_test,
+            num_layers=args.layers,
+            iterations=args.iters,
+            batch_size=args.batch_size,
+            lr=args.lr,
+            seed=args.seed + t,
+            early_stop_target=early_stop,
+            optimizer=args.optimizer,
+        )
+        results.append(r)
+        print(f"\n  Trial {t + 1}:")
+        print(f"    QML device: {r.get('qml_device', 'cpu')}")
+        print(f"    Compile:   {r['compile_time_sec']:.4f} s")
+        print(f"    Train:     {r['train_time_sec']:.4f} s")
+        print(f"    Train acc: {r['train_accuracy']:.4f}  (n={r['n_train']})")
+        print(f"    Test acc:  {r['test_accuracy']:.4f}  (n={r['n_test']})")
+        print(f"    Throughput: {r['samples_per_sec']:.1f} samples/s")
+
+    if args.trials > 1:
+        test_accs = sorted(r["test_accuracy"] for r in results)
+        best = test_accs[-1]
+        mid = args.trials // 2
+        print(
+            f"\n  Best test accuracy:  {best:.4f}  (median: 
{test_accs[mid]:.4f}, min: {test_accs[0]:.4f}, max: {test_accs[-1]:.4f})"
+        )
+        if best >= 0.9:
+            print("  → Target ≥0.9 achieved.")
+
+
+if __name__ == "__main__":
+    main()


Reply via email to