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 nonadiabtic 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 relavent quantities for a given dynamics method.

What is the purpose of NQCCalculators?

NQCCalculators.jl formerly existed as a part of NQCDynamics.jl where it was concieved primarily as a way of calculating and caching relevant quantities for dynamics methods. While it continues to serve this purpose as a seperate module, it can now be more flexibly used in conjuction with NQCModels.jl as a collection of preimplemted methods for calculating the properties and observables of systems defined using the NQCModels.jl package.

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 sophisitcated 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 nessecary 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 to 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 nonadiabtic coupling matrices
  • a temporary matrix for ad-hoc assignment during calculations
Not all Caches store the same data

In general, the data the cache stores depends on the type of model and whether or not the atoms are being modelled with 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 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 coordintates 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 relavent data associated to a model acting at a set of given coordinates. If, however, we are only interested in calculating a specific datapoint at a given positon, 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 difference 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
update_\$(quantity)! vs evaluate_\$(quantity)

Notice that 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.