From 8b0fbf993a11f45208bb838c32011032729a0113 Mon Sep 17 00:00:00 2001 From: hu534766695-dev Date: Sat, 30 May 2026 19:39:10 +0800 Subject: [PATCH] Add CCF2026 professional MNIST submission --- ...t2\\CCF2026_Professional_MNIST\\README.md" | 80 +++++++++ ...nal_MNIST\\ccf2026_mnist_qml\\__init__.py" | 5 + ...onal_MNIST\\ccf2026_mnist_qml\\circuit.py" | 69 ++++++++ ...ional_MNIST\\ccf2026_mnist_qml\\config.py" | 19 +++ ...ssional_MNIST\\ccf2026_mnist_qml\\data.py" | 62 +++++++ ...l_MNIST\\ccf2026_mnist_qml\\experiment.py" | 158 ++++++++++++++++++ ...onal_MNIST\\ccf2026_mnist_qml\\metrics.py" | 47 ++++++ ...ional_MNIST\\ccf2026_mnist_qml\\models.py" | 133 +++++++++++++++ ...NIST\\ccf2026_mnist_qml\\preprocessing.py" | 88 ++++++++++ ...al_MNIST\\ccf2026_mnist_qml\\reporting.py" | 113 +++++++++++++ ..._MNIST\\ccf2026_mnist_qml\\statevector.py" | 62 +++++++ ...CCF2026_Professional_MNIST_submission.zip" | Bin 0 -> 17517 bytes ...2026_Professional_MNIST\\requirements.txt" | 3 + ...l_MNIST\\results\\quick_demo\\config.json" | 16 ++ ...quick_demo\\figures\\noise_robustness.svg" | 1 + ...s\\quick_demo\\figures\\test_accuracy.svg" | 1 + ...l_MNIST\\results\\quick_demo\\metrics.csv" | 10 ++ ...nal_MNIST\\results\\quick_demo\\report.md" | 53 ++++++ ...\\results\\quick_demo\\resource_table.csv" | 4 + ...NIST\\results\\quick_demo\\robustness.csv" | 9 + ...\\results\\quick_demo\\split_summary.json" | 17 ++ ...nal_MNIST\\scripts\\package_submission.py" | 34 ++++ ...ssional_MNIST\\scripts\\run_quick_demo.py" | 34 ++++ ..._Professional_MNIST\\tests\\test_smoke.py" | 36 ++++ 24 files changed, 1054 insertions(+) create mode 100644 "contest2\\CCF2026_Professional_MNIST\\README.md" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\__init__.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\circuit.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\config.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\data.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\experiment.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\metrics.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\models.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\preprocessing.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\reporting.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\statevector.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\dist\\CCF2026_Professional_MNIST_submission.zip" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\requirements.txt" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\config.json" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\noise_robustness.svg" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\test_accuracy.svg" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\metrics.csv" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\report.md" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\resource_table.csv" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\robustness.csv" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\split_summary.json" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\scripts\\package_submission.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\scripts\\run_quick_demo.py" create mode 100644 "contest2\\CCF2026_Professional_MNIST\\tests\\test_smoke.py" diff --git "a/contest2\\CCF2026_Professional_MNIST\\README.md" "b/contest2\\CCF2026_Professional_MNIST\\README.md" new file mode 100644 index 0000000..53a2f3a --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\README.md" @@ -0,0 +1,80 @@ +# CCF2026 Professional Track: 8-Qubit Noise-Robust MNIST Binary Classifier + +This entry targets the 2026 CCF Quantum Computing Programming Challenge, OriginQ Cup, +professional-track MNIST binary-classification task described as: + +- MNIST binary classification +- noise robustness +- 8 qubits +- no more than 100 trainable parameters + +The implementation is dependency-free Python so reviewers can run it immediately on a +plain Python 3.11 environment. The circuit and reporting interfaces are intentionally +kept close to QPanda3/VQNet concepts: angle encoding, hardware-efficient ansatz, +shot-based readout, noise-aware evaluation, and explicit resource accounting. + +## Method + +The model compares three systems on the same train/validation/test split: + +| model | purpose | +| --- | --- | +| `logistic_8feature` | classical baseline on the same 8 compressed features | +| `vqc_clean` | 8-qubit variational circuit trained without noise | +| `vqc_noise_aware` | same 8-qubit circuit trained with stochastic angle/readout noise | + +The quantum model uses: + +- 8 qubits +- 2 ansatz layers +- 32 trainable circuit parameters +- 9 classical-head parameters +- 41 total trainable parameters + +This stays comfortably below the 100-parameter limit. + +## Quick Start + +From this directory: + +```powershell +py -3.11 scripts\run_quick_demo.py +py -3.11 -m unittest discover -s tests +``` + +From the repository root: + +```powershell +py -3.11 contest2\CCF2026_Professional_MNIST\scripts\run_quick_demo.py +``` + +Outputs are written to `results/quick_demo/`: + +- `metrics.csv` +- `resource_table.csv` +- `robustness.csv` +- `config.json` +- `report.md` +- `figures/test_accuracy.svg` +- `figures/noise_robustness.svg` + +## Package + +Create an upload-ready zip: + +```powershell +py -3.11 scripts\package_submission.py +``` + +The zip is written to `dist/CCF2026_Professional_MNIST_submission.zip`. + +## Notes for Full QPanda3/VQNet Migration + +The default backend is a local statevector simulator because the current review +environment may not include QPanda3 or VQNet. To migrate: + +1. Replace `ccf2026_mnist_qml/circuit.py` gate calls with QPanda3 circuit builders. +2. Wrap the parameterized circuit as a VQNet quantum layer. +3. Keep `metrics.csv`, `resource_table.csv`, and `robustness.csv` schemas unchanged. +4. Run the same noise sweep and report the hardware or simulator backend name. + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\__init__.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\__init__.py" new file mode 100644 index 0000000..1de2a5d --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\__init__.py" @@ -0,0 +1,5 @@ +from .config import ExperimentConfig +from .experiment import run_experiment + +__all__ = ["ExperimentConfig", "run_experiment"] + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\circuit.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\circuit.py" new file mode 100644 index 0000000..9ad89aa --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\circuit.py" @@ -0,0 +1,69 @@ +import random +from typing import List, Optional, Sequence + +from .statevector import apply_cnot, apply_h, apply_ry, apply_rz, expectation_z, zero_state + + +def circuit_features( + x: Sequence[float], + theta: Sequence[float], + n_qubits: int, + ansatz_layers: int, + shots: Optional[int], + noise_prob: float, + angle_jitter: float, + rng: random.Random, +) -> List[float]: + state = zero_state(n_qubits) + for qubit in range(n_qubits): + angle = x[qubit] + (rng.gauss(0.0, angle_jitter) if angle_jitter else 0.0) + state = apply_h(state, n_qubits, qubit) + state = apply_ry(state, n_qubits, qubit, angle) + state = apply_rz(state, n_qubits, qubit, 0.5 * angle) + for qubit in range(n_qubits - 1): + state = apply_cnot(state, n_qubits, qubit, qubit + 1) + + cursor = 0 + for _ in range(ansatz_layers): + for qubit in range(n_qubits): + state = apply_ry(state, n_qubits, qubit, theta[cursor]) + cursor += 1 + state = apply_rz(state, n_qubits, qubit, theta[cursor]) + cursor += 1 + for qubit in range(n_qubits - 1): + state = apply_cnot(state, n_qubits, qubit, qubit + 1) + + values = [expectation_z(state, qubit) for qubit in range(n_qubits)] + if noise_prob > 0: + attenuation = max(0.0, (1.0 - noise_prob) ** (2 * n_qubits + 3 * ansatz_layers * n_qubits)) + values = [value * attenuation for value in values] + if shots and shots > 0: + sampled = [] + for value in values: + readout_flip = noise_prob * 0.5 + p_one = min(1.0, max(0.0, (1.0 - value) / 2.0)) + p_one = p_one * (1.0 - readout_flip) + (1.0 - p_one) * readout_flip + ones = sum(1 for _ in range(shots) if rng.random() < p_one) + sampled.append(1.0 - 2.0 * ones / shots) + values = sampled + return values + + +def resource_profile(n_qubits: int, ansatz_layers: int, shots: int, model: str, backend: str): + one_qubit_gates = 3 * n_qubits + ansatz_layers * 2 * n_qubits + two_qubit_gates = (ansatz_layers + 1) * (n_qubits - 1) + circuit_params = ansatz_layers * 2 * n_qubits + head_params = n_qubits + 1 + return { + "model": model, + "qubits": n_qubits, + "depth": 3 + (ansatz_layers + 1) * (n_qubits - 1) + ansatz_layers * 2, + "circuit_params": circuit_params, + "head_params": head_params, + "total_params": circuit_params + head_params, + "one_qubit_gates": one_qubit_gates, + "two_qubit_gates": two_qubit_gates, + "shots": shots, + "backend": backend, + } + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\config.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\config.py" new file mode 100644 index 0000000..a4e5f89 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\config.py" @@ -0,0 +1,19 @@ +from dataclasses import dataclass + + +@dataclass +class ExperimentConfig: + dataset_name: str = "mnist_like_3_vs_8" + n_samples: int = 120 + n_pixels: int = 64 + n_qubits: int = 8 + ansatz_layers: int = 2 + seed: int = 2020 + train_ratio: float = 0.6 + val_ratio: float = 0.2 + logistic_epochs: int = 180 + vqc_epochs: int = 12 + shots: int = 512 + eval_noise_prob: float = 0.04 + train_noise_prob: float = 0.04 + angle_jitter: float = 0.04 diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\data.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\data.py" new file mode 100644 index 0000000..5371880 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\data.py" @@ -0,0 +1,62 @@ +import random +from typing import List, Tuple + + +Matrix = List[List[float]] +Vector = List[float] + + +THREE = [ + "01111100", + "10000010", + "00000010", + "00011100", + "00000010", + "00000010", + "10000010", + "01111100", +] + +EIGHT = [ + "00111100", + "01000010", + "01000010", + "00111100", + "01000010", + "01000010", + "01000010", + "00111100", +] + + +def make_mnist_like_binary_dataset(n_samples: int, seed: int) -> Tuple[Matrix, List[int]]: + rng = random.Random(seed) + rows: Matrix = [] + labels: List[int] = [] + for idx in range(n_samples): + label = idx % 2 + prototype = EIGHT if label else THREE + rows.append(_distort(_flatten(prototype), rng)) + labels.append(label) + order = list(range(n_samples)) + rng.shuffle(order) + return [rows[i] for i in order], [labels[i] for i in order] + + +def _flatten(pattern: List[str]) -> Vector: + return [1.0 if ch == "1" else 0.0 for line in pattern for ch in line] + + +def _distort(base: Vector, rng: random.Random) -> Vector: + row = [] + shift = rng.choice([-1, 0, 0, 1]) + for idx, value in enumerate(base): + col = idx % 8 + shifted_idx = idx + shift if 0 <= col + shift < 8 else idx + source = base[shifted_idx] + noisy = source + rng.gauss(0.0, 0.18) + if rng.random() < 0.035: + noisy = 1.0 - noisy + row.append(min(1.0, max(0.0, noisy))) + return row + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\experiment.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\experiment.py" new file mode 100644 index 0000000..d401b2b --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\experiment.py" @@ -0,0 +1,158 @@ +import csv +import json +import time +from pathlib import Path +from typing import Dict, List + +from .circuit import resource_profile +from .config import ExperimentConfig +from .data import make_mnist_like_binary_dataset +from .metrics import classification_metrics +from .models import LogisticBaseline, NoiseAwareVQC +from .preprocessing import preprocess_splits, stratified_split +from .reporting import write_bar_svg, write_report + + +def run_experiment(output_dir: Path, config: ExperimentConfig) -> Dict[str, object]: + output_dir.mkdir(parents=True, exist_ok=True) + figures_dir = output_dir / "figures" + figures_dir.mkdir(parents=True, exist_ok=True) + + x_rows, y = make_mnist_like_binary_dataset(config.n_samples, config.seed) + splits = stratified_split(x_rows, y, config.train_ratio, config.val_ratio, config.seed) + processed = preprocess_splits(splits) + + models = [ + ("logistic_8feature", LogisticBaseline(config.logistic_epochs), processed["selected"], None), + ( + "vqc_clean", + NoiseAwareVQC( + config.n_qubits, + config.ansatz_layers, + config.vqc_epochs, + config.shots, + train_noise_prob=0.0, + angle_jitter=0.0, + seed=config.seed, + ), + processed["angle"], + 0.0, + ), + ( + "vqc_noise_aware", + NoiseAwareVQC( + config.n_qubits, + config.ansatz_layers, + config.vqc_epochs, + config.shots, + train_noise_prob=config.train_noise_prob, + angle_jitter=config.angle_jitter, + seed=config.seed + 17, + ), + processed["angle"], + config.train_noise_prob, + ), + ] + + metric_rows: List[Dict[str, object]] = [] + resource_rows: List[Dict[str, object]] = [] + robustness_rows: List[Dict[str, object]] = [] + test_accuracy = {} + + for model_name, model, data_splits, model_noise in models: + train_x, train_y = data_splits["train"] + start = time.perf_counter() + model.fit(train_x, train_y) + train_time = time.perf_counter() - start + infer_time = 0.0 + + for split_name in ("train", "val", "test"): + x_split, y_split = data_splits[split_name] + start = time.perf_counter() + if model_name.startswith("vqc"): + probs = model.predict_proba(x_split, noise_prob=model_noise or 0.0, angle_jitter=0.0) + else: + probs = model.predict_proba(x_split) + infer_time += time.perf_counter() - start + row = {"model": model_name, "split": split_name} + row.update(_round_metrics(classification_metrics(y_split, probs))) + metric_rows.append(row) + if split_name == "test": + test_accuracy[model_name] = row["accuracy"] + + if model_name.startswith("vqc"): + profile = resource_profile( + config.n_qubits, + config.ansatz_layers, + config.shots, + model_name, + "local_statevector_noise_aware" if model_noise else "local_statevector", + ) + else: + profile = { + "model": model_name, + "qubits": 0, + "depth": 0, + "circuit_params": 0, + "head_params": 9, + "total_params": 9, + "one_qubit_gates": 0, + "two_qubit_gates": 0, + "shots": 0, + "backend": "classical", + } + profile.update({"train_time_sec": round(train_time, 6), "infer_time_sec": round(infer_time, 6)}) + resource_rows.append(profile) + + for model_name, model, data_splits, _ in models: + if not model_name.startswith("vqc"): + continue + x_test, y_test = data_splits["test"] + for noise in (0.0, 0.02, config.eval_noise_prob, 0.08): + probs = model.predict_proba(x_test, noise_prob=noise, angle_jitter=config.angle_jitter if noise else 0.0) + row = {"model": model_name, "noise_prob": noise} + row.update(_round_metrics(classification_metrics(y_test, probs))) + robustness_rows.append(row) + + split_info = split_summary(splits) + write_csv(output_dir / "metrics.csv", metric_rows, list(metric_rows[0].keys())) + write_csv(output_dir / "resource_table.csv", resource_rows, list(resource_rows[0].keys())) + write_csv(output_dir / "robustness.csv", robustness_rows, list(robustness_rows[0].keys())) + write_json(output_dir / "config.json", config.__dict__) + write_json(output_dir / "split_summary.json", split_info) + write_report(output_dir / "report.md", config, metric_rows, resource_rows, robustness_rows, split_info) + write_bar_svg(figures_dir / "test_accuracy.svg", "Test accuracy", test_accuracy) + write_bar_svg( + figures_dir / "noise_robustness.svg", + "F1 at evaluation noise", + {row["model"] + "@" + str(row["noise_prob"]): row["f1"] for row in robustness_rows}, + ) + return {"metrics": metric_rows, "resources": resource_rows, "robustness": robustness_rows} + + +def write_csv(path: Path, rows: List[Dict[str, object]], fieldnames: List[str]) -> None: + with path.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + writer.writerow(row) + + +def write_json(path: Path, payload: Dict[str, object]) -> None: + path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + + +def split_summary(splits) -> Dict[str, Dict[str, int]]: + summary = {} + for name, (_, labels) in splits.items(): + summary[name] = { + "samples": len(labels), + "class_0": sum(1 for label in labels if label == 0), + "class_1": sum(1 for label in labels if label == 1), + } + return summary + + +def _round_metrics(metrics): + return {key: round(float(value), 6) for key, value in metrics.items()} + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\metrics.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\metrics.py" new file mode 100644 index 0000000..6f800c7 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\metrics.py" @@ -0,0 +1,47 @@ +import math +from typing import Dict, List, Sequence + + +def binary_log_loss(y_true: Sequence[int], probabilities: Sequence[float]) -> float: + eps = 1e-9 + total = 0.0 + for y, p in zip(y_true, probabilities): + p = min(1.0 - eps, max(eps, p)) + total += -(y * math.log(p) + (1 - y) * math.log(1.0 - p)) + return total / max(1, len(y_true)) + + +def classification_metrics(y_true: Sequence[int], probabilities: Sequence[float]) -> Dict[str, float]: + pred = [1 if p >= 0.5 else 0 for p in probabilities] + tp = sum(1 for y, p in zip(y_true, pred) if y == 1 and p == 1) + tn = sum(1 for y, p in zip(y_true, pred) if y == 0 and p == 0) + fp = sum(1 for y, p in zip(y_true, pred) if y == 0 and p == 1) + fn = sum(1 for y, p in zip(y_true, pred) if y == 1 and p == 0) + precision = tp / max(1, tp + fp) + recall = tp / max(1, tp + fn) + return { + "accuracy": (tp + tn) / max(1, len(y_true)), + "precision": precision, + "recall": recall, + "f1": 0.0 if precision + recall == 0 else 2 * precision * recall / (precision + recall), + "auc": roc_auc(y_true, probabilities), + "log_loss": binary_log_loss(y_true, probabilities), + "tp": float(tp), + "tn": float(tn), + "fp": float(fp), + "fn": float(fn), + } + + +def roc_auc(y_true: Sequence[int], scores: Sequence[float]) -> float: + positives = sum(1 for y in y_true if y == 1) + negatives = len(y_true) - positives + if positives == 0 or negatives == 0: + return 0.5 + ordered = sorted(zip(scores, y_true), key=lambda item: item[0]) + rank_sum = 0.0 + for rank, (_, y) in enumerate(ordered, start=1): + if y == 1: + rank_sum += rank + return (rank_sum - positives * (positives + 1) / 2.0) / (positives * negatives) + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\models.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\models.py" new file mode 100644 index 0000000..43e6009 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\models.py" @@ -0,0 +1,133 @@ +import math +import random +from typing import Dict, List, Optional, Sequence + +from .circuit import circuit_features +from .data import Matrix, Vector +from .metrics import binary_log_loss + + +class LogisticBaseline: + def __init__(self, epochs: int, lr: float = 0.08): + self.epochs = epochs + self.lr = lr + self.weights: Vector = [] + self.bias = 0.0 + + def fit(self, x_rows: Matrix, y: Sequence[int]): + self.weights = [0.0] * len(x_rows[0]) + self.bias = 0.0 + for _ in range(self.epochs): + grad_w = [0.0] * len(self.weights) + grad_b = 0.0 + for row, label in zip(x_rows, y): + pred = _sigmoid(_dot(self.weights, row) + self.bias) + err = pred - label + for idx, value in enumerate(row): + grad_w[idx] += err * value + grad_b += err + for idx in range(len(self.weights)): + self.weights[idx] -= self.lr * grad_w[idx] / len(x_rows) + self.bias -= self.lr * grad_b / len(x_rows) + return self + + def predict_proba(self, x_rows: Matrix) -> Vector: + return [_sigmoid(_dot(self.weights, row) + self.bias) for row in x_rows] + + +class NoiseAwareVQC: + def __init__( + self, + n_qubits: int, + ansatz_layers: int, + epochs: int, + shots: int, + train_noise_prob: float, + angle_jitter: float, + seed: int, + ): + self.n_qubits = n_qubits + self.ansatz_layers = ansatz_layers + self.epochs = epochs + self.shots = shots + self.train_noise_prob = train_noise_prob + self.angle_jitter = angle_jitter + self.seed = seed + self.theta: Vector = [] + self.head_weights: Vector = [] + self.head_bias = 0.0 + self.history: List[Dict[str, float]] = [] + + @property + def total_params(self) -> int: + return len(self.theta) + len(self.head_weights) + 1 + + def fit(self, x_rows: Matrix, y: Sequence[int]): + rng = random.Random(self.seed) + self.theta = [rng.uniform(-0.18, 0.18) for _ in range(2 * self.n_qubits * self.ansatz_layers)] + self.head_weights = [0.0] * self.n_qubits + self.head_bias = 0.0 + for epoch in range(1, self.epochs + 1): + features = self._features(x_rows, self.theta, None, self.train_noise_prob, self.angle_jitter, rng) + probs = [_sigmoid(_dot(self.head_weights, feat) + self.head_bias) for feat in features] + loss = binary_log_loss(y, probs) + self._update_head(features, y, probs, lr=0.15) + + delta = [1.0 if rng.random() < 0.5 else -1.0 for _ in self.theta] + step = 0.08 + theta_plus = [value + step * d for value, d in zip(self.theta, delta)] + theta_minus = [value - step * d for value, d in zip(self.theta, delta)] + loss_plus = self._loss(x_rows, y, theta_plus, rng) + loss_minus = self._loss(x_rows, y, theta_minus, rng) + grad_scale = (loss_plus - loss_minus) / (2.0 * step) + for idx, d in enumerate(delta): + self.theta[idx] -= 0.07 * grad_scale * d + self.history.append({"epoch": float(epoch), "loss": loss}) + return self + + def predict_proba(self, x_rows: Matrix, noise_prob: Optional[float] = None, angle_jitter: Optional[float] = None): + rng = random.Random(self.seed + 999) + features = self._features( + x_rows, + self.theta, + self.shots, + self.train_noise_prob if noise_prob is None else noise_prob, + self.angle_jitter if angle_jitter is None else angle_jitter, + rng, + ) + return [_sigmoid(_dot(self.head_weights, feat) + self.head_bias) for feat in features] + + def _update_head(self, features: Matrix, y: Sequence[int], probs: Sequence[float], lr: float): + grad_w = [0.0] * len(self.head_weights) + grad_b = 0.0 + for feat, label, prob in zip(features, y, probs): + err = prob - label + for idx, value in enumerate(feat): + grad_w[idx] += err * value + grad_b += err + for idx in range(len(self.head_weights)): + self.head_weights[idx] -= lr * grad_w[idx] / len(features) + self.head_bias -= lr * grad_b / len(features) + + def _loss(self, x_rows: Matrix, y: Sequence[int], theta: Sequence[float], rng: random.Random) -> float: + features = self._features(x_rows, theta, None, self.train_noise_prob, self.angle_jitter, rng) + return binary_log_loss(y, [_sigmoid(_dot(self.head_weights, feat) + self.head_bias) for feat in features]) + + def _features(self, x_rows, theta, shots, noise_prob, angle_jitter, rng): + return [ + circuit_features(row, theta, self.n_qubits, self.ansatz_layers, shots, noise_prob, angle_jitter, rng) + for row in x_rows + ] + + +def _dot(left: Sequence[float], right: Sequence[float]) -> float: + return sum(a * b for a, b in zip(left, right)) + + +def _sigmoid(value: float) -> float: + if value >= 0: + z = math.exp(-value) + return 1.0 / (1.0 + z) + z = math.exp(value) + return z / (1.0 + z) + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\preprocessing.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\preprocessing.py" new file mode 100644 index 0000000..3dd7385 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\preprocessing.py" @@ -0,0 +1,88 @@ +import math +import random +from typing import Dict, List, Sequence, Tuple + +from .data import Matrix + + +def stratified_split(x_rows: Matrix, y: Sequence[int], train_ratio: float, val_ratio: float, seed: int): + rng = random.Random(seed) + by_label = {0: [], 1: []} + for idx, label in enumerate(y): + by_label[int(label)].append(idx) + splits = {"train": [], "val": [], "test": []} + for indices in by_label.values(): + rng.shuffle(indices) + n_train = int(round(len(indices) * train_ratio)) + n_val = int(round(len(indices) * val_ratio)) + splits["train"].extend(indices[:n_train]) + splits["val"].extend(indices[n_train:n_train + n_val]) + splits["test"].extend(indices[n_train + n_val:]) + return { + name: ([x_rows[i] for i in indices], [int(y[i]) for i in indices]) + for name, indices in splits.items() + } + + +def compress_to_8_features(x_rows: Matrix) -> Matrix: + compressed = [] + for row in x_rows: + blocks = [ + _mean(row[0:16]), + _mean(row[16:32]), + _mean(row[32:48]), + _mean(row[48:64]), + _mean(row[0::8]), + _mean(row[7::8]), + _vertical_symmetry(row), + _center_density(row), + ] + compressed.append(blocks) + return compressed + + +def fit_standardizer(x_rows: Matrix): + cols = list(zip(*x_rows)) + mean = [sum(col) / len(col) for col in cols] + scale = [] + for col, mu in zip(cols, mean): + var = sum((v - mu) ** 2 for v in col) / max(1, len(col) - 1) + scale.append(math.sqrt(var) or 1.0) + return mean, scale + + +def apply_standardizer(x_rows: Matrix, mean, scale) -> Matrix: + return [[(value - mean[i]) / scale[i] for i, value in enumerate(row)] for row in x_rows] + + +def angle_encode(x_rows: Matrix) -> Matrix: + output = [] + for row in x_rows: + output.append([max(-math.pi, min(math.pi, value * math.pi / 2.5)) for value in row]) + return output + + +def preprocess_splits(splits) -> Dict[str, Tuple[Matrix, List[int]]]: + raw = {name: (compress_to_8_features(x), labels) for name, (x, labels) in splits.items()} + mean, scale = fit_standardizer(raw["train"][0]) + selected = {name: (apply_standardizer(x, mean, scale), labels) for name, (x, labels) in raw.items()} + angle = {name: (angle_encode(x), labels) for name, (x, labels) in selected.items()} + return {"selected": selected, "angle": angle, "mean": mean, "scale": scale} + + +def _mean(values) -> float: + return sum(values) / max(1, len(values)) + + +def _vertical_symmetry(row) -> float: + total = 0.0 + for r in range(8): + for c in range(4): + total += 1.0 - abs(row[r * 8 + c] - row[r * 8 + (7 - c)]) + return total / 32.0 + + +def _center_density(row) -> float: + ids = [r * 8 + c for r in range(2, 6) for c in range(2, 6)] + return _mean([row[i] for i in ids]) + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\reporting.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\reporting.py" new file mode 100644 index 0000000..6cbcb92 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\reporting.py" @@ -0,0 +1,113 @@ +from pathlib import Path +from typing import Dict, Iterable, List + +from .config import ExperimentConfig + + +def write_report(path: Path, config: ExperimentConfig, metrics, resources, robustness, split_summary) -> None: + lines = [ + "# CCF2026 Professional Track: MNIST Binary Classification", + "", + "## Constraint Check", + "", + f"- Qubits: `{config.n_qubits}`", + f"- Ansatz layers: `{config.ansatz_layers}`", + "- Circuit parameters: `32`", + "- Classical head parameters: `9`", + "- Total trainable parameters: `41 <= 100`", + f"- Evaluation noise probability: `{config.eval_noise_prob}`", + "", + "## Data", + "", + "A deterministic 8x8 MNIST-like 3-vs-8 binary dataset is generated locally. " + "The full pipeline can be swapped to official MNIST arrays by replacing " + "`make_mnist_like_binary_dataset` while keeping the same compressed 8-feature contract.", + "", + markdown_table(["split", "samples", "class_0_digit3", "class_1_digit8"], [ + [name, item["samples"], item["class_0"], item["class_1"]] + for name, item in split_summary.items() + ]), + "", + "## Test Metrics", + "", + markdown_table(["model", "accuracy", "f1", "auc", "log_loss"], [ + [row["model"], row["accuracy"], row["f1"], row["auc"], row["log_loss"]] + for row in metrics if row["split"] == "test" + ]), + "", + "## Noise Robustness", + "", + markdown_table(["model", "noise_prob", "accuracy", "f1", "auc"], [ + [row["model"], row["noise_prob"], row["accuracy"], row["f1"], row["auc"]] + for row in robustness + ]), + "", + "## Quantum Resources", + "", + markdown_table( + ["model", "qubits", "depth", "total_params", "1q", "2q", "shots", "backend"], + [ + [ + row["model"], + row["qubits"], + row["depth"], + row["total_params"], + row["one_qubit_gates"], + row["two_qubit_gates"], + row["shots"], + row["backend"], + ] + for row in resources + ], + ), + "", + "## Why Noise-Aware Training", + "", + "The clean VQC is optimized for ideal statevector features. The noise-aware VQC " + "injects angle jitter plus readout/gate attenuation during training, so its head " + "and variational parameters see the same distribution shift that appears during " + "noisy evaluation. This is intentionally small enough for the 8-qubit/100-parameter " + "professional-track constraint.", + "", + ] + path.write_text("\n".join(lines), encoding="utf-8") + + +def markdown_table(headers: Iterable[object], rows: Iterable[Iterable[object]]) -> str: + headers = [str(item) for item in headers] + rows = [[str(value) for value in row] for row in rows] + output = ["| " + " | ".join(headers) + " |", "| " + " | ".join("---" for _ in headers) + " |"] + output += ["| " + " | ".join(row) + " |" for row in rows] + return "\n".join(output) + + +def write_bar_svg(path: Path, title: str, values: Dict[str, float]) -> None: + width, height, margin = 760, 340, 58 + max_value = max([0.01] + list(values.values())) + bar_width = 120 + gap = 48 + bars = [] + for idx, (name, value) in enumerate(values.items()): + x = margin + idx * (bar_width + gap) + h = int((height - 2 * margin) * value / max_value) + y = height - margin - h + bars.append( + f'' + f'{value:.3f}' + f'{_esc(name)}' + ) + svg = ( + f'' + '' + f'{_esc(title)}' + f'' + f'' + + "".join(bars) + + "" + ) + path.write_text(svg, encoding="utf-8") + + +def _esc(value: str) -> str: + return value.replace("&", "&").replace("<", "<").replace(">", ">") + diff --git "a/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\statevector.py" "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\statevector.py" new file mode 100644 index 0000000..3734186 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\ccf2026_mnist_qml\\statevector.py" @@ -0,0 +1,62 @@ +import math +from typing import List + + +State = List[complex] + + +def zero_state(n_qubits: int) -> State: + state = [0j] * (1 << n_qubits) + state[0] = 1.0 + 0j + return state + + +def apply_ry(state: State, n_qubits: int, qubit: int, angle: float) -> State: + c = math.cos(angle / 2.0) + s = math.sin(angle / 2.0) + return apply_single_qubit(state, n_qubits, qubit, ((c, -s), (s, c))) + + +def apply_rz(state: State, n_qubits: int, qubit: int, angle: float) -> State: + p0 = complex(math.cos(-angle / 2.0), math.sin(-angle / 2.0)) + p1 = complex(math.cos(angle / 2.0), math.sin(angle / 2.0)) + return apply_single_qubit(state, n_qubits, qubit, ((p0, 0j), (0j, p1))) + + +def apply_h(state: State, n_qubits: int, qubit: int) -> State: + inv = 1.0 / math.sqrt(2.0) + return apply_single_qubit(state, n_qubits, qubit, ((inv, inv), (inv, -inv))) + + +def apply_single_qubit(state: State, n_qubits: int, qubit: int, matrix) -> State: + output = state[:] + step = 1 << qubit + block = step << 1 + for base in range(0, 1 << n_qubits, block): + for offset in range(step): + idx0 = base + offset + idx1 = idx0 + step + amp0 = state[idx0] + amp1 = state[idx1] + output[idx0] = matrix[0][0] * amp0 + matrix[0][1] * amp1 + output[idx1] = matrix[1][0] * amp0 + matrix[1][1] * amp1 + return output + + +def apply_cnot(state: State, n_qubits: int, control: int, target: int) -> State: + output = [0j] * (1 << n_qubits) + control_mask = 1 << control + target_mask = 1 << target + for idx, amp in enumerate(state): + output[idx ^ target_mask if idx & control_mask else idx] += amp + return output + + +def expectation_z(state: State, qubit: int) -> float: + mask = 1 << qubit + total = 0.0 + for idx, amp in enumerate(state): + prob = amp.real * amp.real + amp.imag * amp.imag + total += -prob if idx & mask else prob + return total + diff --git "a/contest2\\CCF2026_Professional_MNIST\\dist\\CCF2026_Professional_MNIST_submission.zip" "b/contest2\\CCF2026_Professional_MNIST\\dist\\CCF2026_Professional_MNIST_submission.zip" new file mode 100644 index 0000000000000000000000000000000000000000..c8a3cb1f67af0e4733dd1cc6f3751b1f5112de0e GIT binary patch literal 17517 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 QpnrT1ef*cm^Xt?90pZ>~y8r+H literal 0 HcmV?d00001 diff --git "a/contest2\\CCF2026_Professional_MNIST\\requirements.txt" "b/contest2\\CCF2026_Professional_MNIST\\requirements.txt" new file mode 100644 index 0000000..1c95393 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\requirements.txt" @@ -0,0 +1,3 @@ +# No third-party dependency is required for the default local run. +# Optional migration targets: pyqpanda3, pyvqnet, numpy, matplotlib. + diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\config.json" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\config.json" new file mode 100644 index 0000000..d18125c --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\config.json" @@ -0,0 +1,16 @@ +{ + "angle_jitter": 0.04, + "ansatz_layers": 2, + "dataset_name": "mnist_like_3_vs_8", + "eval_noise_prob": 0.04, + "logistic_epochs": 180, + "n_pixels": 64, + "n_qubits": 8, + "n_samples": 120, + "seed": 2020, + "shots": 256, + "train_noise_prob": 0.04, + "train_ratio": 0.6, + "val_ratio": 0.2, + "vqc_epochs": 10 +} \ No newline at end of file diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\noise_robustness.svg" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\noise_robustness.svg" new file mode 100644 index 0000000..0528e67 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\noise_robustness.svg" @@ -0,0 +1 @@ +F1 at evaluation noise0.667vqc_clean@0.00.667vqc_clean@0.020.500vqc_clean@0.040.261vqc_clean@0.080.692vqc_noise_aware@0.00.615vqc_noise_aware@0.020.615vqc_noise_aware@0.040.385vqc_noise_aware@0.08 \ No newline at end of file diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\test_accuracy.svg" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\test_accuracy.svg" new file mode 100644 index 0000000..a85cf20 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\figures\\test_accuracy.svg" @@ -0,0 +1 @@ +Test accuracy0.792logistic_8feature0.625vqc_clean0.625vqc_noise_aware \ No newline at end of file diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\metrics.csv" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\metrics.csv" new file mode 100644 index 0000000..2199416 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\metrics.csv" @@ -0,0 +1,10 @@ +model,split,accuracy,precision,recall,f1,auc,log_loss,tp,tn,fp,fn +logistic_8feature,train,0.972222,0.972222,0.972222,0.972222,0.994599,0.137032,35.0,35.0,1.0,1.0 +logistic_8feature,val,0.958333,1.0,0.916667,0.956522,0.986111,0.220991,11.0,12.0,0.0,1.0 +logistic_8feature,test,0.791667,0.888889,0.666667,0.761905,0.923611,0.373309,8.0,11.0,1.0,4.0 +vqc_clean,train,0.680556,0.675676,0.694444,0.684932,0.777006,0.659737,25.0,24.0,12.0,11.0 +vqc_clean,val,0.75,0.8,0.666667,0.727273,0.770833,0.661625,8.0,10.0,2.0,4.0 +vqc_clean,test,0.625,0.6,0.75,0.666667,0.75,0.668958,9.0,6.0,6.0,3.0 +vqc_noise_aware,train,0.597222,0.589744,0.638889,0.613333,0.634259,0.692967,23.0,20.0,16.0,13.0 +vqc_noise_aware,val,0.5,0.5,0.583333,0.538462,0.527778,0.693071,7.0,5.0,7.0,5.0 +vqc_noise_aware,test,0.625,0.615385,0.666667,0.64,0.583333,0.693091,8.0,7.0,5.0,4.0 diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\report.md" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\report.md" new file mode 100644 index 0000000..65c84c9 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\report.md" @@ -0,0 +1,53 @@ +# CCF2026 Professional Track: MNIST Binary Classification + +## Constraint Check + +- Qubits: `8` +- Ansatz layers: `2` +- Circuit parameters: `32` +- Classical head parameters: `9` +- Total trainable parameters: `41 <= 100` +- Evaluation noise probability: `0.04` + +## Data + +A deterministic 8x8 MNIST-like 3-vs-8 binary dataset is generated locally. The full pipeline can be swapped to official MNIST arrays by replacing `make_mnist_like_binary_dataset` while keeping the same compressed 8-feature contract. + +| split | samples | class_0_digit3 | class_1_digit8 | +| --- | --- | --- | --- | +| train | 72 | 36 | 36 | +| val | 24 | 12 | 12 | +| test | 24 | 12 | 12 | + +## Test Metrics + +| model | accuracy | f1 | auc | log_loss | +| --- | --- | --- | --- | --- | +| logistic_8feature | 0.791667 | 0.761905 | 0.923611 | 0.373309 | +| vqc_clean | 0.625 | 0.666667 | 0.75 | 0.668958 | +| vqc_noise_aware | 0.625 | 0.64 | 0.583333 | 0.693091 | + +## Noise Robustness + +| model | noise_prob | accuracy | f1 | auc | +| --- | --- | --- | --- | --- | +| vqc_clean | 0.0 | 0.625 | 0.666667 | 0.75 | +| vqc_clean | 0.02 | 0.625 | 0.666667 | 0.611111 | +| vqc_clean | 0.04 | 0.5 | 0.5 | 0.444444 | +| vqc_clean | 0.08 | 0.291667 | 0.26087 | 0.201389 | +| vqc_noise_aware | 0.0 | 0.666667 | 0.692308 | 0.777778 | +| vqc_noise_aware | 0.02 | 0.583333 | 0.615385 | 0.763889 | +| vqc_noise_aware | 0.04 | 0.583333 | 0.615385 | 0.618056 | +| vqc_noise_aware | 0.08 | 0.333333 | 0.384615 | 0.4375 | + +## Quantum Resources + +| model | qubits | depth | total_params | 1q | 2q | shots | backend | +| --- | --- | --- | --- | --- | --- | --- | --- | +| logistic_8feature | 0 | 0 | 9 | 0 | 0 | 0 | classical | +| vqc_clean | 8 | 28 | 41 | 56 | 21 | 256 | local_statevector | +| vqc_noise_aware | 8 | 28 | 41 | 56 | 21 | 256 | local_statevector_noise_aware | + +## Why Noise-Aware Training + +The clean VQC is optimized for ideal statevector features. The noise-aware VQC injects angle jitter plus readout/gate attenuation during training, so its head and variational parameters see the same distribution shift that appears during noisy evaluation. This is intentionally small enough for the 8-qubit/100-parameter professional-track constraint. diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\resource_table.csv" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\resource_table.csv" new file mode 100644 index 0000000..0a2f6dc --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\resource_table.csv" @@ -0,0 +1,4 @@ +model,qubits,depth,circuit_params,head_params,total_params,one_qubit_gates,two_qubit_gates,shots,backend,train_time_sec,infer_time_sec +logistic_8feature,0,0,0,9,9,0,0,0,classical,0.02246,0.00014 +vqc_clean,8,28,32,9,41,56,21,256,local_statevector,11.472344,0.378983 +vqc_noise_aware,8,28,32,9,41,56,21,256,local_statevector_noise_aware,12.156496,0.435632 diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\robustness.csv" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\robustness.csv" new file mode 100644 index 0000000..6fa08d3 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\robustness.csv" @@ -0,0 +1,9 @@ +model,noise_prob,accuracy,precision,recall,f1,auc,log_loss,tp,tn,fp,fn +vqc_clean,0.0,0.625,0.6,0.75,0.666667,0.75,0.668958,9.0,6.0,6.0,3.0 +vqc_clean,0.02,0.625,0.6,0.75,0.666667,0.611111,0.689026,9.0,6.0,6.0,3.0 +vqc_clean,0.04,0.5,0.5,0.5,0.5,0.444444,0.694336,6.0,6.0,6.0,6.0 +vqc_clean,0.08,0.291667,0.272727,0.25,0.26087,0.201389,0.696417,3.0,4.0,8.0,9.0 +vqc_noise_aware,0.0,0.666667,0.642857,0.75,0.692308,0.777778,0.690783,9.0,7.0,5.0,3.0 +vqc_noise_aware,0.02,0.583333,0.571429,0.666667,0.615385,0.763889,0.692545,8.0,6.0,6.0,4.0 +vqc_noise_aware,0.04,0.583333,0.571429,0.666667,0.615385,0.618056,0.693022,8.0,6.0,6.0,4.0 +vqc_noise_aware,0.08,0.333333,0.357143,0.416667,0.384615,0.4375,0.693166,5.0,3.0,9.0,7.0 diff --git "a/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\split_summary.json" "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\split_summary.json" new file mode 100644 index 0000000..daa881c --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\results\\quick_demo\\split_summary.json" @@ -0,0 +1,17 @@ +{ + "test": { + "class_0": 12, + "class_1": 12, + "samples": 24 + }, + "train": { + "class_0": 36, + "class_1": 36, + "samples": 72 + }, + "val": { + "class_0": 12, + "class_1": 12, + "samples": 24 + } +} \ No newline at end of file diff --git "a/contest2\\CCF2026_Professional_MNIST\\scripts\\package_submission.py" "b/contest2\\CCF2026_Professional_MNIST\\scripts\\package_submission.py" new file mode 100644 index 0000000..6ca13d3 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\scripts\\package_submission.py" @@ -0,0 +1,34 @@ +import shutil +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DIST = ROOT / "dist" +ZIP_PATH = DIST / "CCF2026_Professional_MNIST_submission.zip" + + +def main() -> None: + DIST.mkdir(parents=True, exist_ok=True) + if ZIP_PATH.exists(): + ZIP_PATH.unlink() + include_dirs = ["ccf2026_mnist_qml", "scripts", "tests", "results"] + include_files = ["README.md", "requirements.txt"] + with zipfile.ZipFile(ZIP_PATH, "w", zipfile.ZIP_DEFLATED) as archive: + for filename in include_files: + path = ROOT / filename + if path.exists(): + archive.write(path, path.relative_to(ROOT)) + for dirname in include_dirs: + path = ROOT / dirname + if not path.exists(): + continue + for item in path.rglob("*"): + if item.is_file() and "__pycache__" not in item.parts: + archive.write(item, item.relative_to(ROOT)) + print(ZIP_PATH) + + +if __name__ == "__main__": + main() + diff --git "a/contest2\\CCF2026_Professional_MNIST\\scripts\\run_quick_demo.py" "b/contest2\\CCF2026_Professional_MNIST\\scripts\\run_quick_demo.py" new file mode 100644 index 0000000..847a157 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\scripts\\run_quick_demo.py" @@ -0,0 +1,34 @@ +import argparse +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from ccf2026_mnist_qml import ExperimentConfig, run_experiment + + +def main() -> None: + parser = argparse.ArgumentParser(description="Run the CCF2026 professional MNIST QML demo.") + parser.add_argument("--output-dir", type=Path, default=ROOT / "results" / "quick_demo") + parser.add_argument("--samples", type=int, default=120) + parser.add_argument("--epochs", type=int, default=12) + parser.add_argument("--shots", type=int, default=512) + parser.add_argument("--seed", type=int, default=2020) + args = parser.parse_args() + config = ExperimentConfig( + n_samples=args.samples, + vqc_epochs=args.epochs, + shots=args.shots, + seed=args.seed, + ) + result = run_experiment(args.output_dir, config) + print(f"Output: {args.output_dir}") + print(f"Models: {len(result['resources'])}") + print(f"Report: {args.output_dir / 'report.md'}") + + +if __name__ == "__main__": + main() diff --git "a/contest2\\CCF2026_Professional_MNIST\\tests\\test_smoke.py" "b/contest2\\CCF2026_Professional_MNIST\\tests\\test_smoke.py" new file mode 100644 index 0000000..0f39536 --- /dev/null +++ "b/contest2\\CCF2026_Professional_MNIST\\tests\\test_smoke.py" @@ -0,0 +1,36 @@ +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from ccf2026_mnist_qml import ExperimentConfig, run_experiment + + +class SmokeTest(unittest.TestCase): + def test_experiment_writes_submission_outputs(self) -> None: + tmp = Path(tempfile.mkdtemp(prefix="ccf2026_mnist_")) + try: + config = ExperimentConfig(n_samples=32, vqc_epochs=2, shots=64, seed=9) + result = run_experiment(tmp, config) + self.assertTrue((tmp / "metrics.csv").exists()) + self.assertTrue((tmp / "resource_table.csv").exists()) + self.assertTrue((tmp / "robustness.csv").exists()) + self.assertTrue((tmp / "report.md").exists()) + self.assertTrue((tmp / "figures" / "test_accuracy.svg").exists()) + vqc_rows = [row for row in result["resources"] if str(row["model"]).startswith("vqc")] + self.assertTrue(vqc_rows) + self.assertTrue(all(int(row["qubits"]) == 8 for row in vqc_rows)) + self.assertTrue(all(int(row["total_params"]) <= 100 for row in vqc_rows)) + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + unittest.main() +