Skip to content

Commit 536434b

Browse files
Add documentation on Forward and Reverse differentiation (#517)
1 parent 8fb7406 commit 536434b

File tree

5 files changed

+212
-1
lines changed

5 files changed

+212
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- `average_expect`
2121
- `std_expect`
2222
- Add support to ForwardDiff.jl for `sesolve` and `mesolve`. ([#515])
23+
- Add documentation about automatic differentiation. ([#517])
2324

2425
## [v0.33.0]
2526
Release date: 2025-07-22
@@ -286,3 +287,4 @@ Release date: 2024-11-13
286287
[#512]: https://github.com/qutip/QuantumToolbox.jl/issues/512
287288
[#513]: https://github.com/qutip/QuantumToolbox.jl/issues/513
288289
[#515]: https://github.com/qutip/QuantumToolbox.jl/issues/515
290+
[#517]: https://github.com/qutip/QuantumToolbox.jl/issues/517

docs/Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ Changelog = "5217a498-cd5d-4ec6-b8c2-9b85a09b6e3e"
55
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
66
DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244"
77
DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365"
8+
Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
9+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
810
QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab"
11+
SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1"
12+
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
913

1014
[compat]
11-
DocumenterVitepress = "0.2"
15+
DocumenterVitepress = "0.2"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const PAGES = [
6161
"Solving Problems with Time-dependent Hamiltonians" => "users_guide/time_evolution/time_dependent.md",
6262
"Bloch-Redfield master equation" => "users_guide/time_evolution/brmesolve.md",
6363
],
64+
"Automatic Differentiation" => "users_guide/autodiff.md",
6465
"Intensive parallelization on a Cluster" => "users_guide/cluster.md",
6566
"Hierarchical Equations of Motion" => "users_guide/HEOM.md",
6667
"Solving for Steady-State Solutions" => "users_guide/steadystate.md",

docs/src/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ features:
4646
title: Distributed Computing
4747
details: Distribute the computation over multiple nodes (e.g., a cluster). Simulate hundreds of quantum trajectories in parallel on a cluster, with, again, the same syntax as the simple case.
4848
link: /users_guide/cluster
49+
- icon: <img width="64" height="64" src="https://raw.githubusercontent.com/JuliaDiff/DifferentiationInterface.jl/main/DifferentiationInterface/docs/src/assets/logo.svg" />
50+
title: Differentiable Programming
51+
details: Enable gradient-based optimization for quantum algorithms. Compute gradients of quantum dynamics with respect to their parameters using automatic differentiation.
52+
link: /users_guide/autodiff
4953
---
5054
```
5155

@@ -95,6 +99,11 @@ In order to get a better experience and take full advantage of `QuantumToolbox`
9599
- [`SlurmClusterManager.jl`](https://github.com/JuliaParallel/SlurmClusterManager.jl)
96100
- Plotting Libraries:
97101
- [`Makie.jl`](https://github.com/MakieOrg/Makie.jl)
102+
- Automatic Differentiation:
103+
- [`SciMLSensitivity.jl`](https://github.com/SciML/SciMLSensitivity.jl)
104+
- [`Zygote.jl`](https://github.com/FluxML/Zygote.jl)
105+
- [`Enzyme.jl`](https://github.com/EnzymeAD/Enzyme.jl)
106+
- [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl)
98107
- Packages for other advanced usage:
99108
- [`StaticArrays.jl`](https://github.com/JuliaArrays/StaticArrays.jl)
100109
- [`SciMLOperators.jl`](https://github.com/SciML/SciMLOperators.jl)

docs/src/users_guide/autodiff.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# [Automatic Differentiation](@id doc:autodiff)
2+
3+
Automatic differentiation (AD) has emerged as a key technique in computational science, enabling exact and efficient computation of derivatives for functions defined by code. Unlike symbolic differentiation, which may produce complex and inefficient expressions, or finite-difference methods, which suffer from numerical instability and poor scalability, AD leverages the chain rule at the level of elementary operations to provide machine-precision gradients with minimal overhead.
4+
5+
In `QuantumToolbox.jl`, we have introduced preliminary support for automatic differentiation. Many of the core functions are compatible with AD engines such as [`Zygote.jl`](https://github.com/FluxML/Zygote.jl), [`Enzyme.jl`](https://github.com/EnzymeAD/Enzyme.jl) or [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl), allowing users to compute gradients of observables or cost functionals involving the time evolution of open quantum systems. Although `QuantumToolbox.jl` was not originally designed with AD in mind, its architecture—rooted in Julia’s multiple dispatch and generic programming model—facilitated the integration of AD capabilities. Many core functions were already compatible with AD engines out of the box.
6+
7+
!!! warning "Experimental Functionality"
8+
At present, this functionality is considered experimental and not all parts of the library are AD-compatible. Here we provide a brief overview of the current state of AD support in `QuantumToolbox.jl` and how to use it.
9+
10+
11+
## [Forward versus Reverse Mode AD](@id doc:autodiff:forward-versus-reverse)
12+
13+
Automatic differentiation can be broadly categorized into two modes: forward mode and reverse mode. The choice between these modes depends on the nature of the function being differentiated and the number of inputs and outputs:
14+
15+
- **Forward Mode AD**: This mode is particularly efficient for functions with many outputs and few inputs. It works by propagating derivatives from the inputs through the computational graph to the outputs. Forward mode is often preferred when the number of input variables is small, as it computes the derivative of each output with respect to each input in a single pass.
16+
17+
- **Reverse Mode AD**: In contrast, reverse mode is more efficient for functions with many inputs and few outputs. It operates by first computing the function's output and then propagating derivatives backward through the computational graph. This mode is commonly used in machine learning and optimization applications, where the loss function (output) depends on a large number of parameters (inputs).
18+
19+
Understanding the differences between these two modes can help users choose the most appropriate approach for their specific use case in `QuantumToolbox.jl`.
20+
21+
## [Differentiate the master equation](@id doc:autodiff:master-equation)
22+
23+
One of the primary use cases for automatic differentiation in `QuantumToolbox.jl` is the differentiation of the master equation. The master equation describes the time evolution of a quantum system's density matrix under the influence of non-unitary dynamics, such as dissipation and decoherence. Let's consider a set of parameters $\mathbf{p} = (p_1, p_2, \ldots, p_n)$ that influence the system's dynamics. The Hamiltonian and the dissipators will depend on these parameters
24+
25+
```math
26+
\hat{H} = \hat{H}(\mathbf{p}), \qquad \hat{L}_j = \hat{L}_j(\mathbf{p}),
27+
```
28+
29+
Hence, the density matrix will evolve according to the master equation
30+
31+
```@raw html
32+
<span id="eq:master-equation"></span>
33+
```
34+
```math
35+
\begin{align}
36+
\frac{d \hat{\rho}(\mathbf{p}, t)}{dt} =& -i[\hat{H}(\mathbf{p}), \hat{\rho}(\mathbf{p}, t)] \\
37+
&+ \sum_j \hat{L}_j(\mathbf{p}) \hat{\rho}(\mathbf{p}, t) \hat{L}_j(\mathbf{p})^\dagger - \frac{1}{2} \left\{ \hat{L}_j(\mathbf{p})^\dagger \hat{L}_j(\mathbf{p}), \hat{\rho}(\mathbf{p}, t) \right\} \, ,
38+
\end{align} \tag{1}
39+
```
40+
41+
which depends on the parameters $\mathbf{p}$ and time $t$.
42+
43+
We now want to compute the expectation value of an observable $\hat{O}$ at time $t$:
44+
45+
```math
46+
\langle \hat{O}(\mathbf{p}, t) \rangle = \text{Tr}[\hat{O} \hat{\rho}(\mathbf{p}, t)] \, ,
47+
```
48+
49+
which will also depend on the parameters $\mathbf{p}$ and time $t$.
50+
51+
Our goal is to compute the derivative of the expectation value with respect to the parameters:
52+
53+
```math
54+
\frac{\partial \langle \hat{O}(\mathbf{p}, t) \rangle}{\partial p_j} = \frac{\partial}{\partial p_j} \text{Tr}[\hat{O} \hat{\rho}(\mathbf{p}, t)] \, ,
55+
```
56+
57+
and to achieve this, we can use an AD engine like [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) (forward mode) or [`Zygote.jl`](https://github.com/FluxML/Zygote.jl) (reverse mode).
58+
59+
Let's apply this to a simple example of a driven-dissipative quantum harmonic oscillator. The Hamiltonian in the drive frame is given by
60+
61+
```math
62+
\hat{H} = \Delta \hat{a}^\dagger \hat{a} + F \left( \hat{a} + \hat{a}^\dagger \right) \, ,
63+
```
64+
65+
where $\Delta = \omega_0 - \omega_d$ is the cavity-drive detuning, $F$ is the drive strength, and $\hat{a}$ and $\hat{a}^\dagger$ are the annihilation and creation operators, respectively. The system is subject to a single dissipative channel with a Lindblad operator $\hat{L} = \sqrt{\gamma} \hat{a}$, where $\gamma$ is the dissipation rate. If we start from the ground state $\hat{\rho}(0) = \vert 0 \rangle \langle 0 \vert$, the systems evolves according to the master equation in [Eq. (1)](#eq:master-equation).
66+
67+
We now want to study the number of photons at the steady state, and how it varies with $\mathbf{p} = (\Delta, F, \gamma)$, namely $\nabla_\mathbf{p} \langle \hat{a}^\dagger \hat{a} \rangle (\mathbf{p}, t \to \infty)$. We can extract an analytical expression, in order to verify the correctness of the AD implementation:
68+
69+
```math
70+
\langle \hat{a}^\dagger \hat{a} \rangle_\mathrm{ss} = \frac{F^2}{\Delta^2 + \frac{\gamma^2}{4}} \, ,
71+
```
72+
73+
with the gradient given by
74+
75+
```math
76+
\nabla_\mathbf{p} \langle \hat{a}^\dagger \hat{a} \rangle_\mathrm{ss} =
77+
\begin{pmatrix}
78+
\frac{-2 F^2 \Delta}{(\Delta^2 + \frac{\gamma^2}{4})^2} \\
79+
\frac{2 F}{\Delta^2 + \frac{\gamma^2}{4}} \\
80+
\frac{-F^2 \gamma}{2 (\Delta^2 + \frac{\gamma^2}{4})^2}
81+
\end{pmatrix} \, .
82+
```
83+
84+
Although `QuantumToolbox.jl` has the [`steadystate`](@ref) function to directly compute the steady state without explicitly solving the master equation, here we use the [`mesolve`](@ref) function to integrate up to a long time $t_\mathrm{max}$, and then compute the expectation value of the number operator. We will demonstrate how to compute the gradient using both [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and [`Zygote.jl`](https://github.com/FluxML/Zygote.jl).
85+
86+
### [Forward Mode AD with ForwardDiff.jl](@id doc:autodiff:forward)
87+
88+
```@setup autodiff
89+
using QuantumToolbox
90+
```
91+
92+
We start by importing [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and defining the parameters and operators:
93+
94+
```@example autodiff
95+
using ForwardDiff
96+
97+
const N = 20
98+
const a = destroy(N)
99+
const ψ0 = fock(N, 0)
100+
const t_max = 40
101+
const tlist = range(0, t_max, 100)
102+
```
103+
104+
Then, we define a function that take the parameters `p` as an input and returns the expectation value of the number operator at `t_max`. We also define the analytical solution of the steady state photon number and its gradient for comparison:
105+
106+
```@example autodiff
107+
function my_f_mesolve_direct(p)
108+
H = p[1] * a' * a + p[2] * (a + a')
109+
c_ops = [sqrt(p[3]) * a]
110+
sol = mesolve(H, ψ0, tlist, c_ops, progress_bar = Val(false))
111+
return real(expect(a' * a, sol.states[end]))
112+
end
113+
114+
# Analytical solution
115+
function my_f_analytical(p)
116+
Δ, F, γ = p
117+
return F^2 / (Δ^2 + γ^2 / 4)
118+
end
119+
function my_grad_analytical(p)
120+
Δ, F, γ = p
121+
return [
122+
-2 * F^2 * Δ / (Δ^2 + γ^2 / 4)^2,
123+
2 * F / (Δ^2 + γ^2 / 4),
124+
-F^2 * γ / (2 * (Δ^2 + γ^2 / 4)^2)
125+
]
126+
end
127+
```
128+
129+
The gradient can be computed using `ForwardDiff.gradient`:
130+
131+
```@example autodiff
132+
Δ = 1.5
133+
F = 1.5
134+
γ = 1.5
135+
params = [Δ, F, γ]
136+
137+
grad_exact = my_grad_analytical(params)
138+
grad_fd = ForwardDiff.gradient(my_f_mesolve_direct, params)
139+
```
140+
141+
and test if the results match:
142+
143+
```@example autodiff
144+
isapprox(grad_exact, grad_fd; atol = 1e-5)
145+
```
146+
147+
### [Reverse Mode AD with Zygote.jl](@id doc:autodiff:reverse)
148+
149+
Reverse-mode differentiation is significantly more challenging than forward-mode when dealing ODEs, as the complexity arises from the need to propagate gradients backward through the entire time evolution of the quantum state.
150+
151+
`QuantumToolbox.jl` leverages the advanced capabilities of [`SciMLSensitivity.jl`](https://github.com/SciML/SciMLSensitivity.jl) to handle this complexity. [`SciMLSensitivity.jl`](https://github.com/SciML/SciMLSensitivity.jl) implements sophisticated methods for computing gradients of ODE solutions, such as the adjoint method, which computes gradients by solving an additional "adjoint" ODE backward in time. For more details on the adjoint method and other sensitivity analysis techniques, please refer to the [`SciMLSensitivity.jl` documentation](https://docs.sciml.ai/SciMLSensitivity/stable/).
152+
153+
In order to reverse-differentiate the master equation, we need to define the operators as [`QuantumObjectEvolution`](@ref) objects, which use [`SciMLOperators.jl`](https://github.com/SciML/SciMLOperators.jl) to represent parameter-dependent operators.
154+
155+
```@example autodiff
156+
using Zygote
157+
using SciMLSensitivity
158+
159+
# For SciMLSensitivity.jl
160+
coef_Δ(p, t) = p[1]
161+
coef_F(p, t) = p[2]
162+
coef_γ(p, t) = sqrt(p[3])
163+
H = QobjEvo(a' * a, coef_Δ) + QobjEvo(a + a', coef_F)
164+
c_ops = [QobjEvo(a, coef_γ)]
165+
const L = liouvillian(H, c_ops)
166+
167+
function my_f_mesolve(p)
168+
sol = mesolve(
169+
L,
170+
ψ0,
171+
tlist,
172+
progress_bar = Val(false),
173+
params = p,
174+
sensealg = BacksolveAdjoint(autojacvec = EnzymeVJP()),
175+
)
176+
177+
return real(expect(a' * a, sol.states[end]))
178+
end
179+
```
180+
181+
And the gradient can be computed using `Zygote.gradient`:
182+
183+
```@example autodiff
184+
grad_zygote = Zygote.gradient(my_f_mesolve, params)[1]
185+
```
186+
187+
Finally, we can compare the results from [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and [`Zygote.jl`](https://github.com/FluxML/Zygote.jl):
188+
189+
```@example autodiff
190+
isapprox(grad_fd, grad_zygote; atol = 1e-5)
191+
```
192+
193+
## [Conclusion](@id doc:autodiff:conclusion)
194+
195+
In this section, we have explored the integration of automatic differentiation into `QuantumToolbox.jl`, enabling users to compute gradients of observables and cost functionals involving the time evolution of open quantum systems. We demonstrated how to differentiate the master equation using both forward mode with [`ForwardDiff.jl`](https://github.com/JuliaDiff/ForwardDiff.jl) and reverse mode with [`Zygote.jl`](https://github.com/FluxML/Zygote.jl), showcasing the flexibility and power of automatic differentiation in quantum computing applications. AD can be applied to other functions in `QuantumToolbox.jl`, although the support is still experimental and not all functions are guaranteed to be compatible. We encourage users to experiment with AD in their quantum simulations and contribute to the ongoing development of this feature.

0 commit comments

Comments
 (0)