From 620afe2077c05795f7558d93636a84a5925c87cb Mon Sep 17 00:00:00 2001 From: Mahsa02 Date: Fri, 6 Feb 2026 23:28:42 -0500 Subject: [PATCH] Update Streamlit UI styling and circuit rendering --- pc/app_streamlit.py | 513 ++++++++++++++++++++++++-------------------- requirements.txt | 1 + 2 files changed, 279 insertions(+), 235 deletions(-) diff --git a/pc/app_streamlit.py b/pc/app_streamlit.py index 03e3e60..c863f84 100644 --- a/pc/app_streamlit.py +++ b/pc/app_streamlit.py @@ -1,10 +1,31 @@ +""" +QTrim - Quantum Circuit Optimization UI +---------------------------------------- + +This version: + +- Renders real Qiskit circuit diagrams +- Uses FAKE circuits for demo if backend doesn't provide QASM yet +- Is ready to accept real before_qasm / after_qasm from RL later +- Clean separation of responsibilities +""" + import base64 from pathlib import Path +from io import BytesIO +from typing import Optional, Union, Dict import streamlit as st import requests import pandas as pd import plotly.graph_objects as go +import matplotlib.pyplot as plt +from qiskit import QuantumCircuit + + +# ----------------------------- +# Configuration +# ----------------------------- API_URL = "http://127.0.0.1:8000/optimize" @@ -18,6 +39,7 @@ "Linear Dataflow Pipeline": "linear_dataflow_pipeline", } +# Temporary baseline metrics (until RL returns real ones) BASELINE = { "parity": {"gate_count": 64, "depth": 24, "cost": 118}, "half_adder": {"gate_count": 72, "depth": 28, "cost": 132}, @@ -25,179 +47,226 @@ "linear_dataflow_pipeline": {"gate_count": 95, "depth": 40, "cost": 190}, } +DEFAULT_CIRCUIT_STYLE: Dict[str, str] = { + "backgroundcolor": "none", + "gatefacecolor": "none", + "barrierfacecolor": "none", + "linecolor": "#e8f1ff", + "textcolor": "#e8f1ff", + "gatetextcolor": "#e8f1ff", + "subtextcolor": "#b9e3ff", + "barrieredgecolor": "#b9e3ff", +} + st.set_page_config(page_title="QTrim", layout="wide") -# ---------- Theme helpers ---------- -def b64_file(path: Path) -> str: - return base64.b64encode(path.read_bytes()).decode() -def inject_theme(bg_path: Path): - bg = b64_file(bg_path) +# ----------------------------- +# Utility: Render Qiskit Circuit +# ----------------------------- + +def set_background(image_path: Path) -> None: + """ + Set a full-page background image for the Streamlit app. + """ + try: + encoded = base64.b64encode(image_path.read_bytes()).decode("ascii") + except Exception: + return + st.markdown( f""" """, unsafe_allow_html=True, ) -def centered_logo_png(path: Path, width_px: int = 260, nudge_px: int = 0): + +set_background(ASSETS / "bg.png") + +def image_to_base64(image_path: Path) -> Optional[str]: """ - Streamlit's st.image() can look off-center if the PNG has uneven transparent padding. - This renders the PNG as inline HTML and allows a small translateX nudge. + Read an image from disk and return a base64 string. """ - logo_b64 = b64_file(path) - st.markdown( - f""" -
- -
- """, - unsafe_allow_html=True, - ) + try: + return base64.b64encode(image_path.read_bytes()).decode("ascii") + except Exception: + return None -def pct_change(before: int, after: int) -> float: - if before == 0: - return 0.0 - return (before - after) / before * 100.0 +def circuit_to_png_bytes(qc: QuantumCircuit, style: Optional[Dict[str, str]] = None) -> bytes: + """ + Converts a QuantumCircuit to PNG bytes using matplotlib. + """ + fig = qc.draw(output="mpl", style=style) if style else qc.draw(output="mpl") + fig.patch.set_alpha(0) + buf = BytesIO() + fig.savefig(buf, format="png", dpi=200, bbox_inches="tight", transparent=True) + plt.close(fig) + buf.seek(0) + return buf.getvalue() + + +def render_circuit( + title: str, + qasm_or_circuit: Optional[Union[str, QuantumCircuit]], + style: Optional[Dict[str, str]] = None, +): + """ + Render circuit from a QASM string or an in-memory QuantumCircuit. + """ + st.markdown(f"**{title}**") -def kpi(label: str, value: int, delta_pct: float): - arrow = "↓" if delta_pct >= 0 else "↑" - st.markdown( - f""" -
-
{label}
-
{value}
-
{arrow} {abs(delta_pct):.1f}%
-
- """, - unsafe_allow_html=True, - ) + if qasm_or_circuit is None or ( + isinstance(qasm_or_circuit, str) and not qasm_or_circuit.strip() + ): + st.info("Circuit diagram will appear here.") + return -def bar_chart(before: dict, after: dict): - metrics = ["Circuit Cost", "Gate Count", "Depth"] - before_vals = [before["cost"], before["gate_count"], before["depth"]] - after_vals = [after["cost"], after["gate_count"], after["depth"]] + try: + if isinstance(qasm_or_circuit, QuantumCircuit): + qc = qasm_or_circuit + else: + qc = QuantumCircuit.from_qasm_str(qasm_or_circuit) + except Exception as e: + st.error(f"Failed to parse circuit: {e}") + if isinstance(qasm_or_circuit, str): + st.code(qasm_or_circuit) + return + + try: + img = circuit_to_png_bytes(qc, style=style) + st.image(img, use_container_width=True) + return + except Exception as e: + try: + text_diagram = qc.draw(output="text") + st.code(str(text_diagram)) + return + except Exception: + st.error(f"Failed to render circuit: {e}") + if isinstance(qasm_or_circuit, str): + st.code(qasm_or_circuit) + + +# ----------------------------- +# Fake Circuits for Demo +# ----------------------------- + +def make_fake_circuits(circuit_id: str): + """ + Generates fake before/after circuits for UI demo. + Replace this later with real RL output. + """ + if circuit_id == "half_adder": + before = QuantumCircuit(3, 2) + before.cx(0, 1) + before.cx(1, 2) + before.ccx(0, 1, 2) + before.measure(1, 0) + before.measure(2, 1) + + after = QuantumCircuit(3, 2) + after.cx(0, 1) + after.ccx(0, 1, 2) + after.measure(1, 0) + after.measure(2, 1) + + elif circuit_id == "parity": + before = QuantumCircuit(4, 1) + before.cx(0, 3) + before.cx(1, 3) + before.cx(2, 3) + before.measure(3, 0) + + after = QuantumCircuit(4, 1) + after.cx(0, 3) + after.cx(1, 3) + after.measure(3, 0) + + else: + before = QuantumCircuit(5) + before.h(0) + before.cx(0, 1) + before.rz(0.2, 1) + before.rz(0.3, 1) + before.cx(1, 2) + + after = QuantumCircuit(5) + after.h(0) + after.cx(0, 1) + after.rz(0.5, 1) + after.cx(1, 2) + + return before, after + + +# ----------------------------- +# Metrics helpers +# ----------------------------- + +def pct_change(before, after): + if before == 0: + return 0 + return (before - after) / before * 100 + + +def bar_chart(before, after): + metrics = ["Cost", "Gate Count", "Depth"] fig = go.Figure() - fig.add_bar(name="Before", x=metrics, y=before_vals) - fig.add_bar(name="After", x=metrics, y=after_vals) - fig.update_layout( - barmode="group", - height=340, - margin=dict(l=10, r=10, t=25, b=10), - paper_bgcolor="rgba(0,0,0,0)", - plot_bgcolor="rgba(0,0,0,0)", - font=dict(color="rgba(235,245,255,0.92)"), - legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), - ) + fig.add_bar(name="Before", x=metrics, + y=[before["cost"], before["gate_count"], before["depth"]], + width=0.25) + fig.add_bar(name="After", x=metrics, + y=[after["cost"], after["gate_count"], after["depth"]], + width=0.25) + fig.update_layout(barmode="group", height=350) st.plotly_chart(fig, use_container_width=True) -# ---------- Inject theme ---------- -BG_PATH = ASSETS / "bg.png" -LOGO_PATH = ASSETS / "QTrim_Logo.png" -inject_theme(BG_PATH) +# ----------------------------- +# Header +# ----------------------------- -# ---------- Header (FULL-WIDTH centered, visually corrected logo) ---------- -# Adjust this if the logo still looks slightly off-center due to PNG padding. -LOGO_NUDGE_PX = 18 # try 0, 10, 18, 24 until perfect on your screen -LOGO_WIDTH_PX = 280 # make it bigger here +logo_b64 = image_to_base64(ASSETS / "QTrim_Logo.png") +if logo_b64: + st.markdown( + f'', + unsafe_allow_html=True, + ) +else: + st.image(str(ASSETS / "QTrim_Logo.png"), width=240) -centered_logo_png(LOGO_PATH, width_px=LOGO_WIDTH_PX, nudge_px=LOGO_NUDGE_PX) +st.markdown("

QTrim

", unsafe_allow_html=True) +st.markdown("

Reinforcement Learning for Quantum Circuit Optimization

", unsafe_allow_html=True) -st.markdown( - """ -
QTrim
- """, - unsafe_allow_html=True, -) - -st.markdown( - """ -
Reinforcement Learning for Quantum Circuit Optimization
- """, - unsafe_allow_html=True, -) - -st.markdown("
", unsafe_allow_html=True) - -# ---------- Session state ---------- -if "selected_circuit_id" not in st.session_state: - st.session_state.selected_circuit_id = "half_adder" + +# ----------------------------- +# Session State +# ----------------------------- + +if "selected" not in st.session_state: + st.session_state.selected = "half_adder" if "before" not in st.session_state: - st.session_state.before = BASELINE["half_adder"].copy() + st.session_state.before = BASELINE["half_adder"] if "after" not in st.session_state: st.session_state.after = None if "before_qasm" not in st.session_state: @@ -205,112 +274,86 @@ def bar_chart(before: dict, after: dict): if "after_qasm" not in st.session_state: st.session_state.after_qasm = None -# ---------- Main layout ---------- -left, right = st.columns([0.28, 0.72], gap="large") -# LEFT PANEL: only circuit selection + run -with left: - st.markdown("
", unsafe_allow_html=True) - st.markdown("
Quantum Circuit Optimizer
", unsafe_allow_html=True) +# ----------------------------- +# Layout +# ----------------------------- + +left, right = st.columns([0.3, 0.7]) - circuit_label = st.selectbox("Circuit Type", list(CIRCUITS.keys())) - circuit_id = CIRCUITS[circuit_label] +# Left Panel +with left: + st.markdown("### Circuit Selection") + label = st.selectbox("Choose Algorithm", list(CIRCUITS.keys())) + circuit_id = CIRCUITS[label] - # Update baseline if selection changes - if circuit_id != st.session_state.selected_circuit_id: - st.session_state.selected_circuit_id = circuit_id - st.session_state.before = BASELINE[circuit_id].copy() + if circuit_id != st.session_state.selected: + st.session_state.selected = circuit_id + st.session_state.before = BASELINE[circuit_id] st.session_state.after = None - st.session_state.before_qasm = None - st.session_state.after_qasm = None - run = st.button("Run QTrim", use_container_width=True) - st.markdown("
", unsafe_allow_html=True) + if st.button("Run QTrim", use_container_width=True): - if run: payload = { - "circuit_id": st.session_state.selected_circuit_id, - "constraint_profile": "low_noise", # fixed for now per your requirements + "circuit_id": circuit_id, + "constraint_profile": "low_noise" } - with st.spinner("Optimizing..."): - try: - resp = requests.post(API_URL, json=payload, timeout=12) - resp.raise_for_status() - result = resp.json() - - st.session_state.before = result["before"] - st.session_state.after = result["after"] - - # Future: RL can return these - st.session_state.before_qasm = result.get("before_qasm") - st.session_state.after_qasm = result.get("after_qasm") - except Exception as e: - st.error(f"API call failed: {e}") - -# RIGHT PANEL: tabs (Circuit View / Optimization Metrics) -with right: - tabs = st.tabs(["Circuit View", "Optimization Metrics"]) - # --- Circuit View tab --- - with tabs[0]: - st.markdown("
", unsafe_allow_html=True) - st.markdown("
Circuit View
", unsafe_allow_html=True) + try: + resp = requests.post(API_URL, json=payload, timeout=8) + result = resp.json() - bcol, acol = st.columns(2, gap="large") + st.session_state.before = result["before"] + st.session_state.after = result["after"] - with bcol: - st.markdown("
Untrimmed Circuit
", unsafe_allow_html=True) - if st.session_state.before_qasm: - st.code(st.session_state.before_qasm, language="text") - else: - st.info("Circuit diagram placeholder (before)") - - with acol: - st.markdown("
Trimmed Circuit
", unsafe_allow_html=True) - if st.session_state.after_qasm: - st.code(st.session_state.after_qasm, language="text") - elif st.session_state.after: - st.info("Circuit diagram placeholder (after)") + # If backend provides QASM later, use it + if result.get("before_qasm") and result.get("after_qasm"): + st.session_state.before_qasm = result["before_qasm"] + st.session_state.after_qasm = result["after_qasm"] else: - st.info("Run QTrim to generate optimized circuit") + # Otherwise use fake demo circuits + bq, aq = make_fake_circuits(circuit_id) + st.session_state.before_qasm = bq + st.session_state.after_qasm = aq + + except Exception: + # If API fails, still generate fake circuits for demo + bq, aq = make_fake_circuits(circuit_id) + st.session_state.before_qasm = bq + st.session_state.after_qasm = aq + st.session_state.after = { + "gate_count": int(st.session_state.before["gate_count"] * 0.8), + "depth": int(st.session_state.before["depth"] * 0.8), + "cost": int(st.session_state.before["cost"] * 0.75), + } + + +# Right Panel +with right: - st.markdown("
", unsafe_allow_html=True) + tabs = st.tabs(["Circuit View", "Metrics"]) - # --- Optimization Metrics tab --- - with tabs[1]: - st.markdown("
", unsafe_allow_html=True) - st.markdown("
Optimization Metrics
", unsafe_allow_html=True) + # Circuit View + with tabs[0]: + col1, col2 = st.columns(2) + with col1: + render_circuit("Untrimmed Circuit", st.session_state.before_qasm, style=DEFAULT_CIRCUIT_STYLE) + with col2: + render_circuit("Trimmed Circuit", st.session_state.after_qasm, style=DEFAULT_CIRCUIT_STYLE) + # Metrics View + with tabs[1]: before = st.session_state.before after = st.session_state.after - k1, k2, k3 = st.columns(3, gap="medium") - if after: - d_cost = pct_change(before["cost"], after["cost"]) - d_gate = pct_change(before["gate_count"], after["gate_count"]) - d_depth = pct_change(before["depth"], after["depth"]) - - with k1: kpi("Circuit Cost", after["cost"], d_cost) - with k2: kpi("Gate Count", after["gate_count"], d_gate) - with k3: kpi("Depth", after["depth"], d_depth) - - df = pd.DataFrame( - [ - ["Gate Count", before["gate_count"], after["gate_count"], f"{d_gate:.1f}%"], - ["Depth", before["depth"], after["depth"], f"{d_depth:.1f}%"], - ["Circuit Cost", before["cost"], after["cost"], f"{d_cost:.1f}%"], - ], - columns=["Metric", "Before", "After", "Δ %"], - ) - st.dataframe(df, use_container_width=True, hide_index=True) - bar_chart(before, after) + df = pd.DataFrame([ + ["Gate Count", before["gate_count"], after["gate_count"], f"{pct_change(before['gate_count'], after['gate_count']):.1f}%"], + ["Depth", before["depth"], after["depth"], f"{pct_change(before['depth'], after['depth']):.1f}%"], + ["Cost", before["cost"], after["cost"], f"{pct_change(before['cost'], after['cost']):.1f}%"], + ], columns=["Metric", "Before", "After", "Improvement"]) - st.success(f"Optimization reduced circuit cost by {d_cost:.1f}%.") + st.dataframe(df, use_container_width=True) + bar_chart(before, after) else: - with k1: kpi("Circuit Cost", before["cost"], 0.0) - with k2: kpi("Gate Count", before["gate_count"], 0.0) - with k3: kpi("Depth", before["depth"], 0.0) - st.info("Run QTrim to populate metrics.") - - st.markdown("
", unsafe_allow_html=True) + st.info("Run QTrim to generate results.") diff --git a/requirements.txt b/requirements.txt index e7ee564..e4cef6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ torch>=2.1.0 fastapi>=0.110.0 uvicorn[standard]>=0.27.0 streamlit>=1.31.0 +pylatexenc>=2.10 # Optional utilities numpy>=1.26.0