|
| 1 | +# QuantumCollocation.jl Context |
| 2 | + |
| 3 | +> AI-friendly context for maintaining consistency. Update this when making significant changes. |
| 4 | +
|
| 5 | +## Package Purpose |
| 6 | + |
| 7 | +QuantumCollocation.jl provides **problem templates** for quantum optimal control using direct collocation. It builds on: |
| 8 | +- **DirectTrajOpt.jl** - Core optimization infrastructure (objectives, constraints, integrators, NLP solver) |
| 9 | +- **PiccoloQuantumObjects.jl** - Quantum systems, trajectories, pulses, and utilities |
| 10 | +- **NamedTrajectories.jl** - Named variable trajectory data structure |
| 11 | + |
| 12 | +The main abstractions are: |
| 13 | +1. **QuantumControlProblem** - Wrapper combining quantum trajectory info with optimization problem |
| 14 | +2. **Problem Templates** - Constructors that build complete optimization problems |
| 15 | + |
| 16 | +## Canonical Workflow |
| 17 | + |
| 18 | +### Basic Gate Synthesis |
| 19 | + |
| 20 | +```julia |
| 21 | +using QuantumCollocation |
| 22 | +using PiccoloQuantumObjects |
| 23 | + |
| 24 | +# 1. Define quantum system (no T_max - duration comes from trajectory) |
| 25 | +H_drift = GATES[:Z] |
| 26 | +H_drives = [GATES[:X], GATES[:Y]] |
| 27 | +drive_bounds = [1.0, 1.0] # symmetric bounds |
| 28 | +sys = QuantumSystem(H_drift, H_drives, drive_bounds) |
| 29 | + |
| 30 | +# 2. Create quantum trajectory (defines the control problem) |
| 31 | +U_goal = GATES[:H] # target: Hadamard gate |
| 32 | +T = 10.0 # duration |
| 33 | +qtraj = UnitaryTrajectory(sys, U_goal, T) # creates zero pulse internally |
| 34 | + |
| 35 | +# 3. Build optimization problem |
| 36 | +N = 51 # number of timesteps |
| 37 | +qcp = SmoothPulseProblem(qtraj, N; Q=100.0, R=1e-2) |
| 38 | + |
| 39 | +# 4. Solve (use 'options' keyword for IpoptOptions) |
| 40 | +solve!(qcp; options=IpoptOptions(max_iter=200)) |
| 41 | +# OR pass options during problem construction: |
| 42 | +# qcp = SmoothPulseProblem(qtraj, N; Q=100.0, R=1e-2, ipopt_options=opts) |
| 43 | +# solve!(qcp) # uses options from construction |
| 44 | + |
| 45 | +# 5. Extract results |
| 46 | +traj = get_trajectory(qcp) |
| 47 | +U_final = iso_vec_to_operator(traj[end][:Ũ⃗]) |
| 48 | +fid = unitary_fidelity(U_final, U_goal) |
| 49 | +``` |
| 50 | + |
| 51 | +### State Transfer (Ket) |
| 52 | + |
| 53 | +```julia |
| 54 | +sys = QuantumSystem(H_drift, H_drives, drive_bounds) |
| 55 | + |
| 56 | +ψ_init = ComplexF64[1.0, 0.0] # |0⟩ |
| 57 | +ψ_goal = ComplexF64[0.0, 1.0] # |1⟩ |
| 58 | +T = 10.0 |
| 59 | +qtraj = KetTrajectory(sys, ψ_init, ψ_goal, T) |
| 60 | + |
| 61 | +qcp = SmoothPulseProblem(qtraj, N; Q=50.0, R=1e-3) |
| 62 | +solve!(qcp; max_iter=100) |
| 63 | +``` |
| 64 | + |
| 65 | +### Ensemble Control (Multiple Initial States) |
| 66 | + |
| 67 | +```julia |
| 68 | +sys = QuantumSystem(H_drift, H_drives, drive_bounds) |
| 69 | + |
| 70 | +# X gate: |0⟩→|1⟩ and |1⟩→|0⟩ |
| 71 | +initials = [ComplexF64[1,0], ComplexF64[0,1]] |
| 72 | +goals = [ComplexF64[0,1], ComplexF64[1,0]] |
| 73 | +T = 10.0 |
| 74 | +qtraj = MultiKetTrajectory(sys, initials, goals, T) |
| 75 | + |
| 76 | +qcp = SmoothPulseProblem(qtraj, N; Q=100.0, R=1e-2) |
| 77 | +solve!(qcp; max_iter=200) |
| 78 | +``` |
| 79 | + |
| 80 | +### With Explicit Pulse (Spline-Based) |
| 81 | + |
| 82 | +```julia |
| 83 | +sys = QuantumSystem(H_drift, H_drives, drive_bounds) |
| 84 | + |
| 85 | +# Create explicit pulse with initial guess |
| 86 | +T = 10.0 |
| 87 | +times = collect(range(0.0, T, length=N)) |
| 88 | +controls = 0.1 * randn(sys.n_drives, N) |
| 89 | +pulse = LinearSplinePulse(controls, times) |
| 90 | + |
| 91 | +qtraj = UnitaryTrajectory(sys, pulse, U_goal) |
| 92 | + |
| 93 | +# Use SplinePulseProblem for spline pulses |
| 94 | +qcp = SplinePulseProblem(qtraj, N; Q=100.0, R=1e-2, du_bound=10.0) |
| 95 | +solve!(qcp; max_iter=200) |
| 96 | +``` |
| 97 | + |
| 98 | +### Minimum Time Optimization |
| 99 | + |
| 100 | +```julia |
| 101 | +# First solve with fixed time |
| 102 | +qcp = SmoothPulseProblem(qtraj, N; Q=100.0, Δt_bounds=(0.01, 0.5)) |
| 103 | +solve!(qcp; max_iter=100) |
| 104 | + |
| 105 | +# Convert to minimum-time problem |
| 106 | +qcp_mintime = MinimumTimeProblem(qcp; final_fidelity=0.99, D=50.0) |
| 107 | +solve!(qcp_mintime; max_iter=100) |
| 108 | +``` |
| 109 | + |
| 110 | +### Bootstrapping Across System Sizes |
| 111 | + |
| 112 | +**Best Practice:** When optimizing similar systems of different sizes, **pass the pulse object** between solves to preserve both controls and derivatives: |
| 113 | + |
| 114 | +```julia |
| 115 | +# Optimize for N=3 atoms |
| 116 | +sys3 = build_system(N=3) |
| 117 | +qtraj3 = UnitaryTrajectory(sys3, pulse_init, U_goal) |
| 118 | +qcp3 = SplinePulseProblem(qtraj3, N_timesteps; Q=100.0, R_du=1e-4) |
| 119 | +solve!(qcp3; max_iter=200) |
| 120 | + |
| 121 | +# Bootstrap to N=4 using optimized pulse directly |
| 122 | +optimized_pulse = qcp3.qtraj.pulse # Contains both u and du |
| 123 | +sys4 = build_system(N=4) |
| 124 | +qtraj4 = UnitaryTrajectory(sys4, optimized_pulse, U_goal) # Reuse pulse |
| 125 | +qcp4 = SplinePulseProblem(qtraj4, N_timesteps; Q=100.0, R_du=1e-4) |
| 126 | +solve!(qcp4; max_iter=200) |
| 127 | + |
| 128 | +# Save pulse for next bootstrap |
| 129 | +using JLD2 |
| 130 | +save("optimized_N4.jld2", "pulse", qcp4.qtraj.pulse) |
| 131 | +``` |
| 132 | + |
| 133 | +**Why this works:** |
| 134 | +- `Pulse` objects are system-independent (just time → control mappings) |
| 135 | +- Preserves full optimization state: `u` (controls) + `du` (derivatives/tangents) |
| 136 | +- The new system's dynamics are enforced during `solve!` via rollout constraints |
| 137 | + |
| 138 | +**Don't:** |
| 139 | +```julia |
| 140 | +# ❌ Bad: Extracting controls from NamedTrajectory loses du information |
| 141 | +u_old = get_trajectory(qcp3)[:u] |
| 142 | +du_zeros = zeros(size(u_old)) # Throws away optimized tangents! |
| 143 | +pulse_new = CubicSplinePulse(u_old, du_zeros, times) |
| 144 | + |
| 145 | +# ❌ Bad: Trying to transfer NamedTrajectory (has system-specific states) |
| 146 | +qtraj4 = bootstrap_from_trajectory(traj3, sys4) # Incompatible states |
| 147 | +``` |
| 148 | + |
| 149 | +**Do:** |
| 150 | +```julia |
| 151 | +# ✓ Good: Pass pulse object directly |
| 152 | +pulse_optimized = qcp3.qtraj.pulse # Preserves u and du |
| 153 | +qtraj4 = UnitaryTrajectory(sys4, pulse_optimized, U_goal) |
| 154 | +``` |
| 155 | + |
| 156 | +### Robust Control (Parameter Sampling) |
| 157 | + |
| 158 | +```julia |
| 159 | +# Create base problem |
| 160 | +qtraj = UnitaryTrajectory(sys_nominal, pulse, U_goal) |
| 161 | +qcp = SmoothPulseProblem(qtraj, N; Q=100.0) |
| 162 | + |
| 163 | +# Add robustness over parameter variations |
| 164 | +sampling_prob = SamplingProblem(qcp, [sys_nominal, sys_perturbed]; Q=100.0) |
| 165 | +solve!(sampling_prob; max_iter=200) |
| 166 | +``` |
| 167 | + |
| 168 | +## Key Abstractions |
| 169 | + |
| 170 | +### Quantum Trajectories (defined in PiccoloQuantumObjects.jl) |
| 171 | + |
| 172 | +Parametric type hierarchy (see PiccoloQuantumObjects.jl/CONTEXT.md for details): |
| 173 | +``` |
| 174 | +AbstractQuantumTrajectory{P<:AbstractPulse} |
| 175 | +├── UnitaryTrajectory{P} # Full unitary evolution |
| 176 | +├── KetTrajectory{P} # Single state evolution |
| 177 | +├── MultiKetTrajectory{P} # Multiple states, shared controls |
| 178 | +├── DensityTrajectory{P} # Density matrix evolution (WIP) |
| 179 | +└── SamplingTrajectory{P,Q} # Robustness over system parameters |
| 180 | +``` |
| 181 | + |
| 182 | +QuantumCollocation.jl **uses** these types and provides problem templates that build on them. |
| 183 | + |
| 184 | +### Problem Templates |
| 185 | + |
| 186 | +| Template | Use Case | Pulse Type | |
| 187 | +|----------|----------|------------| |
| 188 | +| `SmoothPulseProblem` | Standard optimization with smooth pulses | `ZeroOrderPulse` (piecewise constant) | |
| 189 | +| `SplinePulseProblem` | Spline-based pulse optimization | `LinearSplinePulse`, `CubicSplinePulse` | |
| 190 | +| `MinimumTimeProblem` | Time-optimal control | Any (converts existing problem) | |
| 191 | +| `SamplingProblem` | Robust control over parameters | Any (wraps existing problem) | |
| 192 | + |
| 193 | +**SmoothPulseProblem** (for ZeroOrderPulse): |
| 194 | +- Adds derivative variables `:du`, `:ddu` for smoothness |
| 195 | +- Creates `DerivativeIntegrator` constraints enforcing `u[k+1] - u[k] = Δt * du[k]` |
| 196 | +- Applies quadratic regularization on `u`, `du`, `ddu` |
| 197 | + |
| 198 | +**SplinePulseProblem** (for spline pulses): |
| 199 | +- For `LinearSplinePulse`: `:du` represents slopes (added automatically) |
| 200 | +- For `CubicSplinePulse`: `:du` represents Hermite tangents (built into pulse) |
| 201 | +- Uses `DerivativeIntegrator` with spline semantics |
| 202 | + |
| 203 | +**Adding constraints to SplinePulseProblem:** When working with `SplinePulseProblem`, especially with dynamical timesteps (`Δt_bounds`), add constraints to the existing problem rather than recreating it: |
| 204 | + |
| 205 | +```julia |
| 206 | +# Create problem with dynamical timesteps |
| 207 | +qcp = SplinePulseProblem(qtraj, N; |
| 208 | + Q=100.0, R_u=1e-2, R_du=1e-4, |
| 209 | + Δt_bounds=(0.01, 0.5)) |
| 210 | + |
| 211 | +# Get trajectory and build additional constraints |
| 212 | +traj = get_trajectory(qcp) |
| 213 | +constraint = MyCustomConstraint(traj, ...) |
| 214 | + |
| 215 | +# Add to existing problem (preserves all problem structure) |
| 216 | +push!(qcp.prob.constraints, constraint) |
| 217 | + |
| 218 | +# Solve the quantum collocation problem |
| 219 | +solve!(qcp; options=ipopt_options) |
| 220 | + |
| 221 | +# Access results |
| 222 | +final_fidelity = fidelity(qcp.qtraj) # Use qcp.qtraj for quantum operations |
| 223 | +u_optimized = qcp.prob.trajectory[:u] # Use qcp.prob.trajectory for variables |
| 224 | +optimized_pulse = qcp.qtraj.pulse # For saving or bootstrapping |
| 225 | +``` |
| 226 | + |
| 227 | +**Important:** Do NOT recreate `DirectTrajOptProblem` when modifying a `SplinePulseProblem`, as this breaks the dynamical timestep mechanism and integration with the quantum trajectory. |
| 228 | + |
| 229 | +### Quantum Integrators (`quantum_integrators.jl`) |
| 230 | + |
| 231 | +`BilinearIntegrator` dispatches on trajectory type: |
| 232 | +```julia |
| 233 | +BilinearIntegrator(qtraj::UnitaryTrajectory, N) # → single integrator |
| 234 | +BilinearIntegrator(qtraj::KetTrajectory, N) # → single integrator |
| 235 | +BilinearIntegrator(qtraj::MultiKetTrajectory, N) # → Vector of integrators |
| 236 | +BilinearIntegrator(qtraj::SamplingTrajectory, N) # → Vector of integrators |
| 237 | +``` |
| 238 | + |
| 239 | +**Signature pattern:** `(qtraj, N::Int)` - creates NamedTrajectory internally |
| 240 | + |
| 241 | +### Quantum Constraints (`quantum_constraints.jl`) |
| 242 | + |
| 243 | +Fidelity constraints for minimum-time problems: |
| 244 | +- `FinalUnitaryFidelityConstraint(qtraj, traj, fidelity)` |
| 245 | +- `FinalKetFidelityConstraint(qtraj, traj, fidelity)` |
| 246 | +- `FinalCoherentKetFidelityConstraint(qtraj, traj, fidelity)` - For ensembles, preserves phase coherence |
| 247 | + |
| 248 | +## Component Naming Conventions |
| 249 | + |
| 250 | +| Component | Symbol | Description | |
| 251 | +|-----------|--------|-------------| |
| 252 | +| Unitary state | `:Ũ⃗` | Isomorphism-vectorized unitary | |
| 253 | +| Ket state | `:ψ̃` | Isomorphism-vectorized ket | |
| 254 | +| Ensemble kets | `:ψ̃1`, `:ψ̃2`, ... | Multiple states | |
| 255 | +| Sampling states | `:Ũ⃗1`, `:Ũ⃗2`, ... | States for each system sample | |
| 256 | +| Controls | `:u` | Pulse amplitudes (canonical name) | |
| 257 | +| Control derivative | `:du` | First derivative / slope | |
| 258 | +| Control 2nd derivative | `:ddu` | Second derivative / acceleration | |
| 259 | +| Timestep | `:Δt` | Duration of each timestep | |
| 260 | +| Time | `:t` | Accumulated time (always present) | |
| 261 | + |
| 262 | +## Testing Conventions |
| 263 | + |
| 264 | +Tests use `@testitem` blocks in source files: |
| 265 | +```julia |
| 266 | +@testitem "descriptive name" begin |
| 267 | + using QuantumCollocation |
| 268 | + using PiccoloQuantumObjects |
| 269 | + using DirectTrajOpt |
| 270 | + using LinearAlgebra |
| 271 | + # ... test code |
| 272 | +end |
| 273 | +``` |
| 274 | + |
| 275 | +Run tests with `TestItemRunner.@run_package_tests`. |
| 276 | + |
| 277 | +## Recent Changes (Update This!) |
| 278 | + |
| 279 | +### January 2026 |
| 280 | +- Removed `time_dependent=true` from test QuantumSystem constructions (`:t` is now always in trajectories) |
| 281 | +- Removed `adapt_trajectory`/`unadapt_trajectory` usage (control scaling removed) |
| 282 | +- Updated `BilinearIntegrator` signatures from `(qtraj, traj)` to `(qtraj, N)` |
| 283 | +- Added `FinalCoherentKetFidelityConstraint` for ensemble minimum-time problems |
| 284 | + |
| 285 | +### Removed Patterns (Don't Reintroduce!) |
| 286 | +- ❌ `adapt_trajectory` / `unadapt_trajectory` - Control scaling was removed |
| 287 | +- ❌ `@test sys.time_dependent` in tests - Not needed since `:t` is always present |
| 288 | +- ❌ `QuantumSystem(...; time_dependent=true)` in tests - Redundant |
| 289 | +- ❌ Control name `:a` - Canonical control name is `:u` |
| 290 | + |
| 291 | +## File Structure |
| 292 | + |
| 293 | +``` |
| 294 | +src/ |
| 295 | +├── QuantumCollocation.jl # Main module, reexports |
| 296 | +├── piccolo_options.jl # PiccoloOptions configuration |
| 297 | +├── quantum_control_problem.jl # QuantumControlProblem wrapper |
| 298 | +├── quantum_integrators.jl # BilinearIntegrator dispatch |
| 299 | +├── quantum_constraints.jl # Fidelity constraints |
| 300 | +├── quantum_objectives.jl # Fidelity objectives |
| 301 | +└── problem_templates/ |
| 302 | + ├── _problem_templates.jl # Submodule definition |
| 303 | + ├── smooth_pulse_problem.jl # ZeroOrderPulse optimization |
| 304 | + ├── spline_pulse_problem.jl # Spline pulse optimization |
| 305 | + ├── minimum_time_problem.jl # Time-optimal control |
| 306 | + └── sampling_problem.jl # Robust control |
| 307 | +``` |
| 308 | + |
| 309 | +## Common Gotchas |
| 310 | + |
| 311 | +1. **Pulse type determines problem template**: Use `SmoothPulseProblem` for `ZeroOrderPulse`, `SplinePulseProblem` for spline pulses |
| 312 | +2. **N is timesteps, not knot points**: For `N=51`, you get 50 intervals |
| 313 | +3. **Trajectories always have `:t`**: Time is accumulated automatically, no need for `time_dependent` flag |
| 314 | +4. **MultiKetTrajectory vs SamplingTrajectory**: Ensemble = same system, different initial states; Sampling = different systems, same initial state |
| 315 | +5. **Fidelity vs Infidelity**: Objectives minimize infidelity, constraints bound infidelity (e.g., `1 - fidelity ≤ 1 - 0.99`) |
| 316 | +6. **Bootstrapping between system sizes**: Pass the **pulse object** (`qcp.qtraj.pulse`), not the `NamedTrajectory` or extracted controls. This preserves both `u` and `du` optimization state. |
0 commit comments