NQCCalculators.jl
NQCCalculators
exists as the intermediary between NQCModels
and NQCDynamics
. It is home to all of the utility functions designed to calculate properties of single particle, multi-particle and ring-polymer systems interacting under the complete range of model potentials and electronic Hamiltonians implemented in NQCModels
. In addition, the module defines the data structures, caches
, that NQCDynamics interacts with for performing simulation processes that depend on model quantities like the total energy derivative or the nonadiabatic coupling terms. These caches
are updated once per time-step during the dynamics simulation and are then subsequently used as a local address in memory that contains all of the relevant quantities for a given dynamics method.
NQCCalculators.jl
formerly existed as a part of NQCDynamics.jl
where it was conceived primarily as a way of calculating and caching relevant quantities for dynamics methods. While it continues to serve this purpose as a separate module, it can now be more flexibly used in conjunction with NQCModels.jl
as a collection of pre-implemented methods for calculating the properties and observables of systems defined using the NQCModels.jl
package. Examples of this use case are given below.
Using Caches
- a simple example
In the overview of NQCModels
we introduced a couple of simple QuantumModels and showed how to calculate some of their core properties with the potential
and derivative
functions. Here we will use the model
object to build a cache
that can calculate, manipulate and store more advanced properties of the system.
using NQCCalculators
using NQCModels
using Unitful, UnitfulAtomic
#Creating a model
model = DoubleWell()
#Creating an empty cache with 3 atoms that propagate according to the double well potential
DoubleWell_Cache = Create_Cache(model, 3, Float64)
QuantumModel_Cache{Float64, DoubleWell{Int64, Int64, Int64, Int64}}(DoubleWell{Int64, Int64, Int64, Int64}
mass: Int64 1
ω: Int64 1
γ: Int64 1
Δ: Int64 1
, [0.0 0.0; 0.0 0.0], LinearAlgebra.Hermitian{Float64, Matrix{Float64}}[[0.0 0.0; 0.0 0.0] [0.0 0.0; 0.0 0.0] [0.0 0.0; 0.0 0.0]], LinearAlgebra.Eigen{Float64, Float64, Matrix{Float64}, Vector{Float64}}([0.0, 0.0], [1.0 0.0; 0.0 1.0]), [[0.0 0.0; 0.0 0.0] [0.0 0.0; 0.0 0.0] [0.0 0.0; 0.0 0.0]], [[0.0 0.0; 0.0 0.0] [0.0 0.0; 0.0 0.0] [0.0 0.0; 0.0 0.0]], [0.0 0.0; 0.0 0.0])
Here we have created an empty cache with all of the data structures necessary to contain quantities that define 3 independent atoms propagating under a double well potential. Notice how we didn't need to define the types of atoms in the system, only how many there are. This is because the cache
only stores data that relates to the electronic structure of the system and therefore depends only on a set of atomic coordinates associated with positions within that electronic structure.
The cache
itself is a struct
that contains,
- the
model
- the
potential
matrix - the
potential
eigenvalues - the
potential
eigenvectors - the
derivative
matrices - the
adiabatic derivative
matrices - the
nonadiabatic coupling
matrices - a temporary matrix for ad-hoc assignment during calculations
In general, the data the cache
stores depends on the type of model
and whether or not the atoms are being modelled as ring polymers
. This is important for minimising memory overhead and customising the calculator routines to work most efficiently on each different system. More details about what data each type of cache
contains can be found in the relevant section of the NQCCalculators
docs.
Now that we have created an empty cache
to store all of the useful model data, we need to call calculation routines that will populate it with the data we wish to store. This can be done easily by calling the update_cache!
function, which takes as input our cache
and a set of nuclear coordinates and then populates the cache
object we passed in with all of the values associated with that set of coordinates.
For this example, we will pass the function DoubleWell_Cache
and a one-dimensional position value for each of the 3 atoms in our system.
#Populating DoubleWell_Cache with the values for a collection of atoms situated
#at 1.0, 2.0 and 3.0 angstroms
r = [austrip(1.0u"Å") austrip(2.0u"Å") austrip(3.0u"Å")]
update_cache!(DoubleWell_Cache, r)
#checking that update_cache!() has altered the values stored in DoubleWell_Cache
println(DoubleWell_Cache)
#Individual quantities can be extracted from the Cache by calling Cache.$quantity
ad_derivative = DoubleWell_Cache.adiabatic_derivative
eigvals = DoubleWell_Cache.eigen.values
eigvecs = DoubleWell_Cache.eigen.vectors
2×2 Matrix{Float64}:
0.0923453 0.995727
-0.995727 0.0923453
update_cache!
is an easy and efficient way of calculating all of the relevant data associated with a model acting at a set of given coordinates. If, however, we are only interested in calculating a specific datapoint at a given position, we can use the update_\$(quantity)!
functions. Similarly to update_cache!,
they only require a cache
and a position matrix, but these functions will only update the specific \$quantity
for which they are named.
r_2 = [austrip(0.9u"Å") austrip(2.2u"Å") austrip(4.1u"Å")]
update_eigen!(DoubleWell_Cache, r_2)
#we can check that the new eigenvalues and eigenvectors are different to before
eigvals = DoubleWell_Cache.eigen.values
eigvecs = DoubleWell_Cache.eigen.vectors
#we can also check that they are the only values that have been updated
ad_derivative = DoubleWell_Cache.adiabatic_derivative
1×3 Matrix{Matrix{Float64}}:
[0.499632 0.260076; 0.260076 3.27982] … [4.27908 0.260076; 0.260076 7.05927]
Alternatively, the evaluate_\$(quantity)
functions can be used to calculate a specific datapoint at a given positon, without updating the cache. The "evaluated" \$quantity
for which the function is named is output to an assigned variable, leaving the cache unchanged. Again, these functions only require a cache
and a position matrix.
r_3 = [austrip(0.8u"Å") austrip(2.4u"Å") austrip(4.2u"Å")]
DoubleWell_eigen = evaluate_eigen(DoubleWell_Cache, r_3)
#we can check that the new evaluated eigenvalues and eigenvectors are different to those currently present in the cache
eigvals_eval = DoubleWell_eigen.values
eigvals_cache = DoubleWell_Cache.eigen.values
eigvecs_cache = DoubleWell_eigen.vectors
eigvecs_cache = DoubleWell_Cache.eigen.vectors
2×2 Matrix{Float64}:
0.0923453 0.995727
-0.995727 0.0923453
!!! info "update!" vs evaluate" functions Notice how the update_\$(quantity)!
functions have an exclimation point, !
, whereas the evaluate_\$(quantity)
functions do not. This is common Julia notation for indicating an in-place function. An in-place function does not return a variable but instead updates one of the provided arguments "in-place". As such, the update_\$(quantity)!
functions are written as inplace functions as they "update" fields in the cache that they are provided with, without returning a new variable. For the evaluate_\$(quantity)
functions however, we want a new variable to be returned and for the cache in its original state to be preserved, so regular (non-in-place) functions are used (indicated by no !
in the name) that return new variables containing the "evaluated" quantity.