Saving and Loading Pulses
Optimized pulses are valuable — they can take minutes or hours to compute. This guide shows how to save pulses to disk and reload them for warm-starting, analysis, or deployment to hardware.
Why Save Pulses?
- Warm-starting: Load a solved pulse as the initial guess for a harder problem (e.g., minimum-time optimization, higher fidelity targets, or robustness sweeps).
- Reproducibility: Archive optimized pulses alongside your scripts so results can be inspected or extended later.
- Hardware deployment: Export the final pulse for upload to an AWG or other control electronics.
- Sharing: Send a
.jld2file to a collaborator — they can rebuild the trajectory and continue optimizing without re-running the original solve.
Saving a Pulse
After solving, extract the optimized pulse with get_pulse and persist it with save. The save(filename, pulse) method writes the pulse under the JLD2 key "pulse", and load_pulse(filename) reads it back as the original pulse type.
using Piccolo
# Set up and solve a simple problem
H_drift = PAULIS[:Z]
H_drives = [PAULIS[:X], PAULIS[:Y]]
sys = QuantumSystem(H_drift, H_drives, [1.0, 1.0])
T, N = 10.0, 100
times = collect(range(0, T, length = N))
pulse = ZeroOrderPulse(0.1 * randn(2, N), times)
qtraj = UnitaryTrajectory(sys, pulse, GATES[:X])
qcp = SmoothPulseProblem(qtraj, N; Q = 100.0, R = 1e-2, ddu_bound = 1.0)
solve!(qcp; max_iter = 50)
# Extract the optimized pulse and save it
optimized_pulse = get_pulse(qcp.qtraj)
save("my_pulse.jld2", optimized_pulse) constructing SmoothPulseProblem for UnitaryTrajectory...
┌ Warning: Trajectory has timestep variable :Δt but no bounds on it.
│ Adding default lower bound of 0 to prevent negative timesteps.
│
│ Recommended: Add explicit bounds when creating the trajectory:
│ NamedTrajectory(...; Δt_bounds=(min, max))
│ Example:
│ NamedTrajectory(qtraj, N; Δt_bounds=(1e-3, 0.5))
│
│ Or use timesteps_all_equal=true in problem options to fix timesteps.
└ @ DirectTrajOpt.Problems ~/.julia/packages/DirectTrajOpt/bnQ8f/src/problems.jl:66
******************************************************************************
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
******************************************************************************
This is Ipopt version 3.14.19, running with linear solver MUMPS 5.8.2.
Number of nonzeros in equality constraint Jacobian...: 38156
Number of nonzeros in inequality constraint Jacobian.: 0
Number of nonzeros in Lagrangian Hessian.............: 38584
Total number of variables............................: 1587
variables with only lower bounds: 100
variables with lower and upper bounds: 1188
variables with only upper bounds: 0
Total number of equality constraints.................: 1287
Total number of inequality constraints...............: 0
inequality constraints with only lower bounds: 0
inequality constraints with lower and upper bounds: 0
inequality constraints with only upper bounds: 0
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
0 9.5926273e+01 8.32e+00 2.37e+00 0.0 0.00e+00 - 0.00e+00 0.00e+00 0
1 9.4988583e+01 6.31e+00 9.99e+01 -0.3 4.14e+00 2.0 9.72e-01 2.42e-01f 1
2 8.9396197e+01 4.38e+00 2.08e+02 0.2 3.15e+00 1.5 9.08e-01 3.06e-01f 1
3 6.2554324e+01 2.72e+00 1.41e+02 -0.2 2.24e+00 1.0 3.48e-01 3.78e-01f 1
4 6.2478376e+01 7.42e-01 4.92e+01 -0.1 1.37e+00 1.5 8.80e-01 7.27e-01f 1
⋮
46 1.8530316e-02 6.47e-06 1.83e-02 -4.0 4.42e-03 -0.5 1.00e+00 1.00e+00h 1
47 1.8198220e-02 5.00e-05 1.26e-01 -4.0 1.20e-02 -1.0 1.00e+00 1.00e+00h 1
48 1.7457683e-02 2.93e-04 1.01e-01 -4.0 3.27e-02 -1.5 1.00e+00 1.00e+00h 1
49 1.5549877e-02 1.74e-03 2.30e-01 -4.0 8.32e-02 -1.9 1.00e+00 1.00e+00h 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
50 1.4891538e-02 2.74e-04 2.60e-02 -4.0 2.91e-02 -1.5 1.00e+00 1.00e+00h 1
Number of Iterations....: 50
(scaled) (unscaled)
Objective...............: 1.4891537531292980e-02 1.4891537531292980e-02
Dual infeasibility......: 2.5988992622093744e-02 2.5988992622093744e-02
Constraint violation....: 2.7424580025146167e-04 2.7424580025146167e-04
Variable bound violation: 0.0000000000000000e+00 0.0000000000000000e+00
Complementarity.........: 1.0186575618335294e-04 1.0186575618335294e-04
Overall NLP error.......: 2.5988992622093744e-02 2.5988992622093744e-02
Number of objective function evaluations = 51
Number of objective gradient evaluations = 51
Number of equality constraint evaluations = 51
Number of inequality constraint evaluations = 0
Number of equality constraint Jacobian evaluations = 51
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations = 50
Total seconds in IPOPT = 20.785
EXIT: Maximum Number of Iterations Exceeded.
initializing optimizer...
building evaluator: 3 integrators, 0 nonlinear constraints
dynamics constraints: 1188, nonlinear constraints: 0
integrator 1 jacobian structure: 0.225s
integrator 2 jacobian structure: 0.014s
integrator 3 jacobian structure: 0.002s
jacobian structure: 38016 nonzeros (0.261s)
integrator 1 hessian structure: 0.069s
integrator 2 hessian structure: 0.039s
integrator 3 hessian structure: 0.006s
computing objective hessian structure (CompositeObjective)...
sub-objective 1 (KnotPointObjective): 0.232s
sub-objective 2 (QuadraticRegularizer): 0.199s
sub-objective 3 (QuadraticRegularizer): 0.001s
sub-objective 4 (QuadraticRegularizer): 0.017s
sub-objective 5 (NullObjective): 0.005s
objective hessian structure: 0.491s
hessian structure: 38944 nonzeros (0.606s)
linear index maps built (0.026s)
evaluator ready (total: 0.994s)
evaluator created (1.282s)
NL constraint bounds extracted (0.017s)
NLP block data built (0.0s)
Ipopt optimizer configured (0.006s)
variables set (0.084s)
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
linear constraints added: 10 (0.322s)
optimizer initialization complete (total: 1.71s)
[ Info: Loaded cached trajectory from saving_loading_base_b6b244c.jld2Bundling a pulse with metadata — fidelity, gate name, system parameters, anything you want to remember — uses jldsave from JLD2 directly:
using JLD2
jldsave(
"my_pulse_with_metadata.jld2";
pulse = optimized_pulse,
fidelity = fidelity(qcp),
duration = sum(get_timesteps(get_trajectory(qcp))),
gate = "X",
system_drift = H_drift,
)Loading a Pulse
load_pulse reads back a pulse written with save:
loaded_pulse = load_pulse("my_pulse.jld2")ZeroOrderPulse
drives: 2
duration: 10.534068838745865It is also the right loader for a metadata bundle saved with jldsave(...; pulse=...):
loaded_with_metadata = load_pulse("my_pulse_with_metadata.jld2")ZeroOrderPulse
drives: 2
duration: 10.534068838745865To inspect every key in a metadata bundle, use JLD2.load:
bundle = load("my_pulse_with_metadata.jld2")
keys(bundle)KeySet for a Dict{String, Any} with 5 entries. Keys:
"duration"
"system_drift"
"gate"
"fidelity"
"pulse"The loaded pulse is a regular pulse object — you can evaluate it, check its duration, and pass it to any trajectory constructor:
duration(loaded_pulse)10.534068838745865n_drives(loaded_pulse)2loaded_pulse(5.0)2-element Vector{Float64}:
0.08212236543160609
-0.08425623181764982Warm-Starting from a Saved Pulse
The most common use case: load a previously optimized pulse and use it as the starting point for a new optimization. This works because the pulse already carries good control values — the optimizer just needs to refine them.
# Load the saved pulse
loaded = load_pulse("my_pulse.jld2")
# Build a new trajectory using the loaded pulse
qtraj_warm = UnitaryTrajectory(sys, loaded, GATES[:X])
# Optimize — starting from a good initial guess converges faster
qcp_warm = SmoothPulseProblem(qtraj_warm, N; Q = 1000.0, R = 1e-3, ddu_bound = 1.0)
solve!(qcp_warm; max_iter = 20)
fidelity(qcp_warm)0.9999924087336114Warm-Starting for Minimum-Time Optimization
A particularly powerful pattern: solve a fixed-duration problem first, save the pulse, then load it for time-optimal compression.
# Step 1: Solve and save (in one script)
qcp = SmoothPulseProblem(qtraj, N; Q=100.0, Δt_bounds=(0.01, 0.5))
solve!(qcp; max_iter=200)
save("gate_pulse.jld2", get_pulse(qcp.qtraj))
# Step 2: Load and minimize time (in another script)
saved_pulse = load_pulse("gate_pulse.jld2")
qtraj = UnitaryTrajectory(sys, saved_pulse, U_goal)
qcp = SmoothPulseProblem(qtraj, N; Q=100.0, Δt_bounds=(0.01, 0.5))
qcp_mintime = MinimumTimeProblem(qcp; final_fidelity=0.999)
solve!(qcp_mintime; max_iter=100)Saving and Loading Trajectories
If you need to save the full optimization state (controls, quantum states at every timestep, and timesteps), save the NamedTrajectory instead. The trajectory-aware save writes under the JLD2 key "traj" and pairs with load_traj:
traj = get_trajectory(qcp)
save("my_trajectory.jld2", traj)Load it back:
loaded_traj = load_traj("my_trajectory.jld2")
typeof(loaded_traj)NamedTrajectory{Float64, (:Ũ⃗, :Δt, :t, :u, :du, :ddu), NTuple{6, Int64}, (:Ũ⃗, :u, :du, :ddu, :Δt), NTuple{5, Tuple{Vector{Float64}, Vector{Float64}}}, (:Ũ⃗, :u), Tuple{Vector{Float64}, Vector{Float64}}, (:u,), Tuple{Vector{Float64}}, (:Ũ⃗,), Tuple{Vector{Float64}}, (:Ũ⃗, :Δt, :t, :u, :du, :ddu), NTuple{6, UnitRange{Int64}}, NTuple{6, Symbol}, Tuple{Symbol, Symbol}, NTuple{4, Symbol}, (), Tuple{}, (), Tuple{}, Tuple{}}The trajectory contains all NLP decision variables. You can inspect controls, states, and timesteps:
loaded_traj[:u][:, 1:3] # First 3 timesteps of controls2×3 Matrix{Float64}:
0.0 -0.0628788 -0.0664955
0.0 -0.0350897 -0.0495202Organizing Saved Pulses
For projects with many gates, use a consistent directory structure:
my_project/
├── scripts/
│ ├── optimize_X.jl
│ ├── optimize_CZ.jl
│ └── mintime_CZ.jl # loads from data/CZ.jld2
└── data/
├── X.jld2
├── CZ.jld2
└── CZ_mintime.jld2Each optimization script saves its result to data/, and downstream scripts (like minimum-time solves) load from there. This creates a clear dependency chain and makes results easy to find.
Tips
Always Save After a Successful Solve
Optimization can be expensive. Get in the habit of saving immediately after solve! returns a good result:
solve!(qcp; max_iter=300)
if fidelity(qcp) > 0.999
save("data/my_gate.jld2", get_pulse(qcp.qtraj))
endInclude Metadata
Future-you will thank present-you for saving context:
using Dates
jldsave("data/CZ.jld2";
pulse = get_pulse(qcp.qtraj),
fidelity = fidelity(qcp),
duration = sum(get_timesteps(get_trajectory(qcp))),
gate_name = "CZ",
system_config = "2-atom Rydberg, 8.7 μm spacing",
date = string(Dates.now()),
)Converting Pulse Types Before Saving
If you optimized with ZeroOrderPulse but need a smooth pulse for hardware, convert before saving:
zop = get_pulse(qcp.qtraj) # ZeroOrderPulse from SmoothPulseProblem
# Convert to cubic spline
ctrl = hcat([zop(t) for t in times]...)
tgts = zeros(size(ctrl))
for k in 1:(N-1)
tgts[:, k] = (ctrl[:, k+1] - ctrl[:, k]) / (times[k+1] - times[k])
end
tgts[:, N] = tgts[:, N-1]
smooth_pulse = CubicSplinePulse(ctrl, tgts, times)
save("data/CZ_smooth.jld2", smooth_pulse)# Clean up temporary files
rm("my_pulse.jld2"; force = true)
rm("my_pulse_with_metadata.jld2"; force = true)
rm("my_trajectory.jld2"; force = true)See Also
- Pulses — Pulse types and conversions
- Trajectories — Full trajectory structure
- SplinePulseProblem — Warm-starting with spline pulses
- MinimumTimeProblem — Time-optimal control from saved solutions
This page was generated using Literate.jl.