Working with Solutions
This guide covers how to solve problems, extract data from solutions, evaluate performance, and save/load results.
using QuantumCollocation
using PiccoloQuantumObjects
using NamedTrajectoriesSolving Problems
Once you've created a problem template, solving is straightforward with the solve! function:
system = QuantumSystem(0.1 * PAULIS.Z, [PAULIS.X, PAULIS.Y], [1.0, 1.0])
U_goal = EmbeddedOperator(GATES.H, system)
T = 10.0 # time duration
qtraj = UnitaryTrajectory(system, U_goal, T)
N = 51 # number of timesteps
prob = SmoothPulseProblem(qtraj, N)QuantumControlProblem{PiccoloQuantumObjects.QuantumTrajectories.UnitaryTrajectory{PiccoloQuantumObjects.Pulses.ZeroOrderPulse{DataInterpolations.ConstantInterpolation{Matrix{Float64}, Vector{Float64}, Vector{Union{}}, Float64}}, SciMLBase.ODESolution{ComplexF64, 3, Vector{Matrix{ComplexF64}}, Nothing, Nothing, Vector{Float64}, Vector{Vector{Matrix{ComplexF64}}}, Nothing, SciMLBase.ODEProblem{Matrix{ComplexF64}, Tuple{Float64, Float64}, true, SciMLBase.NullParameters, SciMLBase.ODEFunction{true, SciMLBase.FullSpecialize, SciMLOperators.MatrixOperator{ComplexF64, Matrix{ComplexF64}, SciMLOperators.FilterKwargs{Nothing, Val{()}}, SciMLOperators.FilterKwargs{PiccoloQuantumObjects.Rollouts.var"#update!#_construct_operator##2"{PiccoloQuantumObjects.QuantumSystems.QuantumSystem{PiccoloQuantumObjects.QuantumSystems.var"#26#27"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, PiccoloQuantumObjects.QuantumSystems.var"#28#29"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}}}, Val{()}}}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, PiccoloQuantumObjects.Rollouts.PiccoloRolloutSystem{Union{Int64, AbstractVector{Int64}, CartesianIndex, CartesianIndices}}, Nothing, Nothing}, Base.Pairs{Symbol, Vector{Float64}, Nothing, @NamedTuple{tstops::Vector{Float64}, saveat::Vector{Float64}}}, SciMLBase.StandardODEProblem}, OrdinaryDiffEqLinear.MagnusGL4, OrdinaryDiffEqCore.InterpolationData{SciMLBase.ODEFunction{true, SciMLBase.FullSpecialize, SciMLOperators.MatrixOperator{ComplexF64, Matrix{ComplexF64}, SciMLOperators.FilterKwargs{Nothing, Val{()}}, SciMLOperators.FilterKwargs{PiccoloQuantumObjects.Rollouts.var"#update!#_construct_operator##2"{PiccoloQuantumObjects.QuantumSystems.QuantumSystem{PiccoloQuantumObjects.QuantumSystems.var"#26#27"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, PiccoloQuantumObjects.QuantumSystems.var"#28#29"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}}}, Val{()}}}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, PiccoloQuantumObjects.Rollouts.PiccoloRolloutSystem{Union{Int64, AbstractVector{Int64}, CartesianIndex, CartesianIndices}}, Nothing, Nothing}, Vector{Matrix{ComplexF64}}, Vector{Float64}, Vector{Vector{Matrix{ComplexF64}}}, Nothing, OrdinaryDiffEqLinear.MagnusGL4Cache{Matrix{ComplexF64}, Matrix{ComplexF64}, Matrix{ComplexF64}, Nothing}, Nothing}, SciMLBase.DEStats, Nothing, Nothing, Nothing, Nothing}, PiccoloQuantumObjects.EmbeddedOperators.EmbeddedOperator{ComplexF64}}}
System: PiccoloQuantumObjects.QuantumSystems.QuantumSystem{PiccoloQuantumObjects.QuantumSystems.var"#26#27"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, PiccoloQuantumObjects.QuantumSystems.var"#28#29"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}}
Goal: PiccoloQuantumObjects.EmbeddedOperators.EmbeddedOperator{ComplexF64}
Trajectory: 51 knots
State: Ũ⃗
Controls: uThe solve! function accepts several key options:
solve!(prob;
max_iter = 100, # Maximum optimization iterations
verbose = true, # Print convergence information
print_level = 1 # Ipopt output level (0-5)
) initializing optimizer...
applying constraint: timesteps all equal constraint
applying constraint: initial value of Ũ⃗
applying constraint: initial value of u
applying constraint: final value of u
applying constraint: bounds on Ũ⃗
applying constraint: bounds on u
applying constraint: bounds on du
applying constraint: bounds on ddu
applying constraint: bounds on Δt
applying constraint: time consistency constraint (t_{k+1} = t_k + Δt_k)
applying constraint: initial time t₁ = 0
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit https://github.com/coin-or/Ipopt
******************************************************************************Understanding Convergence
Ipopt reports several key metrics:
- Objective: Current cost function value
- inf_pr: Constraint violation (primal infidelity) - should go to ~0
- inf_du: Dual infidelity - measure of optimality
- lg(mu): Log of barrier parameter
- alphadu/alphapr: Step sizes
Successful convergence typically shows inf_pr < 1e-6 and status Optimal Solution Found.
Extracting Data from Solutions
Accessing Controls
Control pulses are stored in the trajectory with automatic naming:
u = prob.trajectory.u # Control values [n_drives × N]
du = prob.trajectory.du # First derivatives
ddu = prob.trajectory.ddu # Second derivatives
println("Control shape: ", size(u))
println("Number of drives: ", size(u, 1))
println("Number of timesteps: ", size(u, 2))Control shape: (2, 51)
Number of drives: 2
Number of timesteps: 51Access individual drive controls:
u_drive_1 = u[1, :] # First drive over time51-element Vector{Float64}:
0.0
0.09102972396310519
0.1683407407610345
0.22596245578236104
0.26220656442967155
0.2778324893716056
0.27519111197603585
0.25758291699716895
0.22860014092405484
0.19166799775224544
⋮
0.20730834935883788
0.20598862060908002
0.19709453853529052
0.18042351020245914
0.1560649265242895
0.12450022846037355
0.0867252119063542
0.04439604362165721
0.0Accessing States
For unitary problems:
Ũ⃗ = prob.trajectory.Ũ⃗ # Vectorized unitary [N² × T]8×51 view(reshape(view(::Vector{Float64}, :), 16, 51), 1:8, :) with eltype Float64:
1.0 0.999779 0.998895 0.996209 … 0.0214745 1.65558e-5
0.0 -4.23077e-32 -0.0092729 -0.0270897 -0.00825884 4.5671e-6
0.0 -0.0210169 -0.0420183 -0.0629547 -0.709828 -0.707091
0.0 -3.48142e-33 -0.0189377 -0.0537388 -0.703971 -0.707095
0.0 9.17594e-33 0.0092729 0.0270897 0.00825884 -4.5671e-6
1.0 0.999779 0.998895 0.996209 … 0.0214745 1.65558e-5
0.0 5.63116e-34 -0.0189377 -0.0537388 -0.703971 -0.707095
0.0 0.0210169 0.0420183 0.0629547 0.709828 0.707091The unitary is stored in "isovec" format (vectorized). To get the actual unitary matrix at timestep k:
using LinearAlgebra
k = N # Final timestep
U_k = iso_vec_to_operator(Ũ⃗[:, k])
println("Final unitary dimensions: ", size(U_k))Final unitary dimensions: (2, 2)For ket (state) problems, use: ψ̃ = prob.trajectory.ψ̃ # Vectorized state [2N × T]
Time Grid
Access timestep information:
Δt_vec = prob.trajectory.Δt # Timestep durations1×51 view(reshape(view(::Vector{Float64}, :), 16, 51), 9:9, :) with eltype Float64:
0.210184 0.210184 0.210184 0.210184 … 0.210184 0.210184 0.210184Calculate total duration:
duration = get_duration(prob.trajectory)
println("Total gate time: ", duration, " (arbitrary units)")Total gate time: 10.509209626401601 (arbitrary units)For minimum time problems, timesteps vary: minprob = UnitaryMinimumTimeProblem(prob, Ugoal) solve!(minprob, maxiter=100) Δtoptimized = minprob.trajectory.Δt # Variable timesteps
Evaluating Solutions
Fidelity Calculations
Direct fidelity - Compare final state to goal:
U_final = iso_vec_to_operator(prob.trajectory.Ũ⃗[:, end])
fid_direct = unitary_fidelity(U_final, U_goal.operator)
println("Direct fidelity: ", fid_direct)Direct fidelity: 0.9999607437739154Rollout fidelity - Simulate dynamics forward:
fid_rollout = unitary_rollout_fidelity(prob.trajectory, system)
println("Rollout fidelity: ", fid_rollout)Rollout fidelity: 0.9997937552347185The rollout fidelity is more accurate as it accounts for actual dynamics, while direct fidelity only checks the final point.
For Embedded Operators (Multilevel Systems)
When working with subspaces (e.g., qubit in transmon): op = EmbeddedOperator(:X, system) probembedded = UnitarySmoothPulseProblem(system, op, N) solve!(probembedded, max_iter=100)
Evaluate fidelity only in computational subspace
fidsubspace = unitaryrolloutfidelity( probembedded.trajectory, system; subspace = op.subspace )
Leakage Evaluation
For multilevel systems, check population in leakage levels: This requires analyzing state populations during evolution See the Multilevel Transmon example for details
Constraint Violations
Check if solution satisfies all constraints:
- Dynamics constraints: Compare rollout vs direct fidelity
- Bound constraints: Verify controls within system.drive_bounds
- Derivative constraints: Check |du|, |ddu| within bounds
println("Max control value: ", maximum(abs.(u)))
println("Max control derivative: ", maximum(abs.(du)))Max control value: 0.2778324893716056
Max control derivative: 0.43312082255200207Saving and Loading
Save a Trajectory
using JLD2 saveobject("mysolution.jld2", prob.trajectory)
Load and Reuse
Load trajectory for warm-starting: trajloaded = loadobject("my_solution.jld2")
Use as initial guess for new problem
probrefined = UnitarySmoothPulseProblem( system, Ugoal, T, Δt; uguess = trajloaded.u, piccolo_options = PiccoloOptions(verbose=false) )
Save Entire Problem
To save all problem information including constraints and objectives: saveobject("myproblem.jld2", prob)
Post-Processing
Resampling Trajectories
Change time discretization while preserving control shape: Tnew = 101 # More timesteps trajresampled = resample(prob.trajectory, T_new)
Extracting for Experiments
Prepare control pulses for hardware:
using PiccoloPlots # For visualization
using CairoMakiePlot controls
fig = plot(prob.trajectory)
save("controls.png", fig)
Extract control data for export
control_data = Dict(
"time" => cumsum([0; prob.trajectory.Δt[:]]),
"drive_1_real" => u[1, :],
"drive_2_real" => u[2, :],
"duration" => duration
)Dict{String, Any} with 4 entries:
"duration" => 10.5092
"time" => [0.0, 0.210184, 0.420368, 0.630553, 0.840737, 1.05092, 1.26…
"drive_1_real" => [0.0, 0.0910297, 0.168341, 0.225962, 0.262207, 0.277832, 0.…
"drive_2_real" => [0.0, -0.0422436, -0.0797889, -0.110932, -0.135205, -0.1528…Save to CSV or other format for AWG
using CSV, DataFrames df = DataFrame(controldata) CSV.write("pulsesequence.csv", df)
Pulse Filtering
Apply smoothing to reduce high-frequency noise: using DSP for i in 1:size(u, 1) u_filtered[i, :] = filtfilt(responsetype, u[i, :]) end
Best Practices
Starting a new optimization:
- Begin with coarse discretization (small T)
- Use relaxed bounds and convergence criteria
- Refine solution incrementally
- Use previous solution as warm start
Debugging poor convergence:
- Check
inf_pr- high values indicate constraint violations - Verify system Hamiltonian is correct
- Try looser derivative bounds (dubound, ddubound)
- Increase regularization weights (
R_u,R_du,R_ddu) - Use
piccolo_options.bound_state=truefor better numerics
Improving solutions:
- Increase T (more timesteps = finer control)
- Add derivative constraints for smoother pulses
- Use minimum time optimization for fastest gates
- Apply leakage constraints for multilevel systems
- Use sampling problems for robust control
This page was generated using Literate.jl.