#!/usr/bin/env python # # file: run_experiment.py # # revision history: # 20260810 (am): initial version for workshop lab 02 # # Run Lab 02 experiments from the command line. # # The notebook walks through the ideas; this script lets you keep going # afterwards. Any run can be pointed at your own IMLD csv file and can # write its results, figures and summary into an output directory. # # examples: # # # classical against quantum on the same split -- the lab's question # python run_experiment.py --compare --output out/moons # # # the segment 2 headline: accuracy tracks the encoded range width # python run_experiment.py --scaler-study # # # bring your own data # python run_experiment.py --dataset-path data/imld_two_spirals.csv \ # --compare --output out/spirals # # # bring your own train AND evaluation sets. with --eval-path there is # # no split: the whole dataset trains and this file does the scoring. # python run_experiment.py --dataset-path data/my_train.csv \ # --eval-path data/my_eval.csv --compare --output out/mine # # # one quantum kernel run, with a decision boundary # python run_experiment.py --method qsvm --scaler minmax --output out/good # python run_experiment.py --method qsvm --scaler standard --output out/bad # # # what do finite shots cost? # python run_experiment.py --kernel-backend sampled --shots 512 # # # add device noise # python run_experiment.py --kernel-backend noisy # # # path b, and the naive version it beats # python run_experiment.py --method vqc --layers 2 --maxiter 300 # python run_experiment.py --method vqc_naive --maxiter 200 # # # where quantum actually wins # python run_experiment.py --dataset ad_hoc --scaler none --compare # # # sweep any parameter # python run_experiment.py --sweep reps --values 1,2,3 # python run_experiment.py --sweep dataset --values two_moons,yin_yang # # # reuse a saved config # python run_experiment.py --config configs/qsvm_moons.json #------------------------------------------------------------------------------ # import system modules # import argparse import json import sys from pathlib import Path # make the package importable when run from anywhere # sys.path.insert(0, str(Path(__file__).resolve().parent)) # import qmllab modules # from qmllab import data, experiments # noqa: E402 #------------------------------------------------------------------------------ # # functions are listed here # #------------------------------------------------------------------------------ def build_parser(): """ function: build_parser arguments: none return: a configured ArgumentParser description: Defines the command line. Every experiment knob is exposed as a flag, and any flag overrides the same key in a --config file. """ # describe the script, keeping the examples readable # parser = argparse.ArgumentParser( description="Lab 02 experiment runner", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) # where the data comes from # parser.add_argument("--config", help="json config file; flags override it") parser.add_argument("--dataset", choices=data.GENERATORS + ["ad_hoc"], help="a built-in dataset name") parser.add_argument("--dataset-path", dest="dataset_path", help="an IMLD csv file to use instead") parser.add_argument("--eval-path", dest="eval_path", help="an IMLD csv holding a separate evaluation " "set; when given there is no train/test " "split -- the whole dataset trains and this " "file scores") parser.add_argument("--eval-n-samples", dest="eval_n_samples", type=int, help="subsample the evaluation set") parser.add_argument("--scaler", choices=list(data.SCALERS)) parser.add_argument("--n-samples", dest="n_samples", type=int) parser.add_argument("--test-size", dest="test_size", type=float, help="held-out fraction; ignored with --eval-path") parser.add_argument("--random-state", dest="random_state", type=int) # what to run # parser.add_argument("--method", choices=experiments.METHODS) parser.add_argument("--kernel-backend", dest="kernel_backend", choices=["exact", "sampled", "aer", "noisy"], help="exact=linear algebra, sampled=finite shots, " "aer=simulator, noisy=simulator with device " "noise") parser.add_argument("--feature-map", dest="feature_map", choices=["zz", "z", "pauli"]) parser.add_argument("--reps", type=int, help="feature map repetitions") parser.add_argument("--entanglement", choices=["full", "linear", "circular"]) parser.add_argument("--shots", type=int) parser.add_argument("--layers", type=int, help="data re-uploading layers (path b)") parser.add_argument("--maxiter", type=int) parser.add_argument("--optimizer", choices=["COBYLA", "SPSA", "ADAM"]) # modes # parser.add_argument("--compare", action="store_true", help="classical against quantum on the same split") parser.add_argument("--with-vqc", dest="with_vqc", action="store_true", help="include a variational circuit in --compare") parser.add_argument("--scaler-study", dest="scaler_study", action="store_true", help="run every scaler and show why encoding decides") parser.add_argument("--sweep", help="parameter name to vary") parser.add_argument("--values", help="comma-separated values for --sweep") # where the results go # parser.add_argument("--output", "-o", help="directory for json, figures and summaries") parser.add_argument("--no-plot", dest="no_plot", action="store_true", help="skip the decision boundary figure") # exit gracefully # return parser # # end of function def build_config(args): """ function: build_config arguments: args: the parsed command line return: a config dict for the experiments module description: Starts from a --config file when one was given, then lets any explicitly supplied flag override it. Mode flags are excluded. """ # start from the config file when there is one # cfg = {} if args.config: cfg = json.loads(Path(args.config).read_text()) # every setting that is not a mode flag can override the file # modes = {"config", "compare", "with_vqc", "scaler_study", "sweep", "values", "output", "no_plot"} for key, value in vars(args).items(): if value is not None and key not in modes: if not isinstance(value, bool): cfg[key] = value # exit gracefully # return cfg # # end of function def main(): """ function: main arguments: none return: none description: Parses the command line and dispatches to the requested mode. """ # parse the command line # parser = build_parser() args = parser.parse_args() cfg = build_config(args) plot = not args.no_plot # classical against quantum on the same split # if args.compare: methods = ["classical", "qsvm"] if args.with_vqc: methods.append("vqc") experiments.compare({**cfg, "methods": methods}, output_dir=args.output) # every scaler, ordered by the width of the range it produces # elif args.scaler_study: rows = experiments.scaler_study(cfg, output_dir=args.output) print(f"\n{'scaler':10s} {'width':>7s} {'off-diag':>9s} " f"{'support':>8s} {'test acc':>9s}") for name in sorted(rows, key=lambda n: rows[n]["width"]): row = rows[name] offdiag = row["mean_offdiag"] offdiag = " " * 9 if offdiag is None else f"{offdiag:9.3f}" support = row["n_support"] support = "-" if support is None else str(support) print(f"{name:10s} {row['width']:7.2f} {offdiag} " f"{support:>8s} {row['test_acc']:9.3f}") print("\nNarrower encoded range -> better quantum kernel. The ZZ " "map's angle is a\nproduct of features, so a wide range makes " "the kernel oscillate faster\nthan the data varies.") # vary one parameter over a list of values # elif args.sweep: if not args.values: parser.error("--sweep requires --values") casts = {"reps": int, "layers": int, "shots": int, "maxiter": int, "n_samples": int, "random_state": int} values = [casts.get(args.sweep, str)(v) for v in args.values.split(",")] experiments.sweep(args.sweep, values, cfg, output_dir=args.output) # a single run # else: experiments.run(cfg, output_dir=args.output, plot=plot) # tell the user where everything landed # if args.output: print("\nresults written to", args.output) # # end of function #------------------------------------------------------------------------------ # # the main program starts here # #------------------------------------------------------------------------------ if __name__ == "__main__": main() # # end of file