22import os
33import re
44
5- import matplotlib .pyplot as plt
6-
7- from src .pipeline import (
8- build_grover_circuit ,
9- optimal_iterations ,
10- run_ideal ,
11- run_noisy ,
12- extract_marked_probability ,
13- expected_success_probability ,
14- compute_probabilities ,
15- save_circuit_diagram ,
16- NOISE_MODEL ,
17- )
18-
195
206def _parse_search (value : str ) -> list [str ]:
7+ """Parse comma-separated binary search states into a list."""
218 parts = [s .strip () for s in value .split ("," )]
229 for p in parts :
2310 if not re .fullmatch (r"[01]+" , p ):
@@ -28,6 +15,7 @@ def _parse_search(value: str) -> list[str]:
2815
2916
3017def _validate_args (args : argparse .Namespace ) -> None :
18+ """Validate qubits count and lengths of search state(s)."""
3119 if args .qubits < 1 :
3220 raise ValueError ("--qubits must be >= 1" )
3321 for s in args .search :
@@ -43,6 +31,10 @@ def _plot_histogram(
4331 title : str ,
4432 filepath : str | None ,
4533) -> None :
34+ """Plot or save a probability histogram with marked states highlighted."""
35+ # Lazy import so `-h` stays instant
36+ import matplotlib .pyplot as plt
37+
4638 states = sorted (probs .keys ())
4739 values = [probs [s ] for s in states ]
4840
@@ -72,94 +64,145 @@ def _plot_histogram(
7264 pass
7365
7466
75- def main () -> None :
67+ def _print_header (args : argparse .Namespace , iters : int , label : str ) -> None :
68+ """Print run configuration banner."""
69+ print ()
70+ print ("=" * 60 )
71+ print (f" Grover's Search Algorithm" )
72+ print ("=" * 60 )
73+ print (f" Qubits: { args .qubits } " )
74+ print (f" Search state(s): |{ label } >" )
75+ if len (args .search ) > 1 :
76+ print (f" Marked count: { len (args .search )} " )
77+ print (f" Optimal iters: { iters } " )
78+ print (f" Shots: { args .shots } " )
79+ print (f" Noise model: { 'Enabled' if args .noise else 'Disabled' } " )
80+ print ("=" * 60 )
81+ print ()
82+
83+
84+ def _print_results (
85+ probs : dict ,
86+ marked_prob : float ,
87+ expected : float ,
88+ args : argparse .Namespace ,
89+ label : str ,
90+ ) -> None :
91+ """Print top-5 measurement results and marked-state probability."""
92+ sim_label = "Noisy" if args .noise else "Ideal"
93+ top = sorted (probs .items (), key = lambda x : - x [1 ])[:5 ]
94+ print (f" Measurement Results ({ sim_label } Simulator):" )
95+ print (f" { 'State' :>8} { 'Probability' :>12} " )
96+ print (f" { '-' * 8 } { '-' * 12 } " )
97+ for state , prob in top :
98+ marker = " <- marked" if state in args .search else ""
99+ print (f" { state :>8} { prob :>10.2%} { marker } " )
100+ print ()
101+ print (f" Marked state(s) |{ label } > probability: { marked_prob :.2%} " )
102+ print (f" Expected (ideal): { expected :.1%} " )
103+
104+
105+ def _build_parser () -> argparse .ArgumentParser :
106+ """Build and return the argument parser with all CLI flags."""
76107 parser = argparse .ArgumentParser (
77- description = "Grover's Search Algorithm - scalable, noise-aware implementation" ,
108+ description = "Grover's Search Algorithm -- dynamic oracle, M-state search, noise simulation" ,
109+ epilog = (
110+ "Examples:\n "
111+ " python main.py --qubits 3 --search 101\n "
112+ " python main.py --qubits 4 --search 1101 --noise --shots 4096\n "
113+ " python main.py --qubits 3 --search \" 000,111\" --save-plot outputs/hist.png\n "
114+ " python main.py --qubits 5 --search 10101 --save-circuit outputs/circ.png"
115+ ),
116+ formatter_class = argparse .RawDescriptionHelpFormatter ,
78117 )
79118 parser .add_argument (
80119 "--qubits" ,
81120 type = int ,
82121 default = 4 ,
83- help = "Number of qubits (default: 4)" ,
122+ help = "Number of qubits (n). Search space = 2^n states ( default: 4)" ,
84123 )
85124 parser .add_argument (
86125 "--search" ,
87126 type = str ,
88127 required = True ,
89- help = "Marked state(s) as binary string(s), comma -separated for multi-state, e.g. 1101 or 001,110 " ,
128+ help = "Binary state(s) to search for. Comma -separated for multi-state ( e.g. 1101 or 000,111) " ,
90129 )
91130 parser .add_argument (
92131 "--noise" ,
93132 action = "store_true" ,
94- help = "Run on a noisy simulated backend " ,
133+ help = "Enable noise model (depolarizing gate + readout errors) " ,
95134 )
96135 parser .add_argument (
97136 "--shots" ,
98137 type = int ,
99138 default = 8192 ,
100- help = "Number of measurement shots (default: 8192)" ,
139+ help = "Measurement repetitions. Higher = less sampling noise (default: 8192)" ,
101140 )
102141 parser .add_argument (
103142 "--save-plot" ,
104143 type = str ,
105144 default = None ,
106- help = "File path to save the probability histogram (e.g. outputs/histogram.png)" ,
145+ help = "Save probability histogram as PNG (e.g. outputs/histogram.png)" ,
107146 )
108147 parser .add_argument (
109148 "--save-circuit" ,
110149 type = str ,
111150 default = None ,
112- help = "File path to save the circuit diagram (e.g. outputs/circuit.txt )" ,
151+ help = "Save circuit diagram. .png = mpl style, .txt = ASCII (e.g. outputs/circuit.png )" ,
113152 )
153+ return parser
114154
155+
156+ def main () -> None :
157+ # ── Build argument parser ──
158+ parser = _build_parser ()
159+
160+ # ── Parse and validate arguments ──
115161 args = parser .parse_args ()
116162 args .search = _parse_search (args .search )
117163 _validate_args (args )
118164
165+
166+ # Lazy imports: Qiskit, Aer load here so `-h` stays instant
167+ from src .pipeline import (
168+ build_grover_circuit ,
169+ optimal_iterations ,
170+ run_ideal ,
171+ run_noisy ,
172+ extract_marked_probability ,
173+ expected_success_probability ,
174+ compute_probabilities ,
175+ save_circuit_diagram ,
176+ NOISE_MODEL ,
177+ )
178+
179+ # ── Display run configuration ──
119180 num_marked = len (args .search )
120181 label = "," .join (args .search ) if num_marked > 1 else args .search [0 ]
121182 iters = optimal_iterations (args .qubits , num_marked )
122183
123- print ()
124- print ("=" * 60 )
125- print (f" Grover's Search Algorithm" )
126- print ("=" * 60 )
127- print (f" Qubits: { args .qubits } " )
128- print (f" Search state(s): |{ label } >" )
129- if num_marked > 1 :
130- print (f" Marked count: { num_marked } " )
131- print (f" Optimal iters: { iters } " )
132- print (f" Shots: { args .shots } " )
133- print (f" Noise model: { 'Enabled' if args .noise else 'Disabled' } " )
134- print ("=" * 60 )
135- print ()
184+ _print_header (args , iters , label )
136185
186+ # ── Build Grover circuit ──
137187 circuit = build_grover_circuit (args .qubits , args .search , iters )
138-
139188 if args .save_circuit :
140189 save_circuit_diagram (circuit , args .save_circuit )
141190 print (f" Circuit diagram saved to { args .save_circuit } " )
142191
192+ # ── Run simulation ──
143193 run_fn = run_noisy if args .noise else run_ideal
144194 counts = run_fn (circuit , shots = args .shots )
145195 probs = compute_probabilities (counts , args .qubits )
146196 marked_prob = extract_marked_probability (counts , args .search )
147197
148- sim_label = "Noisy" if args .noise else "Ideal"
149- title = f"Grover's Algorithm ({ sim_label } ) - { args .qubits } qubits, |{ label } >"
150198
151- top = sorted (probs .items (), key = lambda x : - x [1 ])[:5 ]
152- print (f" Measurement Results ({ sim_label } Simulator):" )
153- print (f" { 'State' :>8} { 'Probability' :>12} " )
154- print (f" { '-' * 8 } { '-' * 12 } " )
155- for state , prob in top :
156- marker = " <- marked" if state in args .search else ""
157- print (f" { state :>8} { prob :>10.2%} { marker } " )
158- print ()
159- print (f" Marked state(s) |{ label } > probability: { marked_prob :.2%} " )
199+ # ── Display results ──
160200 expected = expected_success_probability (args .qubits , num_marked , iters )
161- print ( f" Expected (ideal): { expected :.1% } " )
201+ _print_results ( probs , marked_prob , expected , args , label )
162202
203+ # ── Plot histogram ──
204+ sim_label = "Noisy" if args .noise else "Ideal"
205+ title = f"Grover's Algorithm ({ sim_label } ) - { args .qubits } qubits, |{ label } >"
163206 _plot_histogram (probs , args .search , title , args .save_plot )
164207
165208
0 commit comments