|
| 1 | +# Diagnose global phase bifurcation between Ipopt and MadNLP |
| 2 | +# |
| 3 | +# Run this AFTER the main comparison script has populated: |
| 4 | +# qcp_ipopt, qcp_madnlp, integrator, qtraj |
| 5 | +# |
| 6 | +# Or run standalone — it sets everything up. |
| 7 | + |
| 8 | +import Ipopt |
| 9 | +import MadNLP |
| 10 | + |
| 11 | +using DirectTrajOpt |
| 12 | +using Piccolo |
| 13 | +using Piccolissimo |
| 14 | +using LinearAlgebra |
| 15 | + |
| 16 | +function _get_MadNLPSolverExt() |
| 17 | + cur_mods = reverse(Base.loaded_modules_order) |
| 18 | + candidate_mods = [n for n = cur_mods if Symbol(n) == :MadNLPSolverExt] |
| 19 | + length(candidate_mods) == 1 || error("Expected 1 MadNLPSolverExt, found $(length(candidate_mods))") |
| 20 | + return candidate_mods[1] |
| 21 | +end |
| 22 | +const MadNLPSolverExt = _get_MadNLPSolverExt() |
| 23 | + |
| 24 | +# ---------------------------------------------------------------------------- # |
| 25 | +# Setup: same problem for both solvers |
| 26 | +# ---------------------------------------------------------------------------- # |
| 27 | + |
| 28 | +H_drift = 0.5 * PAULIS.Z |
| 29 | +H_drives = [PAULIS.X, PAULIS.Y] |
| 30 | +drive_bounds = [1., 1.] |
| 31 | +sys = QuantumSystem(H_drift, H_drives, drive_bounds) |
| 32 | +T = 10.0 |
| 33 | +N = 100 |
| 34 | +U_goal = GATES.X |
| 35 | + |
| 36 | +qtraj = UnitaryTrajectory(sys, U_goal, T) |
| 37 | +integrator = HermitianExponentialIntegrator(qtraj, N) |
| 38 | + |
| 39 | +# Build two identical problems |
| 40 | +qcp = SmoothPulseProblem(qtraj, N; Q=1e2, R=1e-2, ddu_bound=1e0, Δt_bounds=(0.5, 1.5), integrator=integrator) |
| 41 | +qcp_ipopt = deepcopy(qcp) |
| 42 | +qcp_madnlp = deepcopy(qcp) |
| 43 | + |
| 44 | +# ---------------------------------------------------------------------------- # |
| 45 | +# Solve |
| 46 | +# ---------------------------------------------------------------------------- # |
| 47 | + |
| 48 | +println("\n" * "="^70) |
| 49 | +println("Solving with Ipopt...") |
| 50 | +println("="^70) |
| 51 | +solve!(qcp_ipopt; options=IpoptOptions(max_iter=100)) |
| 52 | + |
| 53 | +println("\n" * "="^70) |
| 54 | +println("Solving with MadNLP...") |
| 55 | +println("="^70) |
| 56 | +solve!(qcp_madnlp; options=MadNLPSolverExt.MadNLPOptions(max_iter=100)) |
| 57 | + |
| 58 | +# ---------------------------------------------------------------------------- # |
| 59 | +# Extract final unitaries from the solver trajectory data |
| 60 | +# ---------------------------------------------------------------------------- # |
| 61 | + |
| 62 | +function get_final_unitary(qcp) |
| 63 | + traj = get_trajectory(qcp) |
| 64 | + # The state Ũ⃗ at the final knot point |
| 65 | + Ũ⃗_final = traj[:Ũ⃗][:, end] |
| 66 | + return iso_vec_to_operator(Ũ⃗_final) |
| 67 | +end |
| 68 | + |
| 69 | +U_ipopt = get_final_unitary(qcp_ipopt) |
| 70 | +U_madnlp = get_final_unitary(qcp_madnlp) |
| 71 | + |
| 72 | +# ---------------------------------------------------------------------------- # |
| 73 | +# Diagnose: what phase did each solver converge to? |
| 74 | +# ---------------------------------------------------------------------------- # |
| 75 | + |
| 76 | +println("\n" * "="^70) |
| 77 | +println("PHASE ANALYSIS") |
| 78 | +println("="^70) |
| 79 | + |
| 80 | +# The trace inner product tr(U_goal' * U_final) |
| 81 | +# For a perfect solution: tr(X' * (α·X)) = α·tr(I) = 2α |
| 82 | +# where α = ±i (from Schrödinger evolution: U = exp(-iHt)) |
| 83 | +trace_ipopt = tr(U_goal' * U_ipopt) |
| 84 | +trace_madnlp = tr(U_goal' * U_madnlp) |
| 85 | + |
| 86 | +println("\ntr(U_goal' * U_final):") |
| 87 | +println(" Ipopt: $trace_ipopt") |
| 88 | +println(" MadNLP: $trace_madnlp") |
| 89 | + |
| 90 | +println("\nPhase angle (arg of trace):") |
| 91 | +println(" Ipopt: $(angle(trace_ipopt)) rad ($(rad2deg(angle(trace_ipopt)))°)") |
| 92 | +println(" MadNLP: $(angle(trace_madnlp)) rad ($(rad2deg(angle(trace_madnlp)))°)") |
| 93 | + |
| 94 | +println("\nPhase sign diagnostic (im * tr / |tr|):") |
| 95 | +phase_ipopt = trace_ipopt / abs(trace_ipopt) |
| 96 | +phase_madnlp = trace_madnlp / abs(trace_madnlp) |
| 97 | +println(" Ipopt: $phase_ipopt") |
| 98 | +println(" MadNLP: $phase_madnlp") |
| 99 | + |
| 100 | +println("\nFidelity (abs2-based, phase-insensitive):") |
| 101 | +n = size(U_goal, 1) |
| 102 | +fid_ipopt = abs2(trace_ipopt) / n^2 |
| 103 | +fid_madnlp = abs2(trace_madnlp) / n^2 |
| 104 | +println(" Ipopt: $fid_ipopt") |
| 105 | +println(" MadNLP: $fid_madnlp") |
| 106 | + |
| 107 | +println("\nFidelity via Piccolo API:") |
| 108 | +println(" Ipopt: $(fidelity(qcp_ipopt))") |
| 109 | +println(" MadNLP: $(fidelity(qcp_madnlp))") |
| 110 | + |
| 111 | +# ---------------------------------------------------------------------------- # |
| 112 | +# Check if solvers converged to different phase basins |
| 113 | +# ---------------------------------------------------------------------------- # |
| 114 | + |
| 115 | +phase_diff = angle(trace_ipopt) - angle(trace_madnlp) |
| 116 | +# Normalize to [-π, π] |
| 117 | +phase_diff = mod(phase_diff + π, 2π) - π |
| 118 | + |
| 119 | +println("\n" * "="^70) |
| 120 | +println("PHASE DIFFERENCE BETWEEN SOLVERS") |
| 121 | +println("="^70) |
| 122 | +println(" Δφ = $(phase_diff) rad ($(rad2deg(phase_diff))°)") |
| 123 | +if abs(phase_diff) > π/2 |
| 124 | + println(" ⚠ SOLVERS CONVERGED TO DIFFERENT PHASE BASINS") |
| 125 | + println(" This is the bifurcation Gennadi identified.") |
| 126 | + println(" The abs2 in the objective makes ±(im·U_goal) equivalent.") |
| 127 | +else |
| 128 | + println(" ✓ Solvers converged to the same phase basin this time.") |
| 129 | + println(" Re-run to potentially observe bifurcation (depends on numerics).") |
| 130 | +end |
| 131 | + |
| 132 | +# ---------------------------------------------------------------------------- # |
| 133 | +# Show the actual final unitaries for inspection |
| 134 | +# ---------------------------------------------------------------------------- # |
| 135 | + |
| 136 | +println("\n" * "="^70) |
| 137 | +println("FINAL UNITARIES") |
| 138 | +println("="^70) |
| 139 | + |
| 140 | +println("\nU_goal = X gate:") |
| 141 | +display(U_goal) |
| 142 | + |
| 143 | +println("\n\nU_final (Ipopt):") |
| 144 | +display(U_ipopt) |
| 145 | + |
| 146 | +println("\n\nU_final (MadNLP):") |
| 147 | +display(U_madnlp) |
| 148 | + |
| 149 | +println("\n\nU_final / (im * U_goal) — should be ≈ ±1 * I if converged:") |
| 150 | +println("\n Ipopt: U / (i·X) =") |
| 151 | +display(U_ipopt / (im * U_goal)) |
| 152 | +println("\n MadNLP: U / (i·X) =") |
| 153 | +display(U_madnlp / (im * U_goal)) |
| 154 | + |
| 155 | +# ---------------------------------------------------------------------------- # |
| 156 | +# Constraint violations (for completeness) |
| 157 | +# ---------------------------------------------------------------------------- # |
| 158 | + |
| 159 | +println("\n" * "="^70) |
| 160 | +println("CONSTRAINT VIOLATIONS") |
| 161 | +println("="^70) |
| 162 | + |
| 163 | +deltas_ipopt = zeros(integrator.dim) |
| 164 | +deltas_madnlp = zeros(integrator.dim) |
| 165 | +DirectTrajOpt.evaluate!(deltas_ipopt, integrator, get_trajectory(qcp_ipopt)) |
| 166 | +DirectTrajOpt.evaluate!(deltas_madnlp, integrator, get_trajectory(qcp_madnlp)) |
| 167 | + |
| 168 | +println(" max |constraint| Ipopt: $(maximum(abs.(deltas_ipopt)))") |
| 169 | +println(" max |constraint| MadNLP: $(maximum(abs.(deltas_madnlp)))") |
| 170 | + |
| 171 | +# ---------------------------------------------------------------------------- # |
| 172 | +# Datavec comparison |
| 173 | +# ---------------------------------------------------------------------------- # |
| 174 | + |
| 175 | +println("\n" * "="^70) |
| 176 | +println("TRAJECTORY DIVERGENCE") |
| 177 | +println("="^70) |
| 178 | + |
| 179 | +data_ipopt = get_trajectory(qcp_ipopt).data |
| 180 | +data_madnlp = get_trajectory(qcp_madnlp).data |
| 181 | +max_diff = maximum(abs.(data_ipopt .- data_madnlp)) |
| 182 | +println(" max |data_ipopt - data_madnlp| = $max_diff") |
| 183 | + |
| 184 | +println("\n" * "="^70) |
| 185 | +println("DIAGNOSIS COMPLETE") |
| 186 | +println("="^70) |
| 187 | +println(""" |
| 188 | +The objective `abs2(tr(U_goal' * U)) / n^2` at: |
| 189 | + qc2/src/quantum_objectives.jl:49 |
| 190 | +
|
| 191 | +is phase-insensitive: abs2(z) = abs2(-z) = abs2(iz) = ... |
| 192 | +Both +im*U_goal and -im*U_goal are equally valid optima. |
| 193 | +
|
| 194 | +When Δt is a free variable, the expanded search space creates |
| 195 | +multiple basins of attraction. The solver's early numerical |
| 196 | +trajectory (from initialization + step selection) determines |
| 197 | +which basin it falls into. |
| 198 | +
|
| 199 | +With fixed Δt, the landscape is more constrained and both |
| 200 | +solvers consistently find the same basin. |
| 201 | +""") |
0 commit comments