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'"
+ )
+ 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 0000000..c8a3cb1
Binary files /dev/null and "b/contest2\\CCF2026_Professional_MNIST\\dist\\CCF2026_Professional_MNIST_submission.zip" differ
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 @@
+
\ 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 @@
+
\ 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()
+