Skip to content

Commit 12a9980

Browse files
authored
Analysis functions for diatomic molecules. (#329)
- Added new DynamicsOutput: OutputDesorptionTrajectory #317 - Added new DynamicsOutput: OutputDesorptionAngle #317 - Created an `Analysis` submodule for common analysis functions - Add `Structure` submodule for common utility functions for atomic structure. * Add rudimentary tests for NQCDynamics.Structure * Improved method definitions, possible efficiency gain by using views * Add unit tests for diatomic analysis functions for desorption * Add "Analysis" group to unit testing * Added explanation for Structure module scope
1 parent a262d2f commit 12a9980

File tree

20 files changed

+158435
-6
lines changed

20 files changed

+158435
-6
lines changed

.github/workflows/CI.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
matrix:
1313
group:
1414
- Core
15+
- Analysis
1516
- InitialConditions
1617
- dynamics_classical
1718
- dynamics_mdef

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ dev/
77
*.dat
88
!/.gitignore
99
!/.github
10-
!/.codecov.yml
10+
!/.codecov.yml
11+
!/test/artifacts/desorption_dynamics.jld2
12+
!/test/artifacts/desorption_test.xyz

Project.toml

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
name = "NQCDynamics"
22
uuid = "36248dfb-79eb-4f4d-ab9c-e29ea5f33e14"
33
authors = ["James <[email protected]>"]
4-
version = "0.13.6"
4+
5+
version = "0.13.7"
6+
57

68
[deps]
79
AdvancedHMC = "0bf59076-c3b1-5ca4-86bd-e02cd72cde3d"
@@ -36,6 +38,7 @@ Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
3638
Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc"
3739
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
3840
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
41+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
3942
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
4043
StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
4144
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
@@ -58,7 +61,6 @@ FastLapackInterface = "1, 2"
5861
HDF5 = "0.15, 0.16, 0.17"
5962
Interpolations = "0.13, 0.14"
6063
MuladdMacro = "0.2"
61-
MKL_jll = "2023" # Pin to before v2024.X to ensure tests build successfully. See #324
6264
NQCBase = "0.2"
6365
NQCDistributions = "0.1"
6466
NQCModels = "0.8"
@@ -92,6 +94,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
9294
DiffEqDevTools = "f3b72e0c-5b89-59e1-b016-84e28bfd966d"
9395
DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503"
9496
FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41"
97+
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
9598
JuLIP = "945c410c-986d-556a-acb1-167a618e0462"
9699
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
97100
MKL = "33e6dc65-8f57-5167-99aa-e5a354878fb2"
@@ -103,4 +106,4 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
103106
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
104107

105108
[targets]
106-
test = ["Test", "SafeTestsets", "CSV", "DataFrames", "DiffEqNoiseProcess", "FiniteDiff", "DiffEqDevTools", "Logging", "MKL", "PyCall", "JuLIP", "Symbolics", "Statistics", "Plots"]
109+
test = ["Test", "SafeTestsets", "CSV", "DataFrames", "DiffEqNoiseProcess", "FiniteDiff", "DiffEqDevTools", "Logging", "MKL", "PyCall", "JuLIP", "Symbolics", "Statistics", "Plots", "JLD2"]

docs/make.jl

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ end
4444
"dynamicssimulations/dynamicssimulations.md"
4545
find_all_files("dynamicssimulations/dynamicsmethods")
4646
]
47+
"Outputs and Analysis" => find_all_files("output_and_analysis")
4748
"Examples" => find_all_files("examples")
4849
"integration_algorithms.md"
4950
"Developer documentation" => find_all_files("devdocs")

docs/src/api/NQCDynamics/analysis.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
```@setup logging
2+
@info "Expanding src/api/NQCDynamics/analysis.md..."
3+
start_time = time()
4+
```
5+
# Analysis
6+
7+
```@autodocs
8+
Modules=[NQCDynamics.Analysis, NQCDynamics.Analysis.Diatomic]
9+
```
10+
11+
```@setup logging
12+
runtime = round(time() - start_time; digits=2)
13+
@info "...done after $runtime s."
14+
```

docs/src/api/NQCDynamics/structure.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
```@setup logging
2+
@info "Expanding src/api/NQCDynamics/structure.md..."
3+
start_time = time()
4+
```
5+
# Structure
6+
This submodule contains utility functions to analyse and modify atomic structure data, such as interatomic distances, centres of mass, both with and without support for periodic boundary conditions.
7+
8+
These functions can be used to build more sophisticated output functions, or for basic analysis of simulation results in post.
9+
10+
This module **doesn't contain**:
11+
- Basic definitions of atomic structures (e.g. `Atoms`, `PeriodicCell`, ...). These are defined in [`NQCBase`](@ref).
12+
- Functions to generate atomic structures. These should be added to [`NQCDynamics.InitialConditions`](@ref).
13+
- Analysis functions for specific systems (e.g. molecules on surfaces). These should be added to [`NQCDynamics.Analysis`](@ref).
14+
15+
16+
## Method reference
17+
18+
19+
```@autodocs
20+
Modules=[NQCDynamics.Structure]
21+
```
22+
23+
```@setup logging
24+
runtime = round(time() - start_time; digits=2)
25+
@info "...done after $runtime s."
26+
```

docs/src/getting_started.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ For classical dynamics we also provide a timestep `dt` since we're using the
238238

239239
The final keyword argument `output` is used to specify the quantities we want
240240
to save during the dynamics.
241-
A list of the available quantities can be found [here](@ref DynamicsOutputs).
241+
A list of the available quantities can be found [here](@ref NQCDynamics.DynamicsOutputs).
242242

243243
!!! tip "Output format"
244244

docs/src/output_and_analysis/intro.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# [Simulations outputs and analysis functions](@id output_and_analysis)
2+
3+
NQCDynamics.jl is able to output a variety of common observables when running simulations with the `run_dynamics` function. Further output functions can be implemented easily as well.
4+
5+
In this section you will find an overview of all available output types, as well as explanations of some common analysis functions in the realm of surface chemistry which we have implemented in the package.
6+
7+
## DynamicsOutputs
8+
9+
In many examples within this documentation, you have seen calls to `run_dynamics`:
10+
11+
```julia
12+
ensemble = run_dynamics(sim, tspan, distribution;selection=1:20,
13+
dt=0.1u"fs", output=OutputPosition, trajectories=20, callback=terminate)
14+
```
15+
16+
Within `run_dynamics`, the `output` argument specifies the desired output values for each trajectory. `output` can either be given as a single function, or as a tuple of multiple output functions, for example:
17+
18+
```julia
19+
output=OutputPosition # or
20+
output=(OutputPosition, OutputVelocity, OutputKineticEnergy)
21+
22+
ensemble[3][:OutputPosition] # will output the positions at all timesteps in trajectory 3
23+
```
24+
25+
Every output type is a function which can use the [`DynamicsVariables`](@ref) and [`Simulation`](@ref) values of the respective trajectory, allowing you to create custom output types of any kind. See the [developer documentation] for more information on how to implement a custom output type.
26+
27+
You can find an overview of all available output types in the [`DynamicsOutputs`](@ref NQCDynamics.DynamicsOutputs) API.
28+
29+
## Analysis functions
30+
31+
The [Analysis](@ref Analysis) submodule in NQCDynamics contains functions commonly used in the analysis of trajectories to make the analysis of existing trajectories easier.
32+
Ideally, most observable quantities could be implemented with a combination of [`DynamicsOutputs`](@ref NQCDynamics.DynamicsOutputs) and `Reduction` types, however we might want to data from existing ensemble simulations where re-running the entire set of trajectories is impractical.
33+
34+
As a result, most functions in the `Analysis` submodule are also implemented as a `DynamicsOutput`.
35+
36+
### Convenient functions for periodic structures
37+
[`NQCDynamics.Structure`](@ref Structure) contains several useful functions for periodic structures, such as `pbc_distance, pbc_center_of_mass`.
38+
39+
These functions take into account periodic copies of the atoms in question, returning the respective values for the closest set of periodic copies.
40+
41+
### Analysis of diatomic molecules
42+
[`NQCDynamics.Analysis.Diatomic`](@ref Analysis)

src/Analysis/Analysis.jl

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Analysis functions common enough to be included in the main package.
3+
"""
4+
module Analysis
5+
using Reexport: @reexport
6+
7+
# Diatomic analysis functions @alexsp32
8+
include("diatomic.jl")
9+
export Diatomic
10+
11+
# Rebinding of quantise_diatomic under Analysis.
12+
using NQCDynamics: InitialConditions
13+
function quantise_diatomic(sim, v, r; args...)
14+
InitialConditions.QuantisedDiatomic.quantise_diatomic(sim, v, r; args...)
15+
end
16+
export quantise_diatomic
17+
18+
end

src/Analysis/diatomic.jl

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Analysis functions for surface chemistry of diatomic molecules.
3+
"""
4+
module Diatomic
5+
using NQCDynamics: AbstractSimulation, Simulation, get_positions, Structure
6+
using NQCBase
7+
using Unitful, UnitfulAtomic
8+
using LinearAlgebra
9+
using Statistics
10+
11+
# Surface normal projection
12+
surface_normal_height(x::AbstractVector, surface_normal::AbstractVector=[0,0,1])=norm(x.*normalize(surface_normal))
13+
14+
"""
15+
surface_distance_condition(x::AbstractArray, indices::Vector{Int}, simulation::AbstractSimulation; surface_distance_threshold=5.0*u"Å")
16+
17+
Checks that the diatomic molecule is at least `surface_distance_threshold` away from the highest substrate atom in the simulation.
18+
"""
19+
function surface_distance_condition(x::AbstractArray, indices::Vector{Int}, simulation::AbstractSimulation; surface_distance_threshold=5.0*u"Å")
20+
molecule_position=surface_normal_height(Structure.pbc_center_of_mass(x, indices..., simulation)) # molecule position wrt surface normal
21+
substrate_heights=surface_normal_height.(eachcol(get_positions(x)[:, symdiff(1:length(simulation.atoms.masses), indices)])) # substrate positions along surface normal
22+
highest_z=max(substrate_heights[substrate_heights.≤molecule_position]...) # Ignore substrate above molecule.
23+
#? surface_distance_threshold must be < vacuum above surface and > distance between substrate layers.
24+
if au_to_ang(molecule_position-highest_z)*u"Å"surface_distance_threshold
25+
@debug "Surface distance condition evaluated true."
26+
return true
27+
else
28+
return false
29+
end
30+
end
31+
32+
"""
33+
com_velocity_condition(x::AbstractArray, indices::Vector{Int}, simulation::AbstractSimulation; surface_normal::AbstractVector=[0,0,1])
34+
35+
Evaluates true if the centre of mass velocity vector of the diatomic molecule points to the surface.
36+
"""
37+
function com_velocity_condition(x::AbstractArray, indices::Vector{Int}, simulation::AbstractSimulation; surface_normal::AbstractVector=[0,0,1])
38+
if dot(Structure.velocity_center_of_mass(x, indices..., simulation), normalize(surface_normal))>0
39+
return false
40+
else
41+
return true
42+
end
43+
end
44+
45+
"""
46+
get_desorption_frame(trajectory::Vector, diatomic_indices::Vector{Int}, simulation::AbstractSimulation;surface_normal::Vector=[0,0,1], surface_distance_threshold=5.0*u"Å")
47+
48+
Determines the index in a trajectory where surface desorption begins.
49+
50+
This is evaluated using two conditions:
51+
52+
1. In the trajectory, the diatomic must be `surface_distance_threshold` or further away from the highest other atom. (In `surface_normal` direction).
53+
54+
2. Desorption begins at the turning point of the centre of mass velocity component along `surface_normal`, indicating overall movement away from the surface.
55+
"""
56+
function get_desorption_frame(trajectory::AbstractVector, diatomic_indices::Vector{Int}, simulation::AbstractSimulation;surface_normal::Vector = [0,0,1], surface_distance_threshold = 5.0*u"Å")
57+
desorbed_frame=findfirst(surface_distance_condition.(trajectory, Ref(diatomic_indices), Ref(simulation); surface_distance_threshold=surface_distance_threshold))
58+
59+
if isa(desorbed_frame, Nothing)
60+
@debug "No desorption event found."
61+
return nothing
62+
else
63+
@debug "Desorption observed in snapshot $(desorbed_frame)"
64+
leaving_surface_frame=findlast(com_velocity_condition.(view(trajectory, 1:desorbed_frame), Ref(diatomic_indices), Ref(simulation); surface_normal=surface_normal)) #ToDo testing views for memory efficiency, need to test time penalty. Also need to test if running on everything and findfirst-ing the Bool array is quicker.
65+
if leaving_surface_frame<0
66+
@warn "Desorption event detected, but no direction change detected."
67+
return nothing
68+
else
69+
return leaving_surface_frame
70+
end
71+
end
72+
end
73+
74+
function get_desorption_angle(trajectory::AbstractVector, indices::Vector{Int}, simulation::AbstractSimulation; surface_normal = [0,0,1], surface_distance_threshold = 5.0*u"Å")
75+
# First determine where the reaction occurred on the surface.
76+
desorption_frame = get_desorption_frame(trajectory, indices, simulation;surface_distance_threshold = surface_distance_threshold, surface_normal = surface_normal)
77+
if isa(desorption_frame, Nothing)
78+
@debug "No desorption event detected in trajectory"
79+
return nothing
80+
end
81+
@debug "Desorption frame: $(desorption_frame)"
82+
# Determine the average centre of mass velocity to decrease error due to vibration and rotation orthogonal to true translational component.
83+
com_velocities = zeros(eltype(trajectory[1]), length(surface_normal), length(trajectory)-desorption_frame)
84+
for i in 1:length(trajectory)-desorption_frame
85+
com_velocities[:, i] .= Structure.velocity_center_of_mass(trajectory[i+desorption_frame], indices[1], indices[2], simulation)
86+
end
87+
average_velocity=mean(com_velocities;dims=2)
88+
# Now convert into an angle by arccos((a•b)/(|a|*|b|))
89+
return Structure.angle_between(vec(average_velocity), surface_normal)
90+
end
91+
92+
# 6×6 Cartesian to internal coordinate transformation.
93+
function transform_to_internal_coordinates(to_transform::Matrix, config::Matrix, index1::Int, index2::Int, sim::Simulation)
94+
U = transform_U(config, index1, index2, sim) # Generate transformation matrix
95+
return transpose(U) * to_transform * U
96+
end
97+
98+
function transform_from_internal_coordinates(to_transform::Matrix, config::Matrix, index1::Int, index2::Int, sim::Simulation)
99+
U_inv = inv(transform_U(config, index1, index2, sim))
100+
return transpose(U_inv) * to_transform * U_inv
101+
end
102+
103+
function transform_r(config, index1::Int, index2::Int)
104+
return sqrt(sum(mapslices(x->(x[2]-x[1])^2, config[:, [index1, index2]];dims=2)))
105+
end
106+
107+
function transform_r1(config, index1::Int, index2::Int)
108+
return transform_r(config[1:2, :], index1, index2)
109+
end
110+
111+
112+
"""
113+
transform_U(config::Matrix, index1::Int, index2::Int, sim::Simulation)
114+
115+
Builds diatomic Cartesian to internal coordinate transformation matrix as described in the SI of `10.1021/jacsau.0c00066`
116+
"""
117+
function transform_U(config::Matrix, index1::Int, index2::Int, sim::Simulation)
118+
masses = Structure.fractional_mass(sim, index1, index2)
119+
config[:,index2] .+= Structure.minimum_distance_translation(config, index1, index2, sim) # PBC wrap positions for correct transformation.
120+
r = transform_r(config, index1, index2)
121+
r1 = transform_r1(config, index1, index2)
122+
unity = LinearAlgebra.I(3)
123+
U_i1 = vcat(
124+
mapslices(x->(x[1]-x[2])/r, config[:, [index1, index2]]; dims=2),
125+
mapslices(x->(x[2]-x[1])/r, config[:, [index2, index1]]; dims=2),
126+
)
127+
U_i2 = vcat(
128+
mapslices(x->(x[2]-x[1])*(config[3, index2]-config[3, index1])/(r^2*r1), config[1:2, [index1, index2]]; dims=2),
129+
-r1/r^2,
130+
mapslices(x->(x[1]-x[2])*(config[3, index2]-config[3, index1])/(r^2*r1), config[1:2, [index2, index1]]; dims=2),
131+
r1/r^2,
132+
)
133+
U_i3 = [
134+
-(config[2, index1]-config[2, index2]),
135+
(config[1, index2]-config[1, index1]),
136+
0.0,
137+
-(config[2, index2]-config[2, index1]),
138+
(config[1, index1]-config[1, index2]),
139+
0.0,
140+
] ./ (r1^2)
141+
return hcat(U_i1, U_i2, U_i3, vcat(unity.*masses[1], unity.*masses[2]))
142+
end
143+
144+
export get_desorption_frame, get_desorption_angle, transform_from_internal_coordinates, transform_to_internal_coordinates, transform_U
145+
146+
end

0 commit comments

Comments
 (0)