Skip to content

Commit b7705b3

Browse files
Merge pull request #241 from harmoniqs/refactor/template-redesign
Refactor/template redesign
2 parents 9666e76 + 55bc175 commit b7705b3

47 files changed

Lines changed: 6595 additions & 3233 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTEXT.md

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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.

Project.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "QuantumCollocation"
22
uuid = "0dc23a59-5ffb-49af-b6bd-932a8ae77adf"
3-
version = "0.9.0"
3+
version = "0.10.0"
44
authors = ["Aaron Trowbridge <aaron.j.trowbridge@gmail.com> and contributors"]
55

66
[deps]
@@ -19,12 +19,12 @@ TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"
1919
TrajectoryIndexingUtils = "6dad8b7f-dd9a-4c28-9b70-85b9a079bfc8"
2020

2121
[compat]
22-
DirectTrajOpt = "0.5"
22+
DirectTrajOpt = "0.8"
2323
Distributions = "0.25"
2424
ExponentialAction = "0.2"
2525
LinearAlgebra = "1.10, 1.11, 1.12"
26-
NamedTrajectories = "0.6"
27-
PiccoloQuantumObjects = "0.7"
26+
NamedTrajectories = "0.8"
27+
PiccoloQuantumObjects = "0.10"
2828
Random = "1.10, 1.11, 1.12"
2929
Reexport = "1.2"
3030
SparseArrays = "1.10, 1.11, 1.12"

docs/dev/ipopt_callbacks.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ T_max = 1.0
4242
u_bounds = [(-1.0, 1.0), (-1.0, 1.0)]
4343
T = 50
4444
Δt = 0.2
45-
sys = QuantumSystem(0.1 * GATES[:Z], [GATES[:X], GATES[:Y]], T_max, u_bounds)
45+
sys = QuantumSystem(0.1 * GATES[:Z], [GATES[:X], GATES[:Y]], u_bounds)
4646
ψ_init = Vector{ComplexF64}([1.0, 0.0])
4747
ψ_target = Vector{ComplexF64}([0.0, 1.0])
4848

@@ -70,7 +70,7 @@ end
7070
# Using a callback to get the best trajectory from all the optimization iterations
7171
T_max = 1.0
7272
u_bounds = [(-1.0, 1.0), (-1.0, 1.0)]
73-
sys2 = QuantumSystem(0.15 * GATES[:Z], [GATES[:X], GATES[:Y]], T_max, u_bounds)
73+
sys2 = QuantumSystem(0.15 * GATES[:Z], [GATES[:X], GATES[:Y]], u_bounds)
7474
ψ_init2 = Vector{ComplexF64}([0.0, 1.0])
7575
ψ_target2 = Vector{ComplexF64}([1.0, 0.0])
7676

0 commit comments

Comments
 (0)