Compare commits

..

4 Commits
v0.4.3 ... main

Author SHA1 Message Date
89e12233f0
docs: update readme 2025-05-15 09:39:54 +03:00
a405df416c
docs: update readme 2025-05-08 18:55:18 +03:00
30cc848ecd
feat(cli): add phase arg 2025-05-08 18:49:39 +03:00
78952d9364
feat(circuits): improve runtime speed 2025-05-08 17:12:58 +03:00
8 changed files with 80 additions and 77 deletions

View File

@ -6,8 +6,6 @@ A tiny Python package that steps through Grovers Search algorithm and shows y
- A sinecurve of successprobability vs. iteration - A sinecurve of successprobability vs. iteration
- A geometric "rotation" on the unit circle - A geometric "rotation" on the unit circle
Choose between a Matplotlib-based CLI or an optional DearPyGui GUI (WIP).
--- ---
![Demo of Grovers Visualizer](media/demo.gif) ![Demo of Grovers Visualizer](media/demo.gif)
@ -16,69 +14,37 @@ Choose between a Matplotlib-based CLI or an optional DearPyGui GUI (WIP).
## Installation ## Installation
### Via [pipx](https://pipx.pypa.io/stable/installation/) ### Using [uv](https://docs.astral.sh/uv/)/[uvx](https://docs.astral.sh/uv/guides/tools/)
Basic (CLI only):
```bash
pipx install grovers-visualizer
grovers-visualizer 1111
```
With the optional DearPyGui UI (WIP):
```bash
pipx "grovers-visualizer[ui]"
grovers-visualizer --ui
```
### Via [uvx](https://docs.astral.sh/uv/guides/tools/)
Basic (CLI only):
```bash ```bash
uvx grovers-visualizer 1111 uvx grovers-visualizer 1111
``` ```
With the optional UI extra: ### Using [pip](https://pypi.org/project/pip/)/[pipx](https://pipx.pypa.io/stable/installation/)
```bash ```bash
uvx "grovers-visualizer[ui]" --ui pip grovers-visualizer
# or
pipx grovers-visualizer # (recommended)
# and then run
grovers-visualizer
``` ```
--- ---
## Usage ## Usage
### CLI Mode ### Flags
Flags: - `TARGET`
Target bitstring (e.g. `010`). Length also determines number of qubits.
`-t, --target` - `-i, --iterations ITERATIONS`
Target bitstring (e.g. `010`). Length determines number of qubits.
`-s, --speed`
Delay between iterations (seconds). Default `0.5`.
`-i, --iterations`
Max iterations; `0` means use the optimal $\lfloor\frac\pi4\sqrt{2^n}\rfloor$. Max iterations; `0` means use the optimal $\lfloor\frac\pi4\sqrt{2^n}\rfloor$.
`--ui` - `-s, --speed SPEED`
Launch the optional DearPyGui GUI (requires the `[ui]` extra) (WIP). Delay between iterations (seconds). Default `0.5`.
- `-p, --phase PHASE`
### GUI Mode (WIP) The phase $\psi$ (in radians) used for both the oracle and diffusion steps. Defaults to $\pi$ (i.e. a sign-flip, $e^{i\pi}=-1$).
If you installed with `"[ui]"`, launch the DearPyGui window:
```
grovers-visualizer --ui
```
In the UI you can:
- Set number of qubits
- Enter the target bitstring
- Choose max iterations or leave at 0 for optimal
- Control the animation speed
Hit **Start** to watch the bar chart, sine plot, and rotationcircle update in real time.
--- ---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

@ -1,6 +1,6 @@
[project] [project]
name = "grovers-visualizer" name = "grovers-visualizer"
version = "0.4.3" version = "0.5.0"
description = "A tiny Python package that steps through Grovers Search algorithm." description = "A tiny Python package that steps through Grovers Search algorithm."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@ -1,3 +1,4 @@
import math
from argparse import ArgumentParser from argparse import ArgumentParser
from dataclasses import dataclass from dataclasses import dataclass
@ -11,6 +12,7 @@ class Args:
iterations: int iterations: int
speed: float speed: float
ui: bool ui: bool
phase: float
def parse_args() -> Args: def parse_args() -> Args:
@ -26,6 +28,7 @@ def parse_args() -> Args:
iterations=ns.iterations, iterations=ns.iterations,
speed=ns.speed, speed=ns.speed,
ui=ns.ui, ui=ns.ui,
phase=ns.phase,
) )
@ -61,4 +64,15 @@ def parse_cli(base_parser: ArgumentParser) -> None:
default=0.5, default=0.5,
help="Pause duration (seconds) between steps (deafult: 0.5)", help="Pause duration (seconds) between steps (deafult: 0.5)",
) )
parser.add_argument(
"-p",
"--phase",
type=float,
default=math.pi,
help=(
"The phase φ (in radians) used for the oracle and diffusion steps. "
"Defaults to π, which implements the usual sign flip e^(iπ) = -1."
),
)
parser.add_argument("--ui", action="store_true", help="Run with DearPyGui UI") parser.add_argument("--ui", action="store_true", help="Run with DearPyGui UI")

View File

@ -1,25 +1,41 @@
import math
from qiskit import QuantumCircuit from qiskit import QuantumCircuit
from qiskit.circuit.library import PhaseGate
from .state import QubitState from .state import QubitState
def oracle(qc: QuantumCircuit, target_state: QubitState) -> None: def oracle(qc: QuantumCircuit, target_state: QubitState, /, *, phase: float = math.pi) -> None:
"""Oracle that flips the sign of the target state.""" """Oracle that flips the sign of the target state."""
n = len(target_state) n = len(target_state)
encode_target_state(qc, target_state) encode_target_state(qc, target_state)
apply_phase_inversion(qc, n) apply_phase_inversion(qc, n, phase=phase)
encode_target_state(qc, target_state) # Undo encode_target_state(qc, target_state) # Undo
def diffusion(qc: QuantumCircuit, n: int) -> None: def oracle_circuit(target: QubitState, /, *, phase: float = math.pi) -> QuantumCircuit:
n = len(target)
qc = QuantumCircuit(n)
oracle(qc, target, phase=phase)
return qc
def diffusion(qc: QuantumCircuit, n: int, /, *, phase: float = math.pi) -> None:
"""Apply the Grovers diffusion operator.""" """Apply the Grovers diffusion operator."""
qc.h(range(n)) qc.h(range(n))
qc.x(range(n)) qc.x(range(n))
apply_phase_inversion(qc, n) apply_phase_inversion(qc, n, phase=phase)
qc.x(range(n)) qc.x(range(n))
qc.h(range(n)) qc.h(range(n))
def diffusion_circuit(n: int, /, *, phase: float = math.pi) -> QuantumCircuit:
qc = QuantumCircuit(n)
diffusion(qc, n, phase=phase)
return qc
def encode_target_state(qc: QuantumCircuit, target_state: QubitState) -> None: def encode_target_state(qc: QuantumCircuit, target_state: QubitState) -> None:
"""Apply X gates to qubits where the target state bit is '0'.""" """Apply X gates to qubits where the target state bit is '0'."""
for i, bit in enumerate(reversed(target_state)): for i, bit in enumerate(reversed(target_state)):
@ -27,11 +43,10 @@ def encode_target_state(qc: QuantumCircuit, target_state: QubitState) -> None:
qc.x(i) qc.x(i)
def apply_phase_inversion(qc: QuantumCircuit, n: int) -> None: def apply_phase_inversion(qc: QuantumCircuit, n: int, /, *, phase: float = math.pi) -> None:
"""Apply a multi-controlled phase inversion (Z) to the marked state.""" """Apply a multi-controlled phase inversion (Z) to the marked state."""
if n == 1: if n == 1:
qc.z(0) qc.p(phase, 0)
return return
qc.h(n - 1) mc_phase = PhaseGate(phase).control(n - 1)
qc.mcx(list(range(n - 1)), n - 1) qc.append(mc_phase, list(range(n)))
qc.h(n - 1)

View File

@ -7,7 +7,7 @@ from .args import Args
def run_cli(args: Args) -> None: def run_cli(args: Args) -> None:
vis = GroverVisualizer(args.target, pause=args.speed) vis = GroverVisualizer(args.target, pause=args.speed)
for it, sv in grover_evolver(vis.target, args.iterations): for it, sv in grover_evolver(vis.target, args.iterations, phase=args.phase):
if not vis.is_running: if not vis.is_running:
break break
vis.update(it, sv) vis.update(it, sv)

View File

@ -1,31 +1,39 @@
import math
from collections.abc import Iterator from collections.abc import Iterator
from itertools import count from itertools import count
from qiskit import QuantumCircuit from qiskit import QuantumCircuit
from qiskit.qasm2.parse import Operator
from qiskit.quantum_info import Statevector from qiskit.quantum_info import Statevector
from grovers_visualizer.circuit import diffusion, oracle from grovers_visualizer.circuit import diffusion_circuit, oracle_circuit
from grovers_visualizer.state import QubitState from grovers_visualizer.state import QubitState
def grover_evolver(target: QubitState, max_iterations: int = 0) -> Iterator[tuple[int, Statevector]]: def grover_evolver(
target: QubitState,
max_iterations: int = 0,
*,
phase: float = math.pi,
) -> Iterator[tuple[int, Statevector]]:
"""Yields (iteration, statevector) pairs. """Yields (iteration, statevector) pairs.
iteration=0 is the uniform-Hadamard initialization. If - iteration=0 is the uniform-Hadamard initialization
max_iterations > 0, stop after that many iterations. If - max_iterations > 0, stop after that many iterations
max_iterations == 0, run indefinitely (until the consumer breaks). - max_iterations == 0, run indefinitely (until the consumer breaks)
""" """
n_qubits = len(target) n_qubits = len(target)
qc = QuantumCircuit(n_qubits) qc = QuantumCircuit(n_qubits)
qc.h(range(n_qubits)) qc.h(range(n_qubits))
# initial statevector # initial statevector
yield 0, Statevector.from_instruction(qc) sv = Statevector.from_instruction(qc)
yield 0, sv
# pick an iterator for subsequent steps oracle_op = Operator(oracle_circuit(target, phase=phase))
iter = range(1, max_iterations + 1) if max_iterations > 0 else count(1) diffusion_op = Operator(diffusion_circuit(n_qubits, phase=phase))
for i in iter: iters = range(1, max_iterations + 1) if max_iterations > 0 else count(1)
oracle(qc, target) for i in iters:
diffusion(qc, n_qubits) sv = sv.evolve(oracle_op).evolve(diffusion_op)
yield i, Statevector.from_instruction(qc) yield i, sv

View File

@ -155,7 +155,7 @@ wheels = [
[[package]] [[package]]
name = "grovers-visualizer" name = "grovers-visualizer"
version = "0.4.2" version = "0.5.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy" },