Skip to content

Commit 9ba5557

Browse files
debug: diagnostic for Ipopt↔MadNLP global-phase basin divergence
Identified by Gennadi. abs2(tr(U_goal'·U))/n² is phase-insensitive, so +im·U_goal and -im·U_goal are equally valid optima. With free Δt, solvers fall into different basins depending on init + step sequence; with fixed Δt, both consistently land in the same basin.
1 parent 8b50061 commit 9ba5557

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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

Comments
 (0)