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

@ -2,11 +2,9 @@
A tiny Python package that steps through Grovers Search algorithm and shows you, after each iteration:
- A barchart of amplitudes (or probabilities)
- A sinecurve of successprobability vs. iteration
- A geometric "rotation" on the unit circle
Choose between a Matplotlib-based CLI or an optional DearPyGui GUI (WIP).
- A barchart of amplitudes (or probabilities)
- A sinecurve of successprobability vs. iteration
- A geometric "rotation" on the unit circle
---
@ -16,69 +14,37 @@ Choose between a Matplotlib-based CLI or an optional DearPyGui GUI (WIP).
## Installation
### Via [pipx](https://pipx.pypa.io/stable/installation/)
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):
### Using [uv](https://docs.astral.sh/uv/)/[uvx](https://docs.astral.sh/uv/guides/tools/)
```bash
uvx grovers-visualizer 1111
```
With the optional UI extra:
### Using [pip](https://pypi.org/project/pip/)/[pipx](https://pipx.pypa.io/stable/installation/)
```bash
uvx "grovers-visualizer[ui]" --ui
pip grovers-visualizer
# or
pipx grovers-visualizer # (recommended)
# and then run
grovers-visualizer
```
---
## Usage
### CLI Mode
### Flags
Flags:
`-t, --target`
Target bitstring (e.g. `010`). Length determines number of qubits.
`-s, --speed`
Delay between iterations (seconds). Default `0.5`.
`-i, --iterations`
- `TARGET`
Target bitstring (e.g. `010`). Length also determines number of qubits.
- `-i, --iterations ITERATIONS`
Max iterations; `0` means use the optimal $\lfloor\frac\pi4\sqrt{2^n}\rfloor$.
`--ui`
Launch the optional DearPyGui GUI (requires the `[ui]` extra) (WIP).
### GUI Mode (WIP)
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.
- `-s, --speed SPEED`
Delay between iterations (seconds). Default `0.5`.
- `-p, --phase PHASE`
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$).
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

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

View File

@ -1,3 +1,4 @@
import math
from argparse import ArgumentParser
from dataclasses import dataclass
@ -11,6 +12,7 @@ class Args:
iterations: int
speed: float
ui: bool
phase: float
def parse_args() -> Args:
@ -26,6 +28,7 @@ def parse_args() -> Args:
iterations=ns.iterations,
speed=ns.speed,
ui=ns.ui,
phase=ns.phase,
)
@ -61,4 +64,15 @@ def parse_cli(base_parser: ArgumentParser) -> None:
default=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")

View File

@ -1,25 +1,41 @@
import math
from qiskit import QuantumCircuit
from qiskit.circuit.library import PhaseGate
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."""
n = len(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
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."""
qc.h(range(n))
qc.x(range(n))
apply_phase_inversion(qc, n)
apply_phase_inversion(qc, n, phase=phase)
qc.x(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:
"""Apply X gates to qubits where the target state bit is '0'."""
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)
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."""
if n == 1:
qc.z(0)
qc.p(phase, 0)
return
qc.h(n - 1)
qc.mcx(list(range(n - 1)), n - 1)
qc.h(n - 1)
mc_phase = PhaseGate(phase).control(n - 1)
qc.append(mc_phase, list(range(n)))

View File

@ -7,7 +7,7 @@ from .args import Args
def run_cli(args: Args) -> None:
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:
break
vis.update(it, sv)

View File

@ -1,31 +1,39 @@
import math
from collections.abc import Iterator
from itertools import count
from qiskit import QuantumCircuit
from qiskit.qasm2.parse import Operator
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
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.
iteration=0 is the uniform-Hadamard initialization. If
max_iterations > 0, stop after that many iterations. If
max_iterations == 0, run indefinitely (until the consumer breaks).
- iteration=0 is the uniform-Hadamard initialization
- max_iterations > 0, stop after that many iterations
- max_iterations == 0, run indefinitely (until the consumer breaks)
"""
n_qubits = len(target)
qc = QuantumCircuit(n_qubits)
qc.h(range(n_qubits))
# initial statevector
yield 0, Statevector.from_instruction(qc)
sv = Statevector.from_instruction(qc)
yield 0, sv
# pick an iterator for subsequent steps
iter = range(1, max_iterations + 1) if max_iterations > 0 else count(1)
oracle_op = Operator(oracle_circuit(target, phase=phase))
diffusion_op = Operator(diffusion_circuit(n_qubits, phase=phase))
for i in iter:
oracle(qc, target)
diffusion(qc, n_qubits)
yield i, Statevector.from_instruction(qc)
iters = range(1, max_iterations + 1) if max_iterations > 0 else count(1)
for i in iters:
sv = sv.evolve(oracle_op).evolve(diffusion_op)
yield i, sv

View File

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