Your First Gate
This tutorial walks through synthesizing your first quantum gate with Piccolo.jl. We'll implement an X gate (NOT gate) on a single qubit.
What We're Doing
We want to find control pulses that implement:
\[X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\]
Our qubit has Hamiltonian:
\[H(t) = \frac{\omega}{2}\sigma_z + u_x(t)\sigma_x + u_y(t)\sigma_y\]
The optimizer will find $u_x(t)$ and $u_y(t)$ that produce the X gate.
Setup
First, load the required packages:
using Piccolo
using CairoMakie
using Random
Random.seed!(42) # For reproducibilityRandom.TaskLocalRNG()Step 1: Define the Quantum System
A QuantumSystem needs:
- Drift Hamiltonian: Always-on terms (qubit frequency)
- Drive Hamiltonians: Controllable interactions
- Drive bounds: Maximum control amplitudes
# The drift Hamiltonian: ω/2 σ_z (qubit frequency)
# We set ω = 1.0 for simplicity
H_drift = 0.5 * PAULIS[:Z]
# The drive Hamiltonians: σ_x and σ_y controls
H_drives = [PAULIS[:X], PAULIS[:Y]]
# Maximum amplitude for each drive (in same units as H_drift)
drive_bounds = [1.0, 1.0]
# Create the system
sys = QuantumSystem(H_drift, H_drives, drive_bounds)QuantumSystem: levels = 2, n_drives = 2Let's check what we created:
sys.levels, sys.n_drives(2, 2)Step 2: Create an Initial Pulse
We need an initial guess for the control pulse. ZeroOrderPulse represents piecewise constant controls - the standard choice for most problems.
# Gate duration and discretization
T = 10.0 # Total time (in units where ω = 1)
N = 100 # Number of timesteps
# Time vector
times = collect(range(0, T, length = N))
# Random initial controls (small amplitude)
# Shape: (n_drives, N) = (2, 100)
initial_controls = 0.1 * randn(2, N)
# Create the pulse
pulse = ZeroOrderPulse(initial_controls, times)ZeroOrderPulse
drives: 2
duration: 10.0Check the pulse:
duration(pulse)10.0n_drives(pulse)2pulse(5.0)2-element Vector{Float64}:
0.036820693581548374
-0.004656094092083756Step 3: Define the Goal
A UnitaryTrajectory combines the system, pulse, and target gate.
# Our target: the X gate
U_goal = GATES[:X]
U_goal
# Create the trajectory
qtraj = UnitaryTrajectory(sys, pulse, U_goal)UnitaryTrajectory{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.AutoSpecialize, SciMLOperators.MatrixOperator{ComplexF64, Matrix{ComplexF64}, SciMLOperators.FilterKwargs{Nothing, Val{()}}, SciMLOperators.FilterKwargs{Piccolo.Quantum.Rollouts.var"#update!#_construct_operator##2"{QuantumSystem{Piccolo.Quantum.QuantumSystems.var"#53#54"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, Piccolo.Quantum.QuantumSystems.var"#55#56"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}, Vector{DriftTerm{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Returns{Float64}, Returns{Float64}}}, SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}}, Val{()}}}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, Piccolo.Quantum.Rollouts.PiccoloRolloutSystem{Union{Int64, AbstractVector{Int64}, CartesianIndex, CartesianIndices}}, Nothing, Nothing}, Base.Pairs{Symbol, Vector{Float64}, Nothing, @NamedTuple{tstops::Vector{Float64}}}, SciMLBase.StandardODEProblem}, OrdinaryDiffEqLinear.MagnusAdapt4, OrdinaryDiffEqCore.InterpolationData{SciMLBase.ODEFunction{true, SciMLBase.AutoSpecialize, SciMLOperators.MatrixOperator{ComplexF64, Matrix{ComplexF64}, SciMLOperators.FilterKwargs{Nothing, Val{()}}, SciMLOperators.FilterKwargs{Piccolo.Quantum.Rollouts.var"#update!#_construct_operator##2"{QuantumSystem{Piccolo.Quantum.QuantumSystems.var"#53#54"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, Piccolo.Quantum.QuantumSystems.var"#55#56"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}, Vector{DriftTerm{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Returns{Float64}, Returns{Float64}}}, SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}}, Val{()}}}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, Piccolo.Quantum.Rollouts.PiccoloRolloutSystem{Union{Int64, AbstractVector{Int64}, CartesianIndex, CartesianIndices}}, Nothing, Nothing}, Vector{Matrix{ComplexF64}}, Vector{Float64}, Vector{Vector{Matrix{ComplexF64}}}, Nothing, OrdinaryDiffEqLinear.MagnusAdapt4Cache{Matrix{ComplexF64}, Matrix{ComplexF64}, Matrix{ComplexF64}, Matrix{ComplexF64}, Nothing}, Nothing}, SciMLBase.DEStats, Nothing, Nothing, Nothing, Nothing}}(QuantumSystem: levels = 2, n_drives = 2, ZeroOrderPulse(Number of drives = 2, T = 10.0), ComplexF64[1.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0 + 0.0im], ComplexF64[0.0 + 0.0im 1.0 + 0.0im; 1.0 + 0.0im 0.0 + 0.0im], ComplexF64[1.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0 + 0.0im;;; 0.99874049446695 - 0.049979006477971294im -0.0025163151855861713 + 0.0036320491838595435im; 0.002516315185586171 + 0.0036320491838595435im 0.99874049446695 + 0.049979006477971294im;;; 0.9949810320689114 - 0.09981289144825772im 0.0005709258525007132 + 0.007057376813909546im; -0.0005709258525007129 + 0.007057376813909546im 0.9949810320689114 + 0.09981289144825772im;;; … ;;; 0.19017980779190863 + 0.9728869829525824im 0.11418657941947384 + 0.06545215191375613im; -0.11418657941947344 + 0.0654521519137558im 0.19017980779190974 - 0.9728869829525807im;;; 0.23711689646909526 + 0.9617476966304098im 0.1264142539474302 + 0.05325769272027365im; -0.12641425394742972 + 0.0532576927202733im 0.23711689646909623 - 0.9617476966304076im;;; 0.28472921676617113 + 0.9512275311815304im 0.11360437213046226 + 0.034489181997622145im; -0.11360437213046172 + 0.03448918199762179im 0.2847292167661721 - 0.951227531181528im])Step 4: Set Up the Optimization Problem
SmoothPulseProblem creates an optimization problem with:
- Fidelity objective (weight
Q) - Control regularization (weight
R) - Smoothness via derivative bounds
qcp = SmoothPulseProblem(
qtraj,
N;
Q = 100.0, # Fidelity weight (higher = prioritize fidelity)
R = 1e-2, # Regularization weight (higher = smoother controls)
ddu_bound = 1.0, # Limit on control acceleration
)QuantumControlProblem{UnitaryTrajectory{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.AutoSpecialize, SciMLOperators.MatrixOperator{ComplexF64, Matrix{ComplexF64}, SciMLOperators.FilterKwargs{Nothing, Val{()}}, SciMLOperators.FilterKwargs{Piccolo.Quantum.Rollouts.var"#update!#_construct_operator##2"{QuantumSystem{Piccolo.Quantum.QuantumSystems.var"#53#54"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, Piccolo.Quantum.QuantumSystems.var"#55#56"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}, Vector{DriftTerm{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Returns{Float64}, Returns{Float64}}}, SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}}, Val{()}}}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, Piccolo.Quantum.Rollouts.PiccoloRolloutSystem{Union{Int64, AbstractVector{Int64}, CartesianIndex, CartesianIndices}}, Nothing, Nothing}, Base.Pairs{Symbol, Vector{Float64}, Nothing, @NamedTuple{tstops::Vector{Float64}}}, SciMLBase.StandardODEProblem}, OrdinaryDiffEqLinear.MagnusAdapt4, OrdinaryDiffEqCore.InterpolationData{SciMLBase.ODEFunction{true, SciMLBase.AutoSpecialize, SciMLOperators.MatrixOperator{ComplexF64, Matrix{ComplexF64}, SciMLOperators.FilterKwargs{Nothing, Val{()}}, SciMLOperators.FilterKwargs{Piccolo.Quantum.Rollouts.var"#update!#_construct_operator##2"{QuantumSystem{Piccolo.Quantum.QuantumSystems.var"#53#54"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, Piccolo.Quantum.QuantumSystems.var"#55#56"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}, Vector{DriftTerm{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Returns{Float64}, Returns{Float64}}}, SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}}, Val{()}}}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, Piccolo.Quantum.Rollouts.PiccoloRolloutSystem{Union{Int64, AbstractVector{Int64}, CartesianIndex, CartesianIndices}}, Nothing, Nothing}, Vector{Matrix{ComplexF64}}, Vector{Float64}, Vector{Vector{Matrix{ComplexF64}}}, Nothing, OrdinaryDiffEqLinear.MagnusAdapt4Cache{Matrix{ComplexF64}, Matrix{ComplexF64}, Matrix{ComplexF64}, Matrix{ComplexF64}, Nothing}, Nothing}, SciMLBase.DEStats, Nothing, Nothing, Nothing, Nothing}}}
System: QuantumSystem{Piccolo.Quantum.QuantumSystems.var"#53#54"{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Vector{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}, Int64}, Piccolo.Quantum.QuantumSystems.var"#55#56"{Vector{SparseArrays.SparseMatrixCSC{Float64, Int64}}, Int64, SparseArrays.SparseMatrixCSC{Float64, Int64}}, @NamedTuple{}, Vector{DriftTerm{SparseArrays.SparseMatrixCSC{ComplexF64, Int64}, Returns{Float64}, Returns{Float64}}}, SparseArrays.SparseMatrixCSC{ComplexF64, Int64}}
Goal: Matrix{ComplexF64}
Trajectory: 100 knots
State: Ũ⃗
Controls: uStep 5: Solve!
The solve! function runs the optimizer:
solve!(qcp; max_iter = 20, verbose = false, print_level = 1)Step 6: Analyze the Results
First, check the fidelity:
fidelity(qcp)0.9992100214821484Get the optimized trajectory:
traj = get_trajectory(qcp)
# Check the final unitary
U_final = iso_vec_to_operator(traj[:Ũ⃗][:, end])
round.(U_final, digits = 3)2×2 Matrix{ComplexF64}:
0.017+0.015im -0.017+1.0im
0.017+1.0im 0.017-0.015imStep 7: Visualize
Plot the optimized control pulses:
fig = Figure(size = (800, 400))
# Time axis
plot_times = cumsum([0; get_timesteps(traj)])[1:(end-1)]
# Control pulses
ax1 = Axis(
fig[1, 1],
xlabel = "Time",
ylabel = "Control Amplitude",
title = "Optimized Controls",
)
lines!(ax1, plot_times, traj[:u][1, :], label = "u_x (σ_x drive)", linewidth = 2)
lines!(ax1, plot_times, traj[:u][2, :], label = "u_y (σ_y drive)", linewidth = 2)
axislegend(ax1, position = :rt)
fig
Understanding the Solution
The optimizer found control pulses that:
- Start and end smoothly (due to derivative regularization)
- Stay within bounds (due to drive_bounds)
- Achieve high fidelity (due to the Q-weighted objective)
The X gate rotates the qubit state around the X-axis by π radians. You can see the controls create the right rotation!
Step 8: Save the Optimized Pulse
Optimized pulses are valuable — save them so you can reload them later for warm-starting, analysis, or hardware deployment.
optimized_pulse = get_pulse(qcp.qtraj)
save("x_gate_pulse.jld2", optimized_pulse)Reload later and continue optimizing:
saved_pulse = load_pulse("x_gate_pulse.jld2")
qtraj_warm = UnitaryTrajectory(sys, saved_pulse, GATES[:X])
qcp_warm = SmoothPulseProblem(qtraj_warm, N; Q=1000.0)
solve!(qcp_warm; max_iter=50) # converges faster from a good starting pointSee the Saving and Loading Pulses guide for more details.
What's Next?
Now that you've synthesized your first gate, try:
- Different gates: Change
U_goaltoGATES[:H](Hadamard) orGATES[:T] - Faster gates: Reduce
Tand see how fidelity changes - Smoother pulses: Increase
Ror decreaseddu_bound - Time-optimal: Add
Δt_boundsand useMinimumTimeProblem - Save and reload: Use Saving and Loading Pulses to build on your results
Continue to the State Transfer tutorial to learn about preparing specific quantum states.
This page was generated using Literate.jl.