A hybrid quantum–classical project exploring how a quantum Bayesian learner's performance depends on variational circuit complexity under realistic NISQ hardware constraints.
Reference: Pothos, E. M., & Chater, N. (2002). A simplicity principle in unsupervised human categorization. Cognitive Psychology, 45, 45–85.
- Repository Structure
- Experimental Pipeline
- How to Run Everything
- Output Structure
- Notebooks
- Expected Results
- Reproduction Steps for Paper Figures
qb-learner-compression/
│
├── src/ # Core source code
│ ├── data.py # Dataset generation (Pothos-Chater style)
│ ├── channels.py # Quantum evidence channels (Kraus operators)
│ ├── ansatz.py # Variational circuit construction
│ ├── learner.py # Core learning logic & loss computation
│ ├── train_baseline.py # Baseline training script
│ ├── train_compressed.py # Compressed training with pruning
│ ├── evaluate_model.py # Model evaluation & comparison
│ ├── run_experiment.py # Single experiment launcher
│ ├── run_all_experiments.py # Batch experiment runner
│ ├── transpile_utils.py # Gate counting & transpilation
│ ├── plots.py # Visualization utilities
│ └── logging_utils.py # Logging & file I/O utilities
│
├── experiments/ # YAML experiment configurations
│ ├── baseline_pothos_large.yaml
│ ├── compressed_pothos_large.yaml
│ └── ... (various configs)
│
├── notebooks/ # Jupyter notebooks for analysis
│ ├── sanity_checks.ipynb # Component verification
│ ├── visualize_channels.ipynb # Channel visualization
│ └── visualize_ansatz.ipynb # Circuit structure visualization
│
├── results/ # All experiment outputs
│ ├── logs/ # Training logs (CSV)
│ ├── runs/ # Individual run directories
│ ├── experiments_<timestamp>/ # Batch experiment results
│ │ ├── experiments/ # Individual experiment outputs
│ │ └── summary/ # Aggregated summaries
│ └── figures/ # Generated plots
│
├── README.md # This file
└── requirements.txt # Python dependencies
Purpose: Generates Pothos-Chater-style toy categorization datasets.
What it does:
- Creates three dataset sizes: small (8 points), medium (20 points), large (40 points)
- Each dataset has two Gaussian clusters in 2D feature space
- Features are normalized to [0, 1] for quantum channel encoding
Key Functions:
get_pothos_chater_small()→ (X, y) with 8 samplesget_pothos_chater_medium()→ (X, y) with 20 samplesget_pothos_chater_large()→ (X, y) with 40 samples (recommended for paper)get_toy_dataset(name)→ Generic entry point
Integration:
- Called by training scripts (
train_baseline.py,train_compressed.py) - Used in notebooks for visualization
- Automatically loaded via YAML configs in
run_experiment.py
Pipeline Position: First step in experimental pipeline (data generation)
Purpose: Implements quantum evidence channels (Kraus operators) that update the learner's belief state.
What it does:
- Builds Kraus operators from stimulus features
- Maps feature values to channel strength parameters
- Supports multiple channel types: amplitude damping, phase damping, rotation, projective update
Key Functions:
evidence_amplitude_damping(x, strength)→ Amplitude-damping channelevidence_projective_update(x, strength)→ Bayesian-like projective update (default)build_evidence_channel(x, kind, strength)→ Unified channel selectorcompute_strength_from_stimulus(x, method)→ Feature-to-strength mappingapply_channel_to_density_matrix(rho, channel)→ Apply channel to state
How it works:
- Takes stimulus features
x(1D array) - Computes channel strength from features (default: mean of features)
- Builds Kraus operators based on strength
- Returns Qiskit
Krauschannel object
Integration:
- Called by
learner.pyinapply_evidence_channel() - Used in notebooks for visualization
- Channel strength affects learning dynamics (higher = stronger evidence)
Pipeline Position: Applied during forward pass (after ansatz, before prediction)
Purpose: Constructs parameterized variational quantum circuits with masked entangling gates.
What it does:
- Builds layered circuits: single-qubit rotations (RX, RZ) + entangling blocks
- Supports two entangler types:
- HEA-style: CX, RY, CX (stronger entanglement)
- Heisenberg: RXX, RYY, RZZ (original)
- Uses binary masks to selectively enable/disable entanglers (compression mechanism)
Key Functions:
build_ansatz(n_qubits, depth, theta, mask, pairs)→ Constructs circuitinit_random_theta(n_qubits, depth, n_edges, scale)→ Random parameter initializationinit_full_mask(depth, n_edges)→ All gates active (baseline)init_sparse_mask(depth, n_edges, sparsity)→ Random sparse maskcount_parameters(theta, mask, n_qubits)→ Counts active parametersget_default_pairs(n_qubits)→ Linear chain connectivity [(0,1), (1,2), ...]
Parameter Structure:
theta: shape(depth, max(n_qubits, n_edges), 5)theta[d, q, 0]: RX angle for qubitqat depthdtheta[d, q, 1]: RZ angle for qubitqat depthdtheta[d, k, 2:5]: Entangling parameters for edgekat depthd
mask: shape(depth, n_edges)binary arraymask[d, k] = 1→ entangler onpairs[k]at depthdis activemask[d, k] = 0→ entangler is pruned (compressed mode)
Integration:
- Called by
learner.pyinforward_loss()to build circuit - Used by
train_baseline.pyandtrain_compressed.pyfor training - Mask is pruned in
train_compressed.pyviagreedy_prune()
Pipeline Position: Circuit construction (before transpilation and loss computation)
Purpose: Core learning logic: forward pass, loss computation, and prediction.
What it does:
- Implements the quantum Bayesian learning workflow:
- Initialize belief state (slightly biased toward |0...0⟩)
- Apply feature encoding (RY, RZ rotations)
- Apply ansatz (unitary transformation)
- Apply evidence channel (belief update)
- Make prediction (Z expectation + sigmoid)
- Compute loss (binary cross-entropy + λ × gate count)
Key Functions:
forward_loss(theta, mask, X, y, lam, ...)→ Computes hybrid loss- Returns:
{"total_loss", "ce_loss", "two_q_cost", "avg_pred", "preds"}
- Returns:
predict_label(rho, readout_alpha)→ Class probability from quantum statepredict_hard(rho, threshold)→ Hard class label (0 or 1)init_belief(n_qubits)→ Initial belief state (not maximally mixed)apply_evidence_channel(rho, x, strength, qargs)→ Apply channel to stateapply_unitary(rho, qc)→ Apply circuit to density matrix
Loss Function:
Loss = CE_loss + λ × two_qubit_gate_count
CE_loss: Binary cross-entropy between predictions and labelstwo_qubit_gate_count: Post-transpile entangling gate count (compression metric)λ: Regularization strength (controls accuracy vs. complexity trade-off)
Integration:
- Called by training scripts in optimization loop
- Used by
evaluate_model.pyfor evaluation transpile_utils.pyprovides gate counting
Pipeline Position: Core of training loop (forward pass + loss computation)
Purpose: Trains baseline model with all entangling gates active (no compression).
What it does:
- Optimizes only continuous parameters
theta(mask fixed at all 1s) - Uses finite-difference gradient descent or Adam optimizer
- Saves training history, parameters, and plots
Key Functions:
main(n_qubits, depth, n_iterations, lr, lam, ...)→ Main training functionfinite_diff_gradient(loss_fn, theta, h)→ Gradient computationAdamOptimizer→ Adam optimizer class
Outputs:
results/runs/baseline_<timestamp>/training_history.csv→ Loss, accuracy, gate count per iterationparams_final.npz→ Finalthetaparametersloss_history.npz→ Arrays for plottingfinal_metrics.json→ Final accuracy, loss, gate countfigures/→ Training curves, Pareto plots
Integration:
- Called by
run_experiment.pywhenmode: "baseline" - Called by
run_all_experiments.pyfor batch runs - Uses
learner.pyfor forward pass,transpile_utils.pyfor gate counting
Pipeline Position: Training execution (baseline mode)
Usage:
python -m src.train_baseline --iterations 100 --n_qubits 2 --depth 3 --lr 0.01 --lam 0.1Purpose: Trains compressed model with greedy pruning of entangling gates.
What it does:
- Starts with all entanglers active (full mask)
- Alternates between:
- Optimizing
thetavia gradient descent - Pruning entanglers that don't significantly impact loss (every
prune_everyiterations)
- Optimizing
- Pruning criterion: disable gate if loss increase ≤
tolerance
Key Functions:
main(n_qubits, depth, n_iterations, lr, lam, prune_every, tolerance, ...)→ Main traininggreedy_prune(theta, mask, X, y, tolerance, ...)→ Pruning logiccompute_mask_sparsity(mask)→ Fraction of active entanglers
Pruning Algorithm:
- For each active entangler
(d, k):- Temporarily set
mask[d, k] = 0 - Compute loss with gate disabled
- If
loss_increase ≤ toleranceANDce_loss_increase ≤ tolerance:- Permanently disable gate
- Recompute baseline loss
- Temporarily set
- Continue until no more gates can be pruned
Outputs:
results/runs/compressed_<timestamp>/training_history.csv→ Includesmask_sparsitycolumnparams_final.npz→ Finalthetaandmaskmask_history.npz→ Mask evolution over trainingfigures/→ Includes mask heatmaps
Integration:
- Called by
run_experiment.pywhenmode: "compressed" - Called by
run_all_experiments.pyfor batch runs - Uses
learner.pyfor forward pass,transpile_utils.pyfor gate counting
Pipeline Position: Training execution (compressed mode)
Usage:
python -m src.train_compressed --iterations 100 --prune_every 20 --tolerance 0.01Purpose: Evaluates trained models and generates comparison plots.
What it does:
- Loads baseline and compressed models from
results/ - Recomputes accuracy by re-running predictions
- Generates comparison plots (loss curves, mask visualizations, Pareto fronts)
Key Functions:
load_training_logs(results_dir)→ Load CSV logsload_compressed_model(results_dir)→ Loadthetaandmaskcompute_accuracy_from_model(theta, mask, ...)→ Re-run predictionsgenerate_comparison_plots(...)→ Create visualization plots
Outputs:
results/figures/eval_*.png→ Comparison plots
Integration:
- Called manually after training
- Uses
learner.pyfor predictions,plots.pyfor visualization
Pipeline Position: Post-training evaluation
Usage:
python -m src.evaluate_modelPurpose: One-click launcher for single experiments from YAML configs.
What it does:
- Loads YAML configuration file
- Validates config structure
- Dispatches to
train_baseline.pyortrain_compressed.pybased onmode
Integration:
- Called manually or by scripts
- Uses
train_baseline.pyortrain_compressed.pyfor actual training
Pipeline Position: Experiment orchestration
Usage:
python -m src.run_experiment --config experiments/baseline_pothos_large.yamlPurpose: Batch runner for multiple experiments with aggregation.
What it does:
- Discovers all YAML configs in
experiments/ - Runs each experiment sequentially
- Aggregates results into summary CSV and comparison plots
- Supports dry-run, limiting, tagging, and skip-failed modes
Key Functions:
discover_configs(experiments_dir)→ Find all YAML filesnormalize_config(config)→ Apply defaults and validaterun_single_experiment(config, ...)→ Run one experimentaggregate_results(all_summaries, summary_dir)→ Generate summaries
Outputs:
results/experiments_<tag>_<timestamp>/experiments/→ Individual experiment outputssummary/→ Aggregated CSV and plotsall_experiments_summary.csv→ Table of all resultsaccuracy_vs_gatecount.png→ Scatter plotbaseline_vs_compressed.png→ Bar chart comparisonexperiment_methods_comparison.png→ Method comparison
Integration:
- Called manually for batch runs
- Uses
train_baseline.pyandtrain_compressed.pyfor training - Uses
plots.pyfor aggregation plots
Pipeline Position: Batch experiment orchestration
Usage:
# Run all experiments
python -m src.run_all_experiments
# Dry run (list configs)
python -m src.run_all_experiments --dry-run
# Run with tag
python -m src.run_all_experiments --tag paper_results
# Limit to first 2 experiments
python -m src.run_all_experiments --limit 2
# Skip failed experiments
python -m src.run_all_experiments --skip_failedPurpose: Transpiles circuits and counts two-qubit gates (compression metric).
What it does:
- Transpiles circuits to match hardware constraints (coupling map, basis gates)
- Counts entangling two-qubit gates (CNOT, CZ, RXX, RYY, RZZ, etc.)
- Provides detailed gate statistics
Key Functions:
transpile_and_count_2q(circuit, backend, ...)→ Main function- Returns:
(transpiled_circuit, two_qubit_gate_count)
- Returns:
count_entangling_gates(circuit)→ Count only entangling gatescount_all_2q_gates(circuit)→ Count all two-qubit gatessummarize_transpile(circuit, backend, verbose)→ Detailed statistics
Why This Matters:
- Two-qubit gate count is the compression metric
- Post-transpile count reflects hardware reality (not just circuit structure)
- Lower gate count = better compression = lower hardware cost
Integration:
- Called by
learner.pyinforward_loss()to compute gate count - Used by training scripts for loss computation
- Cached by mask hash in
learner.pyfor efficiency
Pipeline Position: Gate counting (during loss computation)
Purpose: Visualization utilities for training curves, masks, and comparisons.
Key Functions:
plot_all_curves_from_history(history, prefix, output_dir)→ Training curvesplot_mask_heatmap(mask, title, fname, output_dir)→ Mask visualizationplot_pareto_from_runs(runs, output_dir, fname)→ Pareto front plotscompare_methods_bar(method_results, fname, output_dir)→ Bar chart comparison
Integration:
- Called by training scripts to save plots
- Called by
run_all_experiments.pyfor aggregation plots - Called by
evaluate_model.pyfor comparison plots
Purpose: Utilities for saving training logs, parameters, and metrics.
Key Functions:
save_training_history(run_dir, df_history)→ Save CSV logsave_parameters(run_dir, theta, mask)→ Save NPZ filessave_final_metrics(run_dir, metrics)→ Save JSON metricstimestamp_str()→ Generate timestamp strings
Integration:
- Called by training scripts to save outputs
- Used throughout pipeline for file I/O
Each YAML file defines a single experiment:
experiment_name: "baseline_pothos_large"
mode: "baseline" # or "compressed"
n_qubits: 2
depth: 3
n_iterations: 150
lr: 0.05
lam: 0.01
dataset_name: "pothos_chater_large"
channel_strength: 0.6
seed: 42
# Compressed-specific (only if mode == "compressed")
prune_every: 120
tolerance: 0.01Available Configs:
baseline_pothos_large.yaml→ Baseline with large datasetcompressed_pothos_large.yaml→ Compressed with large dataset- Various variants (different hyperparameters)
Integration:
- Loaded by
run_experiment.pyfor single runs - Discovered by
run_all_experiments.pyfor batch runs
Module: src/data.py
How it works:
- Defines two category prototypes in 2D space:
- Category A: centered at
[0.23, 0.27] - Category B: centered at
[0.77, 0.73]
- Category A: centered at
- Samples points around each prototype using Gaussian noise:
- Small: 4 points per category (std=0.07)
- Medium: 10 points per category (std=0.10)
- Large: 20 points per category (std=0.11) ← Recommended for paper
- Features are clipped to [0, 1] for quantum channel encoding
Recommended Dataset:
- Large (40 points) is recommended for the paper
- Provides sufficient data for meaningful learning while remaining computationally tractable
Usage:
from src.data import get_toy_dataset
X, y = get_toy_dataset("pothos_chater_large")
# X: shape (40, 2), y: shape (40,)Module: src/ansatz.py
How it works:
- Single-qubit rotations: Each layer applies RX and RZ to all qubits
- Entangling blocks: HEA-style (CX, RY, CX) or Heisenberg (RXX, RYY, RZZ)
- Masking: Binary mask controls which entanglers are active
mask[d, k] = 1→ entangler on edgekat depthdis activemask[d, k] = 0→ entangler is pruned (compressed mode)
Circuit Structure:
Layer 0: [RX, RZ] on all qubits → Entanglers (masked) → [RX, RZ] on all qubits
Layer 1: [RX, RZ] on all qubits → Entanglers (masked) → [RX, RZ] on all qubits
Layer 2: [RX, RZ] on all qubits → Entanglers (masked) → [RX, RZ] on all qubits
Compression Mechanism:
- Mask starts as all 1s (all gates active)
- During training, mask is pruned (gates set to 0)
- Pruned gates are not included in circuit → lower gate count
Usage:
from src.ansatz import build_ansatz, init_random_theta, init_full_mask, get_default_pairs
n_qubits = 2
depth = 3
pairs = get_default_pairs(n_qubits) # [(0, 1)]
theta = init_random_theta(n_qubits, depth, len(pairs))
mask = init_full_mask(depth, len(pairs))
qc = build_ansatz(n_qubits, depth, theta, mask, pairs)Module: src/channels.py
How it works:
- Takes stimulus features
x(1D array, e.g.,[0.3, 0.7]) - Computes channel strength from features:
- Default method:
strength = strength_max * (0.5 + 0.5 * (mean(x) - 0.5)) - Maps feature mean to strength in range
[0.25 * max, 0.75 * max]
- Default method:
- Builds Kraus operators based on strength:
- Amplitude damping: Energy dissipation (default in older code)
- Projective update: Bayesian-like soft measurement (default in current code)
- Returns Qiskit
Krauschannel
Channel Strength Effects:
- Higher strength → stronger evidence update → faster learning
- Lower strength → weaker update → slower learning
- Default:
strength_max = 0.4(configurable via YAML)
Usage:
from src.channels import build_evidence_channel
from qiskit.quantum_info import DensityMatrix
x = np.array([0.3, 0.7])
channel = build_evidence_channel(x, kind="projective", strength=0.4)
rho = DensityMatrix([[0.5, 0.0], [0.0, 0.5]])
rho_new = rho.evolve(channel)Module: src/learner.py
How it works:
- Forward Pass (per sample):
rho_0 = init_belief(n_qubits) # Initial state (biased toward |0...0⟩) rho_1 = apply_feature_encoding(rho_0, x) # RY, RZ rotations from features rho_2 = apply_unitary(rho_1, ansatz) # Variational circuit rho_3 = apply_evidence_channel(rho_2, x) # Evidence update prob = predict_label(rho_3) # Z expectation + sigmoid - Loss Computation:
CE_loss = mean(-log(p_i) if y_i==1 else -log(1-p_i)) gate_count = transpile_and_count_2q(ansatz)[1] total_loss = CE_loss + λ * gate_count - Optimization:
- Finite-difference gradient:
grad = (loss(theta+h) - loss(theta)) / h - Parameter update:
theta = theta - lr * grad - For compressed mode: periodically prune mask
- Finite-difference gradient:
Pruning (Compressed Mode):
- Every
prune_everyiterations:- For each active entangler:
- Temporarily disable it
- Compute loss increase
- If increase ≤
tolerance: permanently disable
- Continue until no more gates can be pruned
- For each active entangler:
Prediction:
- Uses multi-qubit Z and X expectations:
logit = α * (0.6*<Z_0> + 0.4*<Z_1> + 0.3*<X_0> + 0.2*<X_1>) prob = sigmoid(logit) readout_alphacontrols nonlinearity (default: 4.0)
Usage:
from src.learner import forward_loss
result = forward_loss(
theta=theta,
mask=mask,
X=X_train,
y=y_train,
lam=0.1,
n_qubits=2,
depth=3,
channel_strength=0.4
)
# result["total_loss"], result["ce_loss"], result["two_q_cost"], result["preds"]Workflow:
- Load dataset
- Initialize
theta(random) andmask(all 1s) - For each iteration:
- Compute loss via
forward_loss() - Compute gradient via finite differences
- Update
thetavia gradient descent - Log metrics (loss, accuracy, gate count)
- Compute loss via
- Save outputs (history, parameters, plots)
Key Parameters:
n_iterations: Number of training steps (default: 100)lr: Learning rate (default: 0.01)lam: Regularization strength (default: 0.1)optimizer_type: "finite_diff" or "adam"
Outputs:
results/runs/baseline_<timestamp>/training_history.csvresults/runs/baseline_<timestamp>/params_final.npzresults/runs/baseline_<timestamp>/figures/
Workflow:
- Load dataset
- Initialize
theta(random) andmask(all 1s) - For each iteration:
- Compute loss via
forward_loss() - Compute gradient via finite differences
- Update
thetavia gradient descent - Every
prune_everyiterations: Rungreedy_prune() - Log metrics (loss, accuracy, gate count, mask sparsity)
- Compute loss via
- Save outputs (history, parameters, mask history, plots)
Key Parameters:
prune_every: Pruning frequency (default: 20)tolerance: Maximum loss increase allowed when pruning (default: 0.01)
Outputs:
results/runs/compressed_<timestamp>/training_history.csv(includesmask_sparsity)results/runs/compressed_<timestamp>/params_final.npz(includesmask)results/runs/compressed_<timestamp>/mask_history.npzresults/runs/compressed_<timestamp>/figures/(includes mask heatmaps)
Module: src/evaluate_model.py
What it does:
- Loads training logs from
results/logs/ - Loads model parameters from
results/ - Recomputes accuracy by re-running predictions
- Generates comparison plots:
- Loss curves (baseline vs compressed)
- Mask sparsity over time
- Mask heatmaps
- Pareto front (CE loss vs gate count)
Usage:
python -m src.evaluate_modelOutputs:
results/figures/eval_loss_comparison.pngresults/figures/eval_compressed_mask_heatmap.pngresults/figures/eval_pareto_ce_loss_vs_cost.png
Module: src/transpile_utils.py
How it works:
- Takes circuit from
build_ansatz() - Transpiles to match hardware constraints (if backend provided):
- Coupling map (which qubit pairs can have gates)
- Basis gates (available gate set)
- Counts entangling two-qubit gates in transpiled circuit
- Returns count as compression metric
Why Post-Transpile Count:
- Pre-transpile count doesn't reflect hardware reality
- Hardware may require SWAP gates to route qubits
- Post-transpile count = actual hardware cost
Usage:
from src.transpile_utils import transpile_and_count_2q
transpiled, count = transpile_and_count_2q(circuit, backend=None)
# count: number of two-qubit gates# Clone repository (if applicable)
cd qb-learner-compression
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txtDependencies:
qiskit==1.1.0- Quantum circuit frameworkqiskit-aer==0.13.2- Quantum simulatorsnumpy,scipy- Numerical computingmatplotlib,pandas- Visualization and datatqdm- Progress barspyyaml- Configuration files
From YAML config:
python -m src.run_experiment --config experiments/baseline_pothos_large.yamlDirect training (baseline):
python -m src.train_baseline \
--n_qubits 2 \
--depth 3 \
--iterations 100 \
--lr 0.01 \
--lam 0.1 \
--dataset pothos_chater_largeDirect training (compressed):
python -m src.train_compressed \
--n_qubits 2 \
--depth 3 \
--iterations 100 \
--lr 0.01 \
--lam 0.1 \
--prune_every 20 \
--tolerance 0.01 \
--dataset pothos_chater_largeBasic usage:
python -m src.run_all_experimentsWith options:
# Dry run (list configs without running)
python -m src.run_all_experiments --dry-run
# Add tag to output directory
python -m src.run_all_experiments --tag final_large
# Limit to first 2 experiments
python -m src.run_all_experiments --limit 2
# Skip failed experiments and continue
python -m src.run_all_experiments --skip_failedOutput:
results/experiments_<tag>_<timestamp>/- Individual experiment outputs in
experiments/ - Aggregated summary in
summary/
- Individual experiment outputs in
python -m src.evaluate_modelPrerequisites:
- Baseline model:
results/baseline_final_theta.npyorresults/logs/baseline_log.csv - Compressed model:
results/compressed_final.npzorresults/logs/compressed_log.csv
Output:
- Comparison plots in
results/figures/eval_*.png
Location: results/runs/<run_name>/
Contents:
config.json→ Exact configuration used (saved automatically)training_history.csv→ Per-iteration metrics:iteration: Training steploss: Total loss (CE + λ × gate count)ce_loss: Cross-entropy loss onlyaccuracy: Classification accuracytwo_qubit_count: Post-transpile gate countmask_sparsity: (compressed only) Fraction of active entanglers
params_final.npz→ Final parameters:theta: Final parameter tensormask: (compressed only) Final mask
loss_history.npz→ Arrays for plotting:loss: Total loss arrayce_loss: CE loss arraygate_cost: Gate count array
mask_history.npz→ (compressed only) Mask evolution:- Shape:
(n_iterations, depth, n_edges)
- Shape:
final_metrics.json→ Final metrics:final_loss,final_ce_loss,final_accuracyfinal_two_qubit_countfinal_mask_sparsity(compressed only)
figures/→ Generated plots:*_loss.png→ Loss curves*_accuracy.png→ Accuracy curves*_gate_cost.png→ Gate count curves*_mask_heatmap.png→ (compressed only) Mask visualization*_pareto.png→ Pareto front plots
Location: results/experiments_<tag>_<timestamp>/summary/
Contents:
all_experiments_summary.csv→ Table of all experiments:- Columns:
experiment_name,mode,final_loss,final_accuracy,two_qubit_count,mask_sparsity, etc.
- Columns:
accuracy_vs_gatecount.png→ Scatter plot (accuracy vs gate count)baseline_vs_compressed.png→ Bar chart comparisonexperiment_methods_comparison.png→ Method comparison bar chart
For backward compatibility, some outputs are also saved to:
results/logs/baseline_log.csv→ Baseline training logresults/logs/compressed_log.csv→ Compressed training logresults/baseline_final_theta.npy→ Baseline final parametersresults/compressed_final.npz→ Compressed final parametersresults/figures/→ Legacy figure location
Purpose: Comprehensive verification of all system components.
What it tests:
- Environment: Imports and version checks
- Dataset: Loading and visualization
- Evidence Channels: Kraus operator construction, CPTP properties
- Density Matrix Updates: Channel application, trace preservation
- Ansatz Construction: Circuit building, parameter counting
- Forward Pass: End-to-end loss computation
- Transpilation: Gate counting accuracy
How to run:
# Start Jupyter
jupyter notebook notebooks/sanity_checks.ipynbWhy it matters:
- Ensures all components work correctly before running experiments
- Useful for debugging when things go wrong
- Verifies mathematical properties (CPTP, unitarity, etc.)
Purpose: Visualize quantum evidence channels and their effects.
What it shows:
- Kraus Operators: Structure and properties
- Channel Strength: How features map to strength parameters
- State Evolution: How channels update density matrices
- Damping Effects: Amplitude and phase damping visualization
How to run:
jupyter notebook notebooks/visualize_channels.ipynbWhy it matters:
- Understands how evidence channels encode stimulus information
- Visualizes the belief update mechanism
- Useful for paper figures showing channel effects
Purpose: Visualize variational circuit structure and mask effects.
What it shows:
- Circuit Structure: Gate decomposition, depth, connectivity
- Mask Visualization: Which entanglers are active/inactive
- Parameter Sensitivity: How parameters affect circuit output
- Transpilation Effects: How hardware constraints change circuits
How to run:
jupyter notebook notebooks/visualize_ansatz.ipynbWhy it matters:
- Understands circuit architecture
- Visualizes compression (mask sparsity)
- Useful for paper figures showing circuit structure
Expected Accuracy: ≈ 0.5
Why:
- Toy dataset is symmetric (two balanced clusters)
- Linear decision boundary on qubit-0 leads to 50/50 classification
- Nonlinear readout (multi-qubit Z/X expectations) helps but may not achieve perfect separation
Expected Gate Count: 6–9 two-qubit gates
Why:
- Depth 3, 1 edge (2 qubits) → 3 entanglers initially
- Transpilation may add SWAP gates → 6–9 total
Expected Loss:
- CE loss: ~1.5–2.0 (depends on dataset and hyperparameters)
- Total loss: CE loss + λ × gate count
Expected Accuracy: 0.0–0.5 (may decrease from baseline)
Why:
- Pruning removes entanglers → circuit becomes less expressive
- May reduce to pure single-qubit classifier → lower accuracy
- This is expected: goal is compression, not accuracy improvement
Expected Gate Count: 0–3 two-qubit gates
Why:
- Pruning removes unnecessary entanglers
- Ideally reduces to 0–3 gates (from 6–9)
- Compression ratio: up to 100% (if all gates pruned)
Expected Mask Sparsity: 0.0–0.5 (50–100% of gates pruned)
Why:
- Greedy pruning removes gates that don't significantly impact loss
- Tolerance controls how aggressive pruning is
This project demonstrates compression, not accuracy improvement.
- Goal 1: Show that circuits can be compressed (gate count reduction)
- Goal 2: Show trade-off between accuracy and gate count
- Goal 3: Demonstrate hardware-aware optimization (post-transpile counts)
Expected Findings:
- Compressed models achieve lower gate counts
- Accuracy may decrease (acceptable trade-off)
- Compression ratio: 50–100% (depending on tolerance)
Run batch experiments:
python -m src.run_all_experiments --tag paper_resultsThis will:
- Run all experiments in
experiments/ - Save results to
results/experiments_paper_results_<timestamp>/ - Generate summary CSV and comparison plots
Output location:
results/experiments_paper_results_<timestamp>/summary/all_experiments_summary.csvresults/experiments_paper_results_<timestamp>/summary/accuracy_vs_gatecount.pngresults/experiments_paper_results_<timestamp>/summary/baseline_vs_compressed.png
For a specific run:
# Run experiment
python -m src.run_experiment --config experiments/baseline_pothos_large.yaml
# Plots are automatically generated in:
# results/runs/baseline_<timestamp>/figures/Available plots:
baseline_loss.png→ Total loss curvebaseline_ce_loss.png→ CE loss curvebaseline_accuracy.png→ Accuracy curvebaseline_gate_cost.png→ Gate count curvebaseline_pareto.png→ Pareto front (if applicable)
After running experiments:
# Evaluate models (generates comparison plots)
python -m src.evaluate_model
# Or regenerate summary from existing results
python -m src.run_all_experiments --tag regenerate_summary-
Accuracy vs Gate Count Scatter:
- Source:
results/experiments_<tag>_<timestamp>/summary/accuracy_vs_gatecount.png - Shows trade-off between accuracy and compression
- Source:
-
Baseline vs Compressed Comparison:
- Source:
results/experiments_<tag>_<timestamp>/summary/baseline_vs_compressed.png - Bar chart comparing average metrics
- Source:
-
Training Curves:
- Source:
results/runs/<run_name>/figures/*_loss.png,*_accuracy.png - Shows training dynamics
- Source:
-
Mask Visualization:
- Source:
results/runs/compressed_<timestamp>/figures/compressed_mask_heatmap.png - Shows which gates were pruned
- Source:
-
Pareto Front:
- Source:
results/runs/<run_name>/figures/*_pareto.png - Shows accuracy vs gate count trade-off
- Source:
Key hyperparameters:
lam(λ): Controls accuracy vs. complexity trade-off- Higher λ → more compression, lower accuracy
- Lower λ → less compression, higher accuracy
tolerance: Pruning aggressiveness (compressed only)- Higher tolerance → more gates pruned
- Lower tolerance → fewer gates pruned
channel_strength: Evidence channel strength- Higher → stronger evidence updates
- Lower → weaker evidence updates
readout_alpha: Prediction nonlinearity- Higher → sharper predictions
- Lower → softer predictions
Low accuracy (< 0.3):
- Check if predictions are inverted (try flipping sign in
predict_label) - Increase
readout_alphafor stronger nonlinearity - Increase
channel_strengthfor stronger evidence updates
No compression (mask stays full):
- Decrease
tolerance(allows more aggressive pruning) - Increase
lam(stronger penalty on gate count) - Check if
prune_everyis too large (pruning happens too infrequently)
Training instability:
- Decrease learning rate
lr - Use Adam optimizer instead of finite differences
- Check gradient norms (should not be too small or too large)
If you use this code in your research, please cite:
Pothos, E. M., & Chater, N. (2002). A simplicity principle in unsupervised human categorization.
Cognitive Psychology, 45, 45–85.
Mohammad Zoraiz mz248@duke.edu