File size: 14,921 Bytes
d3527de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""
Equality-Constrained QAOA Module β€” 3 qubits.

Problem:  Minimize  C(x) = x1 + x2 - x2*x3
          Subject to:  x1 + x2 + x3 = 1
Penalty:  P(x) = A * (x1 + x2 + x3 - 1)^2,  A = 3.0
Total:    C_total(x) = C(x) + P(x)
Target:   |001⟩  (only valid state with lowest objective cost)
"""

import numpy as np
import pandas as pd
import plotly.graph_objects as go
import streamlit as st

from core.costs import cost_equality, cost_standard
from core.simulator import simulate_qaoa, get_statevector_after_init, get_statevector_after_cost
from core.optimizer import run_cobyla
from viz.disc_viz import show_disc
from viz.probabilities import plot_probabilities
from viz.circuits import draw_circuit

_N = 3
_MODE = "equality"
_A = 3.0


def _truth_table() -> pd.DataFrame:
    rows = []
    for i in range(2 ** _N):
        bits = [int(b) for b in format(i, f"0{_N}b")]
        c_obj = cost_standard(*bits)
        constraint_sum = sum(bits)
        penalty = _A * (constraint_sum - 1) ** 2
        c_total = c_obj + penalty
        feasible = "βœ…" if constraint_sum == 1 else "❌"
        rows.append({
            "State": f"|{format(i, f'0{_N}b')}⟩",
            "x₁+xβ‚‚+x₃": constraint_sum,
            "Feasible?": feasible,
            "C_obj": c_obj,
            "Penalty": penalty,
            "C_total": c_total,
        })
    return pd.DataFrame(rows)


def _hamiltonian_df():
    costs = [cost_equality(*[int(b) for b in format(i, f"0{_N}b")]) for i in range(2 ** _N)]
    df = pd.DataFrame(
        np.diag(costs),
        columns=[f"|{format(i, f'0{_N}b')}⟩" for i in range(2 ** _N)],
        index=[f"⟨{format(i, f'0{_N}b')}|" for i in range(2 ** _N)],
    )
    return df, costs


def render():
    # ── Back button ─────────────────────────────────────────────────────────
    if st.button("← Back to Home"):
        st.session_state["page"] = "home"
        st.rerun()

    st.markdown('<h1 style="color:#7C3AED;">Constrained QAOA β€” Equality Penalty</h1>',
                unsafe_allow_html=True)
    st.caption("3-qubit Β· penalty method Β· fully-connected K₃ cost layer Β· sv_disc visualization")

    # ════════════════════════════════════════════════════════════════════════
    # STEP 1 β€” Problem Formulation
    # ════════════════════════════════════════════════════════════════════════
    st.markdown("---")
    st.markdown("## Step 1 Β· Problem Formulation")
    col1, col2 = st.columns(2)
    with col1:
        st.info(r"**Minimize** $C(x) = x_1 + x_2 - x_2 x_3$")
        st.error(r"**Subject to** $x_1 + x_2 + x_3 = 1$")
        st.markdown(
            "Only one qubit may be $|1\\rangle$ at a time. "
            "Among the feasible states, $|001\\rangle$ achieves the lowest objective value."
        )
    with col2:
        st.markdown("**Feasibility & Cost Table**")
        st.dataframe(_truth_table(), hide_index=True)

    # ════════════════════════════════════════════════════════════════════════
    # STEP 2 β€” Penalty Method & QUBO
    # ════════════════════════════════════════════════════════════════════════
    st.markdown("---")
    st.markdown("## Step 2 Β· Penalty Method & QUBO")
    st.write(
        "We handle the equality constraint by adding a **quadratic penalty** "
        "to the objective. Infeasible states receive a large cost that the optimizer avoids."
    )
    st.latex(r"""
C_{\text{total}}(x) = C(x) + A\,(x_1 + x_2 + x_3 - 1)^2, \quad A = 3.0
""")

    with st.expander("How do we choose A = 3.0?"):
        st.markdown(
            "The penalty weight $A$ must be **large enough** that the penalized cost of any "
            "infeasible state exceeds the cost of the best feasible state. "
            "Here, the best feasible cost is $0$ and the smallest infeasible penalty is "
            "$3.0 \\times 1 = 3.0$, which dominates. "
            "Too small an $A$ and the optimizer ignores the constraint; "
            "too large and the landscape becomes steep, slowing convergence."
        )

    st.markdown("#### Expanded QUBO")
    st.write(
        "Expanding $(x_1 + x_2 + x_3 - 1)^2$ introduces **cross terms** between all pairs of "
        "qubits β€” these become the $R_{zz}$ gates in the cost circuit:"
    )
    st.latex(r"""
(x_1+x_2+x_3-1)^2 = x_1^2 + x_2^2 + x_3^2 + 2x_1x_2 + 2x_1x_3 + 2x_2x_3 - 2x_1 - 2x_2 - 2x_3 + 1
""")
    st.latex(r"""
\xrightarrow{x_i^2 = x_i} \;
x_1 + x_2 + x_3 + 2x_1x_2 + 2x_1x_3 + 2x_2x_3 - 2x_1 - 2x_2 - 2x_3 + 1
""")
    st.info("Every pair $(x_i, x_j)$ now appears β€” the QUBO matrix is **fully dense** (K₃ topology).")

    # ════════════════════════════════════════════════════════════════════════
    # STEP 3 β€” Hamiltonian
    # ════════════════════════════════════════════════════════════════════════
    st.markdown("---")
    st.markdown("## Step 3 Β· Penalized Hamiltonian Matrix")
    st.write("The diagonal of $H_C$ now reflects the total penalized cost:")
    diag_df, costs = _hamiltonian_df()
    min_c = min(costs)
    styled = diag_df.style.format("{:.1f}").map(
        lambda v: "background-color:#ede9fe;color:#4c1d95;font-weight:bold;" if np.isclose(v, min_c)
        else ("background-color:#fee2e2;color:#b91c1c;font-weight:bold;" if v > 5
              else ("color:black;" if v != 0 else "color:#94a3b8;"))
    )
    st.dataframe(styled, height=310)

    with st.expander("Worked example: why does |111⟩ cost 13?"):
        st.latex(r"""
C_{\text{total}}(1,1,1) = \underbrace{1+1-1}_{C_{\text{obj}}=1}
+ 3.0\,(1+1+1-1)^2 = 1 + 3.0 \times 4 = 13.0
""")

    # ════════════════════════════════════════════════════════════════════════
    # STEP 4 β€” Circuit Construction + Phase-Probability Disconnect
    # ════════════════════════════════════════════════════════════════════════
    st.markdown("---")
    st.markdown("## Step 4 Β· Circuit Construction & Phase-Probability Disconnect")

    st.markdown("#### Connectivity vs. Standard QAOA")
    st.markdown(
        "| Module | $R_z$ gates | $R_{zz}$ gates | Topology |\n"
        "|--------|------------|----------------|----------|\n"
        "| Standard | 2 | 1 | Sparse (1 edge) |\n"
        "| **Equality** | **3** | **3** | **K₃ fully connected** |"
    )
    st.write(
        "Each $R_{zz}$ corresponds to a cross term in the penalty expansion. "
        "More entangling gates = higher hardware cost = pedagogical contrast."
    )

    st.markdown("#### Stage A β€” Initialization")
    _, c1, c2 = st.columns([1,1, 2])
    with c1:
        fig_init = draw_circuit("init", _MODE)
        st.pyplot(fig_init, width=300)
    with c2:
        show_disc(get_statevector_after_init(_N),
                  caption="Equal superposition: all radii identical, all phases zero.",
                  phase_colors=True)

    st.markdown("#### Stage B β€” Cost Layer (Phases Rotate, Probabilities Flat)")
    st.warning(
        "The penalty Hamiltonian has **larger eigenvalue spread** than the standard one, "
        "causing faster phase rotation for high-cost states like $|111\\rangle$. "
        "Yet the probability bar chart remains **perfectly flat** β€” pure phase encoding."
    )
    c3, c4 = st.columns([1, 1])
    with c3:
        st.pyplot(draw_circuit("cost", _MODE), width="stretch")
        g_cost = st.slider("Ξ³ (Cost Layer demo)", 0.0, 6.28, 0.6, 0.05, key="eq_g_cost")
    with c4:
        sv_cost = get_statevector_after_cost(g_cost, cost_equality, _N)
        show_disc(sv_cost,
                  caption="Large penalty β†’ fast phase rotation for high-cost states.",
                  phase_colors=True)
        probs_cost = np.array([np.abs(a) ** 2 for a in sv_cost.data])
        _, _, cv_cost, _ = simulate_qaoa(g_cost, 0.0, cost_equality, _N, shots=1, seed=0)
        st.plotly_chart(
            plot_probabilities(probs_cost, cv_cost, _N,
                               title="Probabilities (flat β€” phase only)", shots=1024),
            width="stretch"
        )

    st.markdown("#### Stage C β€” Mixer Layer (Selective Amplitude Amplification)")
    st.success(
        "With high-penalty phases on infeasible states, the mixer's interference **suppresses** "
        "their amplitudes and **amplifies** feasible states. "
        "QAOA effectively learns to 'steer' probability mass toward the constraint-satisfying region."
    )
    c5, c6 = st.columns([1, 1])
    with c5:
        st.pyplot(draw_circuit("full", _MODE), width="stretch")
        g_mix = st.slider("Ξ³", 0.0, 6.28, 0.6, 0.05, key="eq_g_mix")
        b_mix = st.slider("Ξ²", 0.0, 3.14, 0.5, 0.05, key="eq_b_mix")
    with c6:
        probs_mix, _, cv_mix, sv_mix = simulate_qaoa(g_mix, b_mix, cost_equality, _N,
                                                      shots=1024, seed=200)
        show_disc(sv_mix, caption="After mixer: feasible states gain amplitude.", phase_colors=True)
        st.plotly_chart(
            plot_probabilities(probs_mix, cv_mix, _N,
                               title="Probabilities (feasible states amplified)", shots=1024),
            width="stretch"
        )

    # ════════════════════════════════════════════════════════════════════════
    # STEP 5 β€” Interactive Simulation
    # ════════════════════════════════════════════════════════════════════════
    st.markdown("---")
    st.markdown("## Step 5 Β· Interactive Simulation")
    col_ctrl, col_vis = st.columns([1, 2])
    with col_ctrl:
        st.subheader("Parameters")
        gamma5 = st.slider("Ξ³", 0.0, 6.28, 0.6, 0.05, key="eq_gamma5")
        beta5  = st.slider("Ξ²", 0.0, 3.14, 0.5, 0.05, key="eq_beta5")
        shots5 = st.select_slider("Shots", [128, 512, 1024, 4096], value=1024, key="eq_shots5")
        probs5, energy5, cv5, sv5 = simulate_qaoa(gamma5, beta5, cost_equality,
                                                   _N, shots=shots5, seed=42)
        st.metric("Expected Total Cost ⟨C_total⟩", f"{energy5:.4f}")
    with col_vis:
        st.plotly_chart(plot_probabilities(probs5, cv5, _N, shots=shots5),
                        width="stretch")
        show_disc(sv5, caption="Current statevector.", phase_colors=True)

    # ════════════════════════════════════════════════════════════════════════
    # STEP 6 β€” COBYLA Optimization + Replay
    # ════════════════════════════════════════════════════════════════════════
    st.markdown("---")
    st.markdown("## Step 6 Β· COBYLA Optimization & Replay")

    if st.button("β–Ά Run COBYLA Optimizer", key="eq_run_opt"):
        with st.spinner("Optimizing… (up to 60 iterations)"):
            res, hist = run_cobyla(cost_equality, _N, mode="equality")
            st.session_state["eq_history"] = hist
            st.session_state["eq_opt_done"] = True
        st.success(f"Done β€” {len(hist['energy'])} calls Β· final ⟨C_total⟩ = {hist['energy'][-1]:.4f}")

    if st.session_state.get("eq_opt_done"):
        hist = st.session_state["eq_history"]

        fig_conv = go.Figure()
        fig_conv.add_trace(go.Scatter(
            x=list(range(len(hist["energy"]))), y=hist["energy"],
            mode="lines+markers", line=dict(color="#7C3AED", width=2)
        ))
        fig_conv.update_layout(
            title="COBYLA Convergence", xaxis_title="Iteration",
            yaxis_title="⟨C_total⟩",
            paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(245,243,255,0.5)",
            height=300, margin=dict(l=20, r=20, t=40, b=20)
        )
        st.plotly_chart(fig_conv, width="stretch")

        g_fin = hist["gamma"][-1]; b_fin = hist["beta"][-1]
        pf, _, _, _ = simulate_qaoa(g_fin, b_fin, cost_equality, _N, shots=8192, seed=999)
        winner = np.argmax(pf)
        feasible_check = "βœ… Feasible" if sum(int(b) for b in format(winner, f"0{_N}b")) == 1 else "⚠ Infeasible"
        st.markdown(
            f'<div style="background:#f5f3ff;padding:18px;border:2px solid #7c3aed;'
            f'border-radius:10px;text-align:center;">'
            f'<div style="font-size:1.1rem;color:#4c1d95;">Most Probable State</div>'
            f'<div style="font-size:2.2rem;font-weight:800;font-family:monospace;color:#6d28d9;">'
            f'|{format(winner, f"0{_N}b")}⟩</div>'
            f'<div style="color:#7c3aed;">{feasible_check} Β· Probability: {pf[winner]:.2%} Β· '
            f'Ξ³* = {g_fin:.3f} Β· Ξ²* = {b_fin:.3f}</div></div>',
            unsafe_allow_html=True
        )

        st.markdown("### Replay Optimization")
        step = st.slider("Iteration", 0, len(hist["energy"]) - 1, 0, key="eq_replay")
        g_s = hist["gamma"][step]; b_s = hist["beta"][step]
        p_s, _, cv_s, sv_s = simulate_qaoa(g_s, b_s, cost_equality, _N, shots=2048, seed=step)
        col_r1, col_r2 = st.columns(2)
        with col_r1:
            st.plotly_chart(
                plot_probabilities(p_s, cv_s, _N,
                                   title=f"Iteration {step}", shots=2048),
                width="stretch"
            )
        with col_r2:
            show_disc(sv_s,
                      caption=f"Statevector at iteration {step}  (Ξ³={g_s:.3f}, Ξ²={b_s:.3f})",
                      phase_colors=True)