Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from Zygote to Enzyme #182

Merged
merged 74 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
223915d
no branching for cutoffs
jgreener64 Jun 25, 2024
5b83a01
coord not needed for cutoff function
jgreener64 Jun 25, 2024
a4251a0
potential cutoff args
jgreener64 Jun 27, 2024
28113c8
check units changes
jgreener64 Jun 28, 2024
e2d3a96
pairwise interaction arguments and fields
jgreener64 Jun 28, 2024
fdd5111
remove Zygote functions
jgreener64 Jun 28, 2024
9467d76
remove chain rules
jgreener64 Jun 28, 2024
f4d6c80
interaction arguments
jgreener64 Jun 28, 2024
004f38c
pairwise interaction docs
jgreener64 Jun 28, 2024
0df6b9f
rename test groups
jgreener64 Jun 28, 2024
e8a6dd5
remove deps
jgreener64 Jun 28, 2024
6cc14b3
force and energy arguments
jgreener64 Jun 28, 2024
05a0a17
fix unit check
jgreener64 Jun 28, 2024
60d373b
rename coulombconst to coulomb_const
jgreener64 Jul 1, 2024
fbc0756
change specific interaction arguments
jgreener64 Jul 1, 2024
6134686
forces reuse memory
jgreener64 Jul 1, 2024
c0f47d2
add step_n to virial
jgreener64 Jul 2, 2024
06dc815
refactor energy functions
jgreener64 Jul 2, 2024
cf148a8
step_n for general interactions
jgreener64 Jul 2, 2024
7c91f14
loggers use step_n
jgreener64 Jul 2, 2024
0c3da3f
use step_n throughout
jgreener64 Jul 2, 2024
60bef87
reuse memory in simulators
jgreener64 Jul 2, 2024
913140f
reuse memory in simulators and couplers
jgreener64 Jul 5, 2024
be88ae2
gradient tests use Enzyme
jgreener64 Jul 5, 2024
ce94d70
move Enzyme code to extension
jgreener64 Jul 8, 2024
ce6c0f3
gradient test clearer typing
jgreener64 Jul 8, 2024
2df8505
LennardJones mixing and shortcut
jgreener64 Jul 18, 2024
10e11f7
Mie mixing and shortcut
jgreener64 Jul 18, 2024
ea60adc
soft sphere mixing and shortcut
jgreener64 Jul 18, 2024
3bb4b04
Buckingham mixing and shortcut
jgreener64 Jul 18, 2024
24fcc66
Coulomb mixing
jgreener64 Jul 18, 2024
dbce320
Gravity zero and + functions
jgreener64 Jul 18, 2024
ef8b4ef
add atom_type to Atom, reorder fields
jgreener64 Jul 19, 2024
3ac6899
rename run_loggers! to apply_loggers!
jgreener64 Jul 22, 2024
e9a362b
Use @kwdef for interactions
jgreener64 Jul 22, 2024
06d1c17
concrete function typing for interactions
jgreener64 Jul 22, 2024
8beea61
fix tests
jgreener64 Jul 23, 2024
67ef5dd
remove abstract interaction types
jgreener64 Jul 23, 2024
befdd31
rng arg for coordinate setup
jgreener64 Jul 24, 2024
7f8efba
make DMS test pass with Enzyme
jgreener64 Jul 24, 2024
5aad841
broadcast gradient injection
jgreener64 Jul 24, 2024
8f7c2ec
DMS protein gradient tests
jgreener64 Jul 24, 2024
238b763
move gradient code to relevant places
jgreener64 Jul 25, 2024
b081caa
move parameter extraction to files
jgreener64 Jul 25, 2024
c3c1618
deepcopy to copy
jgreener64 Jul 26, 2024
65e61ef
Add @inline to simulators for Enzyme
jgreener64 Jul 26, 2024
379e97c
Enzyme loggers fix
jgreener64 Jul 26, 2024
7ff6cdc
Enzyme compat
jgreener64 Jul 26, 2024
bc813b3
test simulation gradients on CI
jgreener64 Jul 26, 2024
68d51ea
remove unused gradient dependencies
jgreener64 Jul 26, 2024
3951042
OBC Born radii don't broadcast
jgreener64 Jul 26, 2024
ced1e7d
GBSA energy don't broadcast
jgreener64 Jul 26, 2024
37583fa
GBn2 force don't broadcast 1
jgreener64 Jul 26, 2024
bd3d9c7
GBn2 force don't broadcast 2
jgreener64 Jul 26, 2024
22c16e5
fix Colors weakdep
jgreener64 Oct 2, 2024
3b72ecf
fix Enzyme weakdep
jgreener64 Oct 2, 2024
c3325da
rename box_volume to volume
jgreener64 Oct 4, 2024
ce979b3
density function and logger
jgreener64 Oct 4, 2024
72a4710
add VolumeLogger
jgreener64 Oct 4, 2024
1525890
dipole_moment function
jgreener64 Oct 4, 2024
38a07c8
density test
jgreener64 Oct 4, 2024
002006f
update Enzyme version
jgreener64 Oct 7, 2024
c585953
add paper to docs
jgreener64 Oct 9, 2024
b6e305c
remove redundant method
jgreener64 Oct 17, 2024
f1e1fca
test options
jgreener64 Oct 17, 2024
3d7bc7d
n_dimensions fixes
jgreener64 Oct 17, 2024
6005825
don't do sim gradient tests on CI
jgreener64 Oct 17, 2024
ab6b225
rename ForceLogger to ForcesLogger
jgreener64 Oct 17, 2024
e5d5249
rename VelocityLogger to VelocitiesLogger
jgreener64 Oct 17, 2024
3e44115
rename CoordinateLogger to CoordinatesLogger
jgreener64 Oct 17, 2024
b5903ad
update API docs
jgreener64 Oct 17, 2024
bef88af
KernelDensity extension
jgreener64 Oct 17, 2024
c0c2a15
docs cover all docstrings
jgreener64 Oct 17, 2024
e9ad4ed
Colors compat
jgreener64 Oct 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
arch:
- x64
test:
- NotZygote
- Zygote
- NotGradients
- Gradients
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
Expand Down
20 changes: 6 additions & 14 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ AtomsCalculators = "a3e0e189-c65a-42c1-833c-339540406eb1"
BioStructures = "de9282ab-8554-53be-b2d6-f6c222edabfc"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
CellListMap = "69e1c6dd-3888-40e6-b3c8-31ac5f578864"
ChainRules = "082447d4-558c-5d27-93f4-14fc19e9eca2"
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
Chemfiles = "46823bd8-5fb3-5f92-9aa0-96921f3dd015"
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
FLoops = "cc61a311-1640-44b5-9fba-1b764f453329"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
PeriodicTable = "7b2266bf-644c-5ea3-82d8-af4bbd25a884"
Expand All @@ -34,17 +29,19 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
UnitfulAtomic = "a7773ee8-282e-5fa2-be4e-bd808c38a91a"
UnitfulChainRules = "f31437dd-25a7-4345-875f-756556e6935d"
UnsafeAtomicsLLVM = "d80eeb9a-aca5-4d75-85e5-170c8b632249"
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

[weakdeps]
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"

[extensions]
MollyEnzymeExt = "Enzyme"
MollyGLMakieExt = ["GLMakie", "Colors"]
MollyKernelDensityExt = "KernelDensity"
MollyPythonCallExt = "PythonCall"

[compat]
Expand All @@ -54,18 +51,15 @@ AtomsCalculators = "0.2"
BioStructures = "4"
CUDA = "4.2, 5"
CellListMap = "0.8.11, 0.9"
ChainRules = "1.44"
ChainRulesCore = "1"
Chemfiles = "0.10.3"
Colors = "0.11, 0.12"
Colors = "0.11, 0.12, 0.13"
Combinatorics = "1"
DataStructures = "0.18"
Distances = "0.10"
Distributions = "0.23, 0.24, 0.25"
Enzyme = "0.11.15, 0.12"
Enzyme = "0.13"
EzXML = "1"
FLoops = "0.2"
ForwardDiff = "0.10.35"
GLMakie = "0.8, 0.9, 0.10"
Graphs = "1.8"
KernelDensity = "0.5, 0.6"
Expand All @@ -81,7 +75,5 @@ StaticArrays = "1.8.2"
Statistics = "1.9"
Unitful = "1"
UnitfulAtomic = "1"
UnitfulChainRules = "0.1.2"
UnsafeAtomicsLLVM = "0.1, 0.2"
Zygote = "0.6.67"
julia = "1.9"
20 changes: 10 additions & 10 deletions benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ coords = [c1, c2]
dr = vector(c1, c2, boundary)
b1 = HarmonicBond(k=100_000.0u"kJ * mol^-1 * nm^-2", r0=0.6u"nm")

SUITE["interactions"]["LennardJones force" ] = @benchmarkable force($(LennardJones()), $(dr), $(c1), $(c2), $(a1), $(a1), $(boundary))
SUITE["interactions"]["LennardJones energy"] = @benchmarkable potential_energy($(LennardJones()), $(dr), $(c1), $(c2), $(a1), $(a1), $(boundary))
SUITE["interactions"]["Coulomb force" ] = @benchmarkable force($(Coulomb()), $(dr), $(c1), $(c2), $(a1), $(a1), $(boundary))
SUITE["interactions"]["Coulomb energy" ] = @benchmarkable potential_energy($(Coulomb()), $(dr), $(c1), $(c2), $(a1), $(a1), $(boundary))
SUITE["interactions"]["LennardJones force" ] = @benchmarkable force($(LennardJones()), $(dr), $(a1), $(a1))
SUITE["interactions"]["LennardJones energy"] = @benchmarkable potential_energy($(LennardJones()), $(dr), $(a1), $(a1))
SUITE["interactions"]["Coulomb force" ] = @benchmarkable force($(Coulomb()), $(dr), $(a1), $(a1))
SUITE["interactions"]["Coulomb energy" ] = @benchmarkable potential_energy($(Coulomb()), $(dr), $(a1), $(a1))
SUITE["interactions"]["HarmonicBond force" ] = @benchmarkable force($(b1), $(c1), $(c2), $(boundary))
SUITE["interactions"]["HarmonicBond energy"] = @benchmarkable potential_energy($(b1), $(c1), $(c2), $(boundary))

Expand Down Expand Up @@ -90,14 +90,14 @@ function test_sim(nl::Bool, parallel::Bool, f32::Bool, gpu::Bool)
end

if gpu
coords = CuArray(deepcopy(f32 ? starting_coords_f32 : starting_coords))
velocities = CuArray(deepcopy(f32 ? starting_velocities_f32 : starting_velocities))
atoms = CuArray([Atom(charge=f32 ? 0.0f0 : 0.0, mass=atom_mass, σ=f32 ? 0.2f0u"nm" : 0.2u"nm",
coords = CuArray(copy(f32 ? starting_coords_f32 : starting_coords))
velocities = CuArray(copy(f32 ? starting_velocities_f32 : starting_velocities))
atoms = CuArray([Atom(mass=atom_mass, charge=f32 ? 0.0f0 : 0.0, σ=f32 ? 0.2f0u"nm" : 0.2u"nm",
ϵ=f32 ? 0.2f0u"kJ * mol^-1" : 0.2u"kJ * mol^-1") for i in 1:n_atoms])
else
coords = deepcopy(f32 ? starting_coords_f32 : starting_coords)
velocities = deepcopy(f32 ? starting_velocities_f32 : starting_velocities)
atoms = [Atom(charge=f32 ? 0.0f0 : 0.0, mass=atom_mass, σ=f32 ? 0.2f0u"nm" : 0.2u"nm",
coords = copy(f32 ? starting_coords_f32 : starting_coords)
velocities = copy(f32 ? starting_velocities_f32 : starting_velocities)
atoms = [Atom(mass=atom_mass, charge=f32 ? 0.0f0 : 0.0, σ=f32 ? 0.2f0u"nm" : 0.2u"nm",
ϵ=f32 ? 0.2f0u"kJ * mol^-1" : 0.2u"kJ * mol^-1") for i in 1:n_atoms]
end

Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ makedocs(
prettyurls=(get(ENV, "CI", nothing) == "true"),
size_threshold_ignore=["api.md"],
),
modules=[Molly],
pages=[
"Home" => "index.md",
"Documentation" => "documentation.md",
Expand Down
16 changes: 11 additions & 5 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

The API reference can be found here.

Molly also re-exports [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) and [Unitful.jl](https://github.com/PainterQubits/Unitful.jl), making the likes of `SVector` and `1.0u"nm"` available when you call `using Molly`.
Molly re-exports [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl) and [Unitful.jl](https://github.com/PainterQubits/Unitful.jl), making the likes of `SVector` and `1.0u"nm"` available when you call `using Molly`.

The [`visualize`](@ref) function is in a package extension and is only available once you have called `using GLMakie`.
The [`ASECalculator`](@ref) code is in a package extension and is only available once you have called `using PythonCall`.
Package extensions are used in order to reduce the number of dependencies:
- To use [`visualize`](@ref), call `using GLMakie`.
- To use [`ASECalculator`](@ref), call `using PythonCall`.
- To use [`rdf`](@ref), call `using KernelDensity`.

## Exported names

```@index
Order = [:module, :type, :constant, :function, :macro]
Order = [:module, :type, :constant, :function, :macro]
```

## Docstrings

```@autodocs
Modules = [Molly]
Private = false
Order = [:module, :type, :constant, :function, :macro]
Order = [:module, :type, :constant, :function, :macro]
```
2 changes: 1 addition & 1 deletion docs/src/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Various environmental variables can be set to modify the tests:
- `VISTESTS` determines whether to run the [GLMakie.jl](https://github.com/JuliaPlots/Makie.jl) plotting tests which will error on remote systems where a display is not available, default `VISTESTS=1`.
- `GPUTESTS` determines whether to run the GPU tests, default `GPUTESTS=1`.
- `DEVICE` determines which GPU to run the GPU tests on, default `DEVICE=0`.
- `GROUP` can be used to run a subset of the tests, options `All`/`Protein`/`Zygote`/`NotZygote`, default `GROUP=All`.
- `GROUP` can be used to run a subset of the tests, options `All`/`Protein`/`Gradients`/`NotGradients`, default `GROUP=All`.
The CI run does not carry out all tests - for example the GPU tests are not run - and this is reflected in the code coverage.

## Benchmarks
Expand Down
47 changes: 19 additions & 28 deletions docs/src/differentiable.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ In the last few years, the deep learning revolution has broadened to include the
The concept of using automatic differentiation (AD) to obtain exact gradients through physical simulations has many interesting applications, including parameterising force fields and training neural networks to describe atomic potentials.

There are some projects that explore differentiable molecular simulations - see [Related software](@ref).
However Julia provides a strong suite of AD tools, with [Zygote.jl](https://github.com/FluxML/Zygote.jl) and [Enzyme.jl](https://github.com/EnzymeAD/Enzyme.jl) allowing source-to-source transformations for much of the language.
With Molly you can use the power of Zygote and Enzyme to obtain gradients through molecular simulations, even in the presence of complex interactions such as implicit solvation and stochasticity such as Langevin dynamics or the Andersen thermostat.
However Julia provides a strong suite of AD tools, with [Enzyme.jl](https://github.com/EnzymeAD/Enzyme.jl) allowing source-to-source transformations for much of the language.
With Molly you can use the power of Enzyme to obtain gradients through molecular simulations, even in the presence of complex interactions such as implicit solvation and stochasticity such as Langevin dynamics or the Andersen thermostat.
Reverse mode AD can be used on the CPU with multithreading and on the GPU; performance is typically within an order of magnitude of the primal run.
Forward mode AD can also be used on the CPU.
Pairwise, specific and general interactions work, along with neighbor lists, and the same abstractions for running simulations are used as in the main package.
Expand Down Expand Up @@ -44,7 +44,6 @@ end
Now we can set up and run the simulation in a similar way to that described in the [Molly documentation](@ref).
The difference is that we wrap the simulation in a `loss` function.
This returns a single value that we want to obtain gradients with respect to, in this case the difference between the value of the above function at the end of the simulation and a target distance.
The `Zygote.ignore()` block allows us to ignore code for the purposes of obtaining gradients; you could add the [`visualize`](@ref) function there for example.
```julia
using Zygote
using Format
Expand All @@ -66,8 +65,6 @@ neighbor_finder = DistanceNeighborFinder(
lj = LennardJones(
cutoff=DistanceCutoff(1.5),
use_neighbors=true,
force_units=NoUnits,
energy_units=NoUnits,
)
pairwise_inters = (lj,)
coords = place_atoms(n_atoms, boundary; min_dist=0.6)
Expand All @@ -78,8 +75,8 @@ simulator = VelocityVerlet(
)

function loss(σ, coords, velocities)
atoms = [Atom(0, 0.0, atom_mass, σ, 0.2, false) for i in 1:n_atoms]
loggers = (coords=CoordinateLogger(Float64, 10),)
atoms = [Atom(0, 0, atom_mass, 0.0, σ, 0.2) for i in 1:n_atoms]
loggers = (coords=CoordinatesLogger(Float64, 10),)

sys = System(
atoms=atoms,
Expand All @@ -98,10 +95,8 @@ function loss(σ, coords, velocities)
mms_end = mean_min_separation(Array(sys.coords), boundary)
loss_val = abs(mms_end - dist_true)

Zygote.ignore() do
printfmt("σ {:6.3f} | Mean min sep expected {:6.3f} | Mean min sep end {:6.3f} | Loss {:6.3f} | ",
σ, σ * (2 ^ (1 / 6)), mms_end, loss_val)
end
printfmt("σ {:6.3f} | Mean min sep expected {:6.3f} | Mean min sep end {:6.3f} | Loss {:6.3f} | ",
σ, σ * (2 ^ (1 / 6)), mms_end, loss_val)

return loss_val
end
Expand Down Expand Up @@ -186,8 +181,8 @@ simulator = VelocityVerlet(
)

function loss(θ)
atoms = [Atom(0, 0.0, atom_mass, 0.0, 0.0, false) for i in 1:n_atoms]
loggers = (coords=CoordinateLogger(Float64, 2),)
atoms = [Atom(0, 0, atom_mass, 0.0, 0.0, 0.0) for i in 1:n_atoms]
loggers = (coords=CoordinatesLogger(Float64, 2),)
specific_inter_lists = (
InteractionList2Atoms(
[1, 2, 4, 5],
Expand All @@ -204,9 +199,9 @@ function loss(θ)

sys = System(
atoms=atoms,
coords=deepcopy(coords),
coords=copy(coords),
boundary=boundary,
velocities=deepcopy(velocities),
velocities=copy(velocities),
specific_inter_lists=specific_inter_lists,
loggers=loggers,
force_units=NoUnits,
Expand All @@ -220,10 +215,8 @@ function loss(θ)
dist_end = 0.5 * (d1 + d2)
loss_val = abs(dist_end - dist_true)

Zygote.ignore() do
printfmt("θ {:5.1f}° | Final dist {:4.2f} | Loss {:5.3f} | ",
rad2deg(θ), dist_end, loss_val)
end
printfmt("θ {:5.1f}° | Final dist {:4.2f} | Loss {:5.3f} | ",
rad2deg(θ), dist_end, loss_val)

return loss_val
end
Expand Down Expand Up @@ -278,7 +271,7 @@ The plot of these shows that the gradient has the expected sign either side of t

## Neural network potentials

Since gradients can be computed with Zygote, [Flux](https://fluxml.ai) models can also be incorporated into simulations.
[Flux](https://fluxml.ai) models can also be incorporated into simulations.
Here we show a neural network in the force function, though they can also be used in other parts of the simulation.
This example also shows how gradients for multiple parameters can be obtained, in this case the parameters of the neural network.
The jump from single to multiple parameters is important because single parameters can be optimised using finite differencing, whereas differentiable simulation is well-placed to optimise many parameters simultaneously.
Expand Down Expand Up @@ -328,15 +321,15 @@ simulator = VelocityVerlet(
)

function loss()
atoms = [Atom(0, 0.0f0, mass, 0.0f0, 0.0f0, false) for i in 1:n_atoms]
loggers = (coords=CoordinateLogger(Float32, 10),)
atoms = [Atom(0, 0, mass, 0.0f0, 0.0f0, 0.0f0) for i in 1:n_atoms]
loggers = (coords=CoordinatesLogger(Float32, 10),)
general_inters = (NNBonds(),)

sys = System(
atoms=atoms,
coords=deepcopy(coords),
coords=copy(coords),
boundary=boundary,
velocities=deepcopy(velocities),
velocities=copy(velocities),
general_inters=general_inters,
loggers=loggers,
force_units=NoUnits,
Expand All @@ -350,10 +343,8 @@ function loss()
norm(vector(sys.coords[3], sys.coords[1], boundary))) / 3
loss_val = abs(dist_end - dist_true)

Zygote.ignore() do
printfmt("Dist end {:6.3f} | Loss {:6.3f}\n", dist_end, loss_val)
visualize(sys.loggers.coords, boundary, "sim.mp4"; show_boundary=false)
end
printfmt("Dist end {:6.3f} | Loss {:6.3f}\n", dist_end, loss_val)
visualize(sys.loggers.coords, boundary, "sim.mp4"; show_boundary=false)

return loss_val
end
Expand Down
Loading
Loading