Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
375f98f
Add helper to plot counts from results
alessandro-santini Aug 14, 2026
e242233
precommit
alessandro-santini Aug 14, 2026
2575beb
added option to stack different runs
alessandro-santini Aug 14, 2026
4e2d6dd
Simplified visualization.py
alessandro-santini Aug 14, 2026
8a5f5ea
update notebooks
alessandro-santini Aug 14, 2026
a075254
added flag for xlabel and ylabel
alessandro-santini Aug 14, 2026
43eb7db
Change color naming to colors
alessandro-santini Aug 14, 2026
628cd32
Revert colors in color, added some checks on the lists lengths
alessandro-santini Aug 14, 2026
778f92c
precommit
alessandro-santini Aug 14, 2026
4f06f89
More forgiving with legend labels format
alessandro-santini Aug 14, 2026
d95a356
Added test
alessandro-santini Aug 14, 2026
5d6bef7
precommit
alessandro-santini Aug 14, 2026
0a9e976
Gone through changes
alessandro-santini Aug 14, 2026
92acd16
precommit
alessandro-santini Aug 14, 2026
432b476
fixed docstring
alessandro-santini Aug 14, 2026
a98d245
Added fallback label for legend
alessandro-santini Aug 14, 2026
764064b
Add fallback label only when there is more than one histogram
alessandro-santini Aug 14, 2026
09bf443
Rename default legend labels
alessandro-santini Aug 14, 2026
8723140
Apply suggestions from code review
alessandro-santini Aug 14, 2026
9691d64
Fix labels position
alessandro-santini Aug 14, 2026
e2cb091
fix docstring
alessandro-santini Aug 14, 2026
936a653
Changed highlight behavior
alessandro-santini Aug 14, 2026
08d16bd
Fix comment
alessandro-santini Aug 14, 2026
f3b714d
uniform colors
alessandro-santini Aug 14, 2026
a61c571
Update visualization.py
alessandro-santini Aug 14, 2026
c829d48
Update visualization.py
alessandro-santini Aug 14, 2026
90fc216
Update visualization.py
alessandro-santini Aug 14, 2026
a67993d
Merge branch 'main' into as/451-add-plot-histogram-function
sgrava Aug 17, 2026
7809752
format
sgrava Aug 17, 2026
726e6b5
Merge branch 'main' into as/451-add-plot-histogram-function
sgrava Sep 8, 2026
2e9474a
simplify API
alessandro-santini Sep 9, 2026
47fc4e9
Kept the old way of highlighting the Histogram since now we just plot…
alessandro-santini Sep 9, 2026
e64fa65
changed comment
alessandro-santini Sep 9, 2026
a7ebfe2
small fix
alessandro-santini Sep 10, 2026
811c80f
more small fix
alessandro-santini Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 7 additions & 28 deletions docs/tutorials/Solving_a_MWIS.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@
"id": "24",
"metadata": {},
"source": [
"Finally we plot the full distribution of measured bitstrings, highlighting the exact MWIS solution `0110` in green. An adiabatic run that stayed in the ground state should return `0110` with overwhelming probability."
"Finally we plot the full distribution of measured bitstrings, highlighting the exact MWIS solution `0110`. An adiabatic run that stayed in the ground state should return `0110` with overwhelming probability."
]
},
{
Expand All @@ -446,33 +446,12 @@
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"from qoolqit.visualization import plot_bitstrings\n",
"\n",
"SOLUTION = \"0110\"\n",
"\n",
"def plot_distribution(counts, solution, top=None):\n",
" \"\"\"Bar plot of a bitstring-count distribution, highlighting the solution.\n",
"\n",
" Args:\n",
" counts (dict[str, int]): Mapping from measured bitstring to its count.\n",
" solution (str): The bitstring to highlight (the exact MWIS answer).\n",
" top (int | None): If given, only show the `top` most frequent bitstrings.\n",
" \"\"\"\n",
" counts = dict(sorted(counts.items(), key=lambda kv: kv[1], reverse=True))\n",
" if top is not None:\n",
" counts = dict(list(counts.items())[:top])\n",
"\n",
" colors = [\"tab:green\" if b == solution else \"tab:blue\" for b in counts]\n",
" plt.figure(figsize=(12, 5))\n",
" plt.bar(counts.keys(), counts.values(), width=0.6, color=colors)\n",
" plt.xlabel(\"bitstring\")\n",
" plt.ylabel(\"counts\")\n",
" plt.title(f\"Measurement distribution (solution {solution} in green)\")\n",
" plt.xticks(rotation=\"vertical\")\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"plot_distribution(counts, SOLUTION, top=20)"
"plot_bitstrings(counts, highlight={SOLUTION: \"#00C887\"})\n"
]
},
{
Expand Down Expand Up @@ -518,7 +497,7 @@
"print(\"Most frequent bitstring:\", max(counts_analog, key=counts_analog.get))\n",
"print(f\"P({SOLUTION}) = {p_solution:.2%}\")\n",
"\n",
"plot_distribution(counts_analog, SOLUTION, top=20)"
"plot_bitstrings(counts, highlight={SOLUTION: \"#00C887\"})"
]
},
{
Expand Down Expand Up @@ -550,7 +529,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "qoolqit",
"display_name": "devqoolqit (3.14.6.final.0)",
"language": "python",
"name": "python3"
},
Expand All @@ -564,7 +543,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.11"
"version": "3.14.6"
}
},
"nbformat": 4,
Expand Down
47 changes: 12 additions & 35 deletions docs/tutorials/solving_a_qubo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -365,46 +365,23 @@
"metadata": {},
"outputs": [],
"source": [
"from collections import Counter\n",
"from qoolqit.visualization import plot_bitstrings\n",
"\n",
"import matplotlib.pyplot as plt\n",
"\n",
"\n",
"def plot_distribution(counter, solutions, bins=10):\n",
" counter = Counter(counter)\n",
" counter = dict(counter.most_common(bins))\n",
" color = [\n",
" \"tab:green\" if key in solutions.tolist() else \"tab:blue\" for key in counter\n",
" ]\n",
" fig, ax = plt.subplots()\n",
" ax.set_xlabel(\"Bitstrings\")\n",
" ax.set_ylabel(\"Counts\")\n",
" ax.bar(\n",
" range(len(counter)), counter.values(), color=color, tick_label=counter.keys()\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "26",
"metadata": {},
"outputs": [],
"source": [
"plot_distribution(counter, marked_bitstrings)"
"highlight = {b: \"#00C887\" for b in marked_bitstrings}\n",
"plot_bitstrings(counter, highlight=highlight)"
]
},
{
"cell_type": "markdown",
"id": "27",
"id": "26",
"metadata": {},
"source": [
"As we can see, the bitstrings we had marked as the optimal solutions of this QUBO problem were the ones sampled with the highest probability, meaning the the QUBO problem was successfully solved with the quantum program we defined."
]
},
{
"cell_type": "markdown",
"id": "28",
"id": "27",
"metadata": {},
"source": [
"## Advanced Compilation \n",
Expand All @@ -418,7 +395,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "29",
"id": "28",
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -428,7 +405,7 @@
},
{
"cell_type": "markdown",
"id": "30",
"id": "29",
"metadata": {},
"source": [
"Concretely, we can see the beneficial effect of rescaling the drive duration on the simulation results:"
Expand All @@ -437,19 +414,19 @@
{
"cell_type": "code",
"execution_count": null,
"id": "31",
"id": "30",
"metadata": {},
"outputs": [],
"source": [
"job = emulator.run(program)\n",
"results = job.results()\n",
"counter = results.final_bitstrings\n",
"plot_distribution(counter, marked_bitstrings)"
"plot_bitstrings(counter, highlight=highlight)"
]
},
{
"cell_type": "markdown",
"id": "32",
"id": "31",
"metadata": {},
"source": [
"Here the execution was relatively fast and easy, but for larger QUBO instances, or for QPU execution (which might have some queue), see the [Execution](https://docs.pasqal.com/qoolqit/qoolqitDoc/fundamentals/execution/execution/) section of the QoolQit documentation."
Expand All @@ -458,7 +435,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "qoolqit",
"display_name": "devqoolqit (3.14.6.final.0)",
"language": "python",
"name": "python3"
},
Expand All @@ -472,7 +449,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.11"
"version": "3.14.6"
}
},
"nbformat": 4,
Expand Down
79 changes: 79 additions & 0 deletions qoolqit/visualization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Visualization helpers for QoolQit results."""

from __future__ import annotations

from collections import Counter

import matplotlib.pyplot as plt
from matplotlib.axes import Axes

DEFAULT_BAR_COLOR = "#397378"


def plot_bitstrings(
counts: dict[str, int],
top: int | None = None,
normalize: bool = False,
color: str = DEFAULT_BAR_COLOR,
highlight: dict[str, str] | None = None,
label: str | None = None,
ax: Axes | None = None,
) -> None:
"""Plot bitstring counts, optionally highlighting selected ones.

Arguments:
counts: Mapping of bitstrings to counts.
top: Plot only the top N counts.
normalize: Normalize counts to probabilities. Defaults to False.
color: Bar color.
highlight: Mapping of bitstrings to highlight colors. Highlighted
outcomes get their bar and tick label colored accordingly.
label: Legend label for the bars. Call ax.legend() to show it.
ax: Axes to draw on. Creates new axes if omitted.
"""

if not counts:
raise ValueError("counts cannot be empty")

Comment thread
alessandro-santini marked this conversation as resolved.
total = sum(counts.values())
if normalize and total == 0:
raise ValueError("cannot plot normalized counts with zero total counts")

if top is not None and top <= 0:
raise ValueError("top must be a positive integer")

# most_common(None) returns all entries, sorted by decreasing count
selected_counts = Counter(counts).most_common(top)
bitstrings = [bitstring for bitstring, _ in selected_counts]
values = [count / total if normalize else count for _, count in selected_counts]

highlight = highlight or {}

# Create the plot if no axes are provided
if ax is None:
_, ax = plt.subplots(figsize=(12, 5))

positions = range(len(bitstrings))
bar_colors = [highlight.get(bitstring, color) for bitstring in bitstrings]
ax.bar(positions, values, width=0.65, color=bar_colors)

if label is not None:
# A zero-height bar draws nothing but gives the legend a swatch in
# `color`, regardless of which bitstrings are highlighted.
ax.bar(0, 0, color=color, label=label)

ax.set_xticks(list(positions))
ax.set_xticklabels(bitstrings)

# Highlighted outcomes are also marked on their tick label
for tick_label in ax.get_xticklabels():
if tick_label.get_text() in highlight:
tick_label.set_color(highlight[tick_label.get_text()])
tick_label.set_fontweight("bold")

ax.tick_params(axis="x", labelrotation=90)
ax.grid(axis="y", linestyle="--", alpha=0.4)

# Default labels; the caller can override via ax.set_xlabel/ax.set_ylabel
ax.set_ylabel("Probability" if normalize else "Counts")
ax.set_xlabel("Bitstrings")
61 changes: 61 additions & 0 deletions tests/test_visualization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

import matplotlib.pyplot as plt
import pytest
from matplotlib.colors import to_rgba

from qoolqit.visualization import DEFAULT_BAR_COLOR, plot_bitstrings


def test_plot_bitstrings_errors() -> None:
with pytest.raises(ValueError, match="counts cannot be empty"):
plot_bitstrings(counts={})
with pytest.raises(ValueError, match="cannot plot normalized counts with zero total counts"):
plot_bitstrings(counts={"000": 0}, normalize=True)
with pytest.raises(ValueError, match="top must be a positive integer"):
plot_bitstrings(counts={"000": 1, "001": 2}, top=0)


def test_plot_bitstrings_default_bar_color() -> None:
_, ax = plt.subplots()
plot_bitstrings(counts={"000": 1, "001": 2}, ax=ax)

bars = ax.containers[0]
assert all(bar.get_facecolor() == to_rgba(DEFAULT_BAR_COLOR) for bar in bars)


def test_plot_bitstrings_label_shows_up_in_legend() -> None:
_, ax = plt.subplots()
plot_bitstrings(counts={"000": 1, "001": 2}, label="run 1", ax=ax)

assert ax.get_legend() is None
legend = ax.legend()
assert legend.legend_handles[0].get_facecolor() == to_rgba(DEFAULT_BAR_COLOR)
assert legend.get_texts()[0].get_text() == "run 1"


def test_plot_bitstrings_highlight_colors_the_bar() -> None:
_, ax = plt.subplots()
plot_bitstrings(counts={"000": 1, "001": 2}, highlight={"001": "tab:red"}, ax=ax)

bars = dict(zip(["001", "000"], ax.containers[0]))
assert bars["001"].get_facecolor() == to_rgba("tab:red")
assert bars["000"].get_facecolor() == to_rgba(DEFAULT_BAR_COLOR)


def test_plot_bitstrings_legend_uses_base_color_even_if_first_bar_highlighted() -> None:
_, ax = plt.subplots()
# "001" has the higher count so it plots first, and is also highlighted.
plot_bitstrings(counts={"000": 1, "001": 2}, highlight={"001": "tab:red"}, label="run 1", ax=ax)

legend = ax.legend()
assert legend.legend_handles[0].get_facecolor() == to_rgba(DEFAULT_BAR_COLOR)


def test_plot_bitstrings_two_calls_on_same_axes_keep_their_own_highlights() -> None:
_, ax = plt.subplots()
plot_bitstrings(counts={"000": 1, "001": 2}, highlight={"001": "tab:red"}, ax=ax)
plot_bitstrings(counts={"000": 1, "001": 2}, color="C2", ax=ax)

first_call_bars = dict(zip(["001", "000"], ax.containers[0]))
assert first_call_bars["001"].get_facecolor() == to_rgba("tab:red")
Loading