Visualization
Piccolo.jl provides visualization tools for analyzing optimization results. This guide covers plotting controls, states, and populations.
Setup
Visualization requires a Makie backend. We'll create a solved problem to work with:
using Piccolo
using CairoMakie
using Random
Random.seed!(42)
# Create and solve a simple qubit gate problem
H_drift = 0.5 * PAULIS[:Z]
H_drives = [PAULIS[:X], PAULIS[:Y]]
sys = QuantumSystem(H_drift, H_drives, [1.0, 1.0])
T = 10.0
N = 50
times = collect(range(0, T, length = N))
initial_controls = 0.1 * randn(2, N)
pulse = ZeroOrderPulse(initial_controls, times)
qtraj = UnitaryTrajectory(sys, pulse, GATES[:X])
# Lock the time grid to a uniform spacing so the ZOH stair plot has
# consistent step widths. Without this, the optimizer can vary Δt_k.
opts = PiccoloOptions(timesteps_all_equal = true, verbose = false)
qcp = SmoothPulseProblem(
qtraj,
N;
Q = 100.0,
R = 1e-2,
ddu_bound = 1.0,
piccolo_options = opts,
)
solve!(qcp; max_iter = 50, print_level = 1)┌ 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
******************************************************************************
initializing optimizer...
building evaluator: 3 integrators, 0 nonlinear constraints
dynamics constraints: 588, nonlinear constraints: 0
integrator 1 jacobian structure: 0.106s
integrator 2 jacobian structure: 0.008s
integrator 3 jacobian structure: 0.0s
jacobian structure: 18816 nonzeros (0.126s)
integrator 1 hessian structure: 0.01s
integrator 2 hessian structure: 0.01s
integrator 3 hessian structure: 0.002s
computing objective hessian structure (CompositeObjective)...
sub-objective 1 (KnotPointObjective): 0.16s
sub-objective 2 (QuadraticRegularizer): 0.144s
sub-objective 3 (QuadraticRegularizer): 0.0s
sub-objective 4 (QuadraticRegularizer): 0.0s
sub-objective 5 (NullObjective): 0.003s
objective hessian structure: 0.33s
hessian structure: 19344 nonzeros (0.353s)
linear index maps built (0.005s)
evaluator ready (total: 0.542s)
evaluator created (0.742s)
NL constraint bounds extracted (0.01s)
NLP block data built (0.0s)
Ipopt optimizer configured (0.005s)
variables set (0.092s)
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
linear constraints added: 11 (0.293s)
optimizer initialization complete (total: 1.154s)
[ Info: Loaded cached trajectory from visualization_unitary_f5c089b.jld2Inspect the resulting fidelity:
fidelity(qcp)0.9999965881970787Pulse Plotting
plot_pulse renders any AbstractPulse with type-appropriate visuals (step functions for ZeroOrderPulse, line segments for LinearSplinePulse, smooth curves with knots for CubicSplinePulse, dense samples for analytic / FunctionPulse types). For per-pulse-type construction and rendering examples — including hardware bounds, tangent whiskers, theming, and the :stacked vs :overlay layouts — see the Pulses concept page. This guide focuses on plotting workflow pieces specific to optimization output.
Plot directly from a QuantumControlProblem
After solving, plot_pulse(qcp) extracts the optimized pulse and renders it in one call. Per-drive labels default to "<drive_name>_<i>". Pass bounds=true to shade hardware bounds derived from the system, and components=[:du, :ddu] to stack the derivative trajectories beneath the pulse with their own bounded panels (set component_bounds=true to read bounds from the NamedTrajectory).
fig = plot_pulse(qcp; title = "Optimized Pulse")
Add hardware bounds and the smoothness derivatives:
fig = plot_pulse(
qcp;
title = "Optimized Pulse + Smoothness",
bounds = true,
components = [:du, :ddu],
component_bounds = true,
)
Manual extract + plot
If you need to manipulate the pulse before plotting (e.g. resample, convert to a different pulse type), reconstruct it explicitly. The higher-level plot_pulse(qcp; ...) calls do this internally.
optimized_traj = get_trajectory(qcp)
optimized_pulse = ZeroOrderPulse(optimized_traj)
fig = plot_pulse(optimized_pulse; title = "Optimized Pulse")
Basic Trajectory Plotting
The plot function from NamedTrajectories.jl plots trajectory components.
Plot Controls
traj = get_trajectory(qcp)
fig = plot(traj, [:u])
Plot Controls and Derivatives
fig = plot(traj, [:u, :du, :ddu])
Quantum-Specific Plots
Unitary Populations
For UnitaryTrajectory, visualize how state populations evolve during the gate:
fig = plot_unitary_populations(traj)
Ket State Populations
For KetTrajectory, use plot_state_populations:
ψ_init = ComplexF64[1.0, 0.0]
ψ_goal = ComplexF64[0.0, 1.0]
pulse_ket = ZeroOrderPulse(0.1 * randn(2, N), times)
qtraj_ket = KetTrajectory(sys, pulse_ket, ψ_init, ψ_goal)
qcp_ket = SmoothPulseProblem(qtraj_ket, N; Q = 100.0, R = 1e-2, ddu_bound = 1.0)
solve!(qcp_ket; max_iter = 50, verbose = false, print_level = 1)
traj_ket = get_trajectory(qcp_ket)
fig = plot_state_populations(traj_ket)
IQ pairs (plot_pulse_IQ)
For 4-drive pulses interpreted as two IQ pairs — drive (ΩI, ΩQ) and displacement (αI, αQ) — plot_pulse_IQ renders each pair on its own row along with the magnitude envelope. Typical view for cat-qubit / oscillator control where both the drive and displacement are complex-valued.
T_iq = 50.0
Ω_max = 0.5
α_max = 0.3
σ = T_iq / 6
pulse_iq = GaussianPulse([Ω_max, 0.7 * Ω_max, α_max, 0.7 * α_max], σ, T_iq)
fig = plot_pulse_IQ(pulse_iq; title = "IQ view (Ω, α)")
Magnitude + phase (plot_pulse_phases)
Same 4-drive structure, polar form: magnitude and unwrapped phase for the drive on the top row, displacement on the bottom row. Phase is masked to NaN wherever the corresponding amplitude is below 1% of its peak (configurable via phase_threshold) since atan(Q, I) is numerically meaningless in the near-zero region.
fig = plot_pulse_phases(pulse_iq; title = "Polar view (|·|, ∠·)")
Custom Plotting
For full control, extract trajectory data and use Makie directly.
Manual Control Plots
plot_times = cumsum([0; get_timesteps(traj)])[1:(end-1)]
fig = Figure(size = (800, 400))
ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "Control Amplitude")
for i = 1:size(traj[:u], 1)
lines!(ax, plot_times, traj[:u][i, :], label = "Drive $i", linewidth = 2)
end
axislegend(ax, position = :rt)
fig
Subplot Layouts
fig = Figure(size = (1000, 500))
# Controls
ax1 = Axis(fig[1, 1], xlabel = "Time", ylabel = "Amplitude", title = "Controls")
lines!(ax1, plot_times, traj[:u][1, :], label = "u_x", linewidth = 2)
lines!(ax1, plot_times, traj[:u][2, :], label = "u_y", linewidth = 2)
axislegend(ax1, position = :rt)
# Derivatives
ax2 = Axis(fig[1, 2], xlabel = "Time", ylabel = "Derivative", title = "Control Derivatives")
lines!(ax2, plot_times, traj[:du][1, :], label = "du_x", linewidth = 2)
lines!(ax2, plot_times, traj[:du][2, :], label = "du_y", linewidth = 2)
axislegend(ax2, position = :rt)
fig
Phase Space Plot
fig = Figure(size = (500, 500))
ax = Axis(fig[1, 1], xlabel = "u_x", ylabel = "u_y", title = "Control Phase Space")
lines!(ax, traj[:u][1, :], traj[:u][2, :], linewidth = 2)
scatter!(
ax,
[traj[:u][1, 1]],
[traj[:u][2, 1]],
color = :green,
markersize = 15,
label = "Start",
)
scatter!(
ax,
[traj[:u][1, end]],
[traj[:u][2, end]],
color = :red,
markersize = 15,
label = "End",
)
axislegend(ax, position = :rt)
fig
Fidelity Evolution
Track fidelity during the pulse:
using LinearAlgebra
U_goal = GATES[:X]
fidelities = Float64[]
for k = 1:size(traj[:Ũ⃗], 2)
U_k = iso_vec_to_operator(traj[:Ũ⃗][:, k])
F_k = abs(tr(U_goal' * U_k))^2 / sys.levels^2
push!(fidelities, F_k)
end
fig = Figure(size = (800, 300))
ax = Axis(fig[1, 1], xlabel = "Timestep", ylabel = "Fidelity")
lines!(ax, 1:length(fidelities), fidelities, linewidth = 2)
hlines!(ax, [0.99], color = :red, linestyle = :dash, label = "99% target")
axislegend(ax, position = :rb)
fig
Comparing Solutions
Compare solutions with different regularization weights:
fig = Figure(size = (800, 400))
ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "u_x", title = "Effect of Regularization")
for (R, label) in [(1e-3, "R=1e-3"), (1e-2, "R=1e-2"), (1e-1, "R=1e-1")]
pulse_r = ZeroOrderPulse(0.1 * randn(2, N), times)
qtraj_r = UnitaryTrajectory(sys, pulse_r, GATES[:X])
qcp_r = SmoothPulseProblem(qtraj_r, N; Q = 100.0, R = R, ddu_bound = 1.0)
solve!(
qcp_r;
max_iter = 50,
verbose = false,
print_level = 1,
)
traj_r = get_trajectory(qcp_r)
t_r = cumsum([0; get_timesteps(traj_r)])[1:(end-1)]
lines!(ax, t_r, traj_r[:u][1, :], label = label, linewidth = 2)
end
axislegend(ax, position = :rt)
fig
Saving Figures
# PNG (raster)
# save("controls.png", fig)
# PDF (vector graphics)
# save("controls.pdf", fig)
# SVG (vector graphics)
# save("controls.svg", fig)Plotting Tips
1. Use Appropriate Resolution
For publications, use high-res settings:
fig_hires = Figure(size = (1200, 800), fontsize = 14)
2. Use Consistent Colors
colors = Makie.wong_colors()3. Show Drive Bounds
bound = 1.0
fig = Figure(size = (800, 300))
ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "Amplitude")
band!(
ax,
plot_times,
-bound * ones(length(plot_times)),
bound * ones(length(plot_times)),
color = (:gray, 0.2),
label = "Bounds",
)
lines!(ax, plot_times, traj[:u][1, :], label = "u_x", linewidth = 2)
axislegend(ax, position = :rt)
fig
See Also
- Visualizations API - Complete API reference
- Problem Templates - Generating solutions to plot
- Trajectories - Understanding trajectory structure
This page was generated using Literate.jl.