From 50e843a8844e9ccfcfcb5f93bc01fceeeebead48 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Thu, 24 Apr 2025 15:02:09 +0300 Subject: [PATCH] chore(ui): setup dependencies --- pyproject.toml | 13 ++--- src/grovers_visualizer/cli.py | 79 +++++++++++++++++++++++++++++++ src/grovers_visualizer/main.py | 84 +++------------------------------ src/grovers_visualizer/parse.py | 3 ++ src/grovers_visualizer/ui.py | 16 +++++++ uv.lock | 33 +++++++++---- 6 files changed, 135 insertions(+), 93 deletions(-) create mode 100644 src/grovers_visualizer/cli.py create mode 100644 src/grovers_visualizer/ui.py diff --git a/pyproject.toml b/pyproject.toml index 125eaa4..a72cbba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,20 +5,21 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "argparse>=1.4.0", - "numpy>=2.2.4", - "qiskit-aer>=0.17.0", - "qiskit[visualization]>=2.0.0", + "argparse==1.4.0", + "numpy==2.2.4", + "pyqt6==6.9.0", + "qiskit-aer==0.17.0", + "qiskit[visualization]==2.0.0", ] [project.scripts] grovers-visualizer = "grovers_visualizer.main:main" [project.optional-dependencies] -mpl = ["pyqt6>=6.9.0"] +ui = ["dearpygui==2.0.0"] [dependency-groups] -dev = ["mypy>=1.15.0", "ruff>=0.11.4"] +dev = ["mypy~=1.15", "ruff~=0.11"] [build-system] requires = ["hatchling"] diff --git a/src/grovers_visualizer/cli.py b/src/grovers_visualizer/cli.py new file mode 100644 index 0000000..202623d --- /dev/null +++ b/src/grovers_visualizer/cli.py @@ -0,0 +1,79 @@ +from math import asin, sqrt +from typing import Callable + +import matplotlib.pyplot as plt +from matplotlib.backend_bases import KeyEvent +from matplotlib.gridspec import GridSpec +from qiskit import QuantumCircuit +from qiskit.quantum_info import Statevector + +from .circuit import diffusion, oracle +from .parse import Args +from .plot import SinePlotData, plot_amplitudes, plot_circle, plot_sine +from .utils import all_states, optimal_grover_iterations + + +def run_cli(args: Args) -> None: + target_state = args.target + n_qubits = len(target_state) + basis_states = [str(bit) for bit in all_states(n_qubits)] + optimal_iterations = optimal_grover_iterations(n_qubits) + theta = 2 * asin(1 / sqrt(2**n_qubits)) + state_angle = 0.5 * theta + + plt.ion() + fig = plt.figure(figsize=(14, 6)) + gs = GridSpec(2, 2, width_ratios=(3, 1), figure=fig) + ax_bar = fig.add_subplot(gs[0, 0]) + ax_sine = fig.add_subplot(gs[1, 0]) + ax_circle = fig.add_subplot(gs[:, 1]) + bars = ax_bar.bar(basis_states, [0] * len(basis_states), color="skyblue") + ax_bar.set_ylim(-1, 1) + ax_bar.set_title("Amplitudes (example)") + + sine_data = SinePlotData() + + def plot_bar( + operation: Callable[[QuantumCircuit], None] | None, + step_label: str, + iteration: int, + ) -> None: + if operation is not None: + operation(qc) + sv = Statevector.from_instruction(qc) + plot_amplitudes(ax_bar, bars, sv, basis_states, step_label, iteration, target_state, optimal_iterations) + + # Start with Hadamard + qc = QuantumCircuit(n_qubits) + qc.h(range(n_qubits)) + plot_bar(None, "Hadamard (Initialization)", 0) + + iteration = 1 + running = True + + def on_key(event: KeyEvent) -> None: + nonlocal running + if event.key == "q": + running = False + + cid = fig.canvas.mpl_connect("key_press_event", on_key) + while plt.fignum_exists(fig.number) and running: + plot_bar(lambda qc: oracle(qc, target_state), "Oracle (Query Phase)", iteration) + plot_bar(lambda qc: diffusion(qc, n_qubits), "Diffusion (Inversion Phase)", iteration) + + plot_circle(ax_circle, iteration, optimal_iterations, theta, state_angle) + sine_data.calc_and_append_probability(iteration, theta) + plot_sine(ax_sine, sine_data) + + plt.pause(args.speed) + + iteration += 1 + if args.iterations > 0 and iteration > args.iterations: + break + + fig.canvas.mpl_disconnect(cid) + plt.ioff() + + +if __name__ == "__main__": + main() diff --git a/src/grovers_visualizer/main.py b/src/grovers_visualizer/main.py index b15ec2a..b322bd6 100644 --- a/src/grovers_visualizer/main.py +++ b/src/grovers_visualizer/main.py @@ -6,84 +6,14 @@ simulation using Qiskit's Aer simulator, and visualizes the results using matplotlib. """ -from math import asin, sqrt -from typing import Callable - -import matplotlib.pyplot as plt -from matplotlib.backend_bases import KeyEvent -from matplotlib.gridspec import GridSpec -from qiskit import QuantumCircuit -from qiskit.quantum_info import Statevector - -from grovers_visualizer.circuit import diffusion, oracle -from grovers_visualizer.parse import parse_args -from grovers_visualizer.plot import SinePlotData, plot_amplitudes, plot_circle, plot_sine -from grovers_visualizer.utils import all_states, optimal_grover_iterations +from .cli import run_cli +from .parse import parse_args +from .ui import run_dpg_ui def main() -> None: args = parse_args() - - target_state = args.target - n_qubits = len(target_state) - basis_states = [str(bit) for bit in all_states(n_qubits)] - optimal_iterations = optimal_grover_iterations(n_qubits) - theta = 2 * asin(1 / sqrt(2**n_qubits)) - state_angle = 0.5 * theta - - plt.ion() - fig = plt.figure(figsize=(14, 6)) - gs = GridSpec(2, 2, width_ratios=(3, 1), figure=fig) - ax_bar = fig.add_subplot(gs[0, 0]) - ax_sine = fig.add_subplot(gs[1, 0]) - ax_circle = fig.add_subplot(gs[:, 1]) - bars = ax_bar.bar(basis_states, [0] * len(basis_states), color="skyblue") - ax_bar.set_ylim(-1, 1) - ax_bar.set_title("Amplitudes (example)") - - sine_data = SinePlotData() - - def plot_bar( - operation: Callable[[QuantumCircuit], None] | None, - step_label: str, - iteration: int, - ) -> None: - if operation is not None: - operation(qc) - sv = Statevector.from_instruction(qc) - plot_amplitudes(ax_bar, bars, sv, basis_states, step_label, iteration, target_state, optimal_iterations) - - # Start with Hadamard - qc = QuantumCircuit(n_qubits) - qc.h(range(n_qubits)) - plot_bar(None, "Hadamard (Initialization)", 0) - - iteration = 1 - running = True - - def on_key(event: KeyEvent) -> None: - nonlocal running - if event.key == "q": - running = False - - cid = fig.canvas.mpl_connect("key_press_event", on_key) - while plt.fignum_exists(fig.number) and running: - plot_bar(lambda qc: oracle(qc, target_state), "Oracle (Query Phase)", iteration) - plot_bar(lambda qc: diffusion(qc, n_qubits), "Diffusion (Inversion Phase)", iteration) - - plot_circle(ax_circle, iteration, optimal_iterations, theta, state_angle) - sine_data.calc_and_append_probability(iteration, theta) - plot_sine(ax_sine, sine_data) - - plt.pause(args.speed) - - iteration += 1 - if args.iterations > 0 and iteration > args.iterations: - break - - fig.canvas.mpl_disconnect(cid) - plt.ioff() - - -if __name__ == "__main__": - main() + if args.ui: + run_dpg_ui(args) + else: + run_cli(args) diff --git a/src/grovers_visualizer/parse.py b/src/grovers_visualizer/parse.py index c8c2d88..0b1665c 100644 --- a/src/grovers_visualizer/parse.py +++ b/src/grovers_visualizer/parse.py @@ -9,6 +9,7 @@ class Args: target: QubitState iterations: int speed: float + ui: bool def parse_args() -> Args: @@ -32,9 +33,11 @@ def parse_args() -> Args: default=0.5, help="Pause duration (seconds) between steps (deafult: 0.5)", ) + parser.add_argument("--ui", action="store_true", help="Run with DearPyGui UI") ns = parser.parse_args() return Args( target=QubitState.from_str(ns.target), iterations=ns.iterations, speed=ns.speed, + ui=ns.ui, ) diff --git a/src/grovers_visualizer/ui.py b/src/grovers_visualizer/ui.py new file mode 100644 index 0000000..34b7ad8 --- /dev/null +++ b/src/grovers_visualizer/ui.py @@ -0,0 +1,16 @@ +from importlib.util import find_spec + +from .parse import Args + + +def is_dearpygui_available() -> bool: + try: + return find_spec("dearpygui.dearpygui") is not None + except ModuleNotFoundError: + return False + + +def run_dpg_ui(_args: Args) -> None: + if not is_dearpygui_available(): + print("DearPyGui is not installed. Install with: pip install .[ui]") + return diff --git a/uv.lock b/uv.lock index 6368025..732886d 100644 --- a/uv.lock +++ b/uv.lock @@ -51,6 +51,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, ] +[[package]] +name = "dearpygui" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/58/4e1744d2af72a3aa432fd82aeb97dcbc43351b17f837d879b4c4926bd16c/dearpygui-2.0.0-cp313-cp313-macosx_10_6_x86_64.whl", hash = "sha256:3bf0aa19baedb4f130b8de636d4644740ffd9b7008481794c574bf59240808dc", size = 2223494 }, + { url = "https://files.pythonhosted.org/packages/70/e0/09885f99df476d03e573183a755dc515cd88565cdbf036c385f0dda41dbb/dearpygui-2.0.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:ae6fc9aa3390b29387c2bc2bf3ac7a92c10eae2b479818171864fc29a7de344e", size = 1873562 }, + { url = "https://files.pythonhosted.org/packages/d6/2b/0714feed9e072377d575cb8801ac13a909f7880ca51c9dd6f2d768bcd1ba/dearpygui-2.0.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:f02b0ab56700a775d7e6446e3c424d5bed3386efe721a04518d02461851daadf", size = 2636153 }, + { url = "https://files.pythonhosted.org/packages/55/9e/a957ffc21d12a1fb66d611f82871944565112b44afa0856bb15c6ece77d5/dearpygui-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e3d52057f49773b10808962806711c3b3119e829d36407afb84ad50522edc9b0", size = 1814308 }, +] + [[package]] name = "dill" version = "0.3.9" @@ -84,13 +95,14 @@ source = { editable = "." } dependencies = [ { name = "argparse" }, { name = "numpy" }, + { name = "pyqt6" }, { name = "qiskit", extra = ["visualization"] }, { name = "qiskit-aer" }, ] [package.optional-dependencies] -mpl = [ - { name = "pyqt6" }, +ui = [ + { name = "dearpygui" }, ] [package.dev-dependencies] @@ -101,18 +113,19 @@ dev = [ [package.metadata] requires-dist = [ - { name = "argparse", specifier = ">=1.4.0" }, - { name = "numpy", specifier = ">=2.2.4" }, - { name = "pyqt6", marker = "extra == 'mpl'", specifier = ">=6.9.0" }, - { name = "qiskit", extras = ["visualization"], specifier = ">=2.0.0" }, - { name = "qiskit-aer", specifier = ">=0.17.0" }, + { name = "argparse", specifier = "==1.4.0" }, + { name = "dearpygui", marker = "extra == 'ui'", specifier = "==2.0.0" }, + { name = "numpy", specifier = "==2.2.4" }, + { name = "pyqt6", specifier = "==6.9.0" }, + { name = "qiskit", extras = ["visualization"], specifier = "==2.0.0" }, + { name = "qiskit-aer", specifier = "==0.17.0" }, ] -provides-extras = ["mpl"] +provides-extras = ["ui"] [package.metadata.requires-dev] dev = [ - { name = "mypy", specifier = ">=1.15.0" }, - { name = "ruff", specifier = ">=0.11.4" }, + { name = "mypy", specifier = "~=1.15" }, + { name = "ruff", specifier = "~=0.11" }, ] [[package]]