#+TITLE: Code examples #+SETUPFILE: ../tools/theme.setup #+INCLUDE: ../tools/lib.org In this section, we provide hands-on examples to demonstrate the capabilities of the QMCkl library. We furnish code samples in C, Fortran, and Python, serving as exhaustive tutorials for effectively leveraging QMCkl. For simplicity, we assume that the wave function parameters are stored in a [[https://github.com/TREX-CoE/trexio][TREXIO]] file. * Overlap matrix in the MO basis The focal point of this example is the numerical evaluation of the overlap matrix in the MO basis. Utilizing QMCkl, we approximate the MOs at discrete grid points to compute the overlap matrix \( S_{ij} \) as follows: \[ S_{ij} = \int \phi_i(\mathbf{r})\, \phi_j(\mathbf{r}) \text{d}\mathbf{r} \approx \sum_k \phi_i(\mathbf{r}_k)\, \phi_j(\mathbf{r}_k) \delta\mathbf{r} \] The code starts by reading a wave function from a TREXIO file. This is accomplished using the ~qmckl_trexio_read~ function, which populates a ~qmckl_context~ with the necessary wave function parameters. The context serves as the primary interface for interacting with the QMCkl library, encapsulating the state and configurations of the system. Subsequently, the code retrieves various attributes such as the number of nuclei ~nucl_num~ and coordinates ~nucl_coord~. These attributes are essential for setting up the integration grid. The core of the example lies in the numerical computation of the overlap matrix. To achieve this, the code employs a regular grid in three-dimensional space, and the grid points are then populated into the ~qmckl_context~ using the ~qmckl_set_point~ function. The MO values at these grid points are computed using the ~qmckl_get_mo_basis_mo_value~ function. These values are then used to calculate the overlap matrix through a matrix multiplication operation facilitated by the ~qmckl_dgemm~ function. The code is also instrumented to measure the execution time for the MO value computation, providing an empirical assessment of the computational efficiency. Error handling is robustly implemented at each stage to ensure the reliability of the simulation. In summary, this example serves as a holistic guide for leveraging the QMCkl library, demonstrating its ease of use. It provides a concrete starting point for researchers and developers interested in integrating QMCkl into their QMC code. ** Python :PROPERTIES: :header-args: :tangle mo_ortho.py :END: In this example, we will compute numerically the overlap between the molecular orbitals: \[ S_{ij} = \int \phi_i(\mathbf{r}) \phi_j(\mathbf{r}) \text{d}\mathbf{r} \sim \sum_{k=1}^{N} \phi_i(\mathbf{r}_k) \phi_j(\mathbf{r}_k) \delta \mathbf{r} \] \[ S_{ij} = \langle \phi_i | \phi_j \rangle \sim \sum_{k=1}^{N} \langle \phi_i | \mathbf{r}_k \rangle \langle \mathbf{r}_k | \phi_j \rangle \] #+begin_src python :exports code import numpy as np import qmckl #+end_src #+RESULTS: First, we create a context for the QMCkl calculation, and load the wave function stored in =h2o_5z.h5= inside it. It is a Hartree-Fock determinant for the water molecule in the cc-pV5Z basis set. #+begin_src python :exports code trexio_filename = "..//share/qmckl/test_data/h2o_5z.h5" context = qmckl.context_create() qmckl.trexio_read(context, trexio_filename) #+end_src #+RESULTS: : None We now define the grid points $\mathbf{r}_k$ as a regular grid around the molecule. We fetch the nuclear coordinates from the context, #+begin_src python :exports code nucl_num = qmckl.get_nucleus_num(context) nucl_charge = qmckl.get_nucleus_charge(context, nucl_num) nucl_coord = qmckl.get_nucleus_coord(context, 'N', nucl_num*3) nucl_coord = np.reshape(nucl_coord, (3, nucl_num)) for i in range(nucl_num): print("%d %+f %+f %+f"%(int(nucl_charge[i]), nucl_coord[i,0], nucl_coord[i,1], nucl_coord[i,2]) ) #+end_src #+begin_example 8 +0.000000 +0.000000 +0.000000 1 -1.430429 +0.000000 -1.107157 1 +1.430429 +0.000000 -1.107157 #+end_example and compute the coordinates of the grid points: #+begin_src python :exports code nx = ( 120, 120, 120 ) shift = np.array([5.,5.,5.]) point_num = nx[0] * nx[1] * nx[2] rmin = np.array( list([ np.min(nucl_coord[:,a]) for a in range(3) ]) ) rmax = np.array( list([ np.max(nucl_coord[:,a]) for a in range(3) ]) ) linspace = [ None for i in range(3) ] step = [ None for i in range(3) ] for a in range(3): linspace[a], step[a] = np.linspace(rmin[a]-shift[a], rmax[a]+shift[a], num=nx[a], retstep=True) dr = step[0] * step[1] * step[2] #+end_src #+RESULTS: Now the grid is ready, we can create the list of grid points $\mathbf{r}_k$ on which the MOs $\phi_i$ will be evaluated, and transfer them to the QMCkl context: #+begin_src python :exports code point = [] for x in linspace[0]: for y in linspace[1]: for z in linspace[2]: point += [ [x, y, z] ] point = np.array(point) point_num = len(point) qmckl.set_point(context, 'N', point_num, np.reshape(point, (point_num*3))) #+end_src #+RESULTS: : None Then, we evaluate all the MOs at the grid points (and time the execution), and thus obtain the matrix $M_{ki} = \langle \mathbf{r}_k | \phi_i \rangle = \phi_i(\mathbf{r}_k)$. #+begin_src python :exports code import time mo_num = qmckl.get_mo_basis_mo_num(context) before = time.time() mo_value = qmckl.get_mo_basis_mo_value(context, point_num*mo_num) after = time.time() mo_value = np.reshape( mo_value, (point_num, mo_num) ).T # Transpose to get mo_num x point_num print("Number of MOs: ", mo_num) print("Number of grid points: ", point_num) print("Execution time : ", (after - before), "seconds") #+end_src #+begin_example Number of MOs: 201 Number of grid points: 1728000 Execution time : 5.577778577804565 seconds #+end_example and finally we compute the overlap between all the MOs as $M.M^\dagger$. #+begin_src python :exports code overlap = mo_value @ mo_value.T * dr print (overlap) #+end_src #+begin_example [[ 9.88693941e-01 2.34719693e-03 -1.50518232e-08 ... 3.12084178e-09 -5.81064929e-10 3.70130091e-02] [ 2.34719693e-03 9.99509628e-01 3.18930040e-09 ... -2.46888958e-10 -1.06064273e-09 -7.65567973e-03] [-1.50518232e-08 3.18930040e-09 9.99995073e-01 ... -5.84882580e-06 -1.21598117e-06 4.59036468e-08] ... [ 3.12084178e-09 -2.46888958e-10 -5.84882580e-06 ... 1.00019107e+00 -2.03342837e-04 -1.36954855e-08] [-5.81064929e-10 -1.06064273e-09 -1.21598117e-06 ... -2.03342837e-04 9.99262427e-01 1.18264754e-09] [ 3.70130091e-02 -7.65567973e-03 4.59036468e-08 ... -1.36954855e-08 1.18264754e-09 8.97215950e-01]] #+end_example ** C In this example, electron-nucleus cusp fitting is added. :PROPERTIES: :header-args: :tangle mo_ortho.c :END: In this example, we will compute numerically the overlap between the molecular orbitals: \[ S_{ij} = \int \phi_i(\mathbf{r}) \phi_j(\mathbf{r}) \text{d}\mathbf{r} \sim \sum_{k=1}^{N} \phi_i(\mathbf{r}_k) \phi_j(\mathbf{r}_k) \delta \mathbf{r} \] \[ S_{ij} = \langle \phi_i | \phi_j \rangle \sim \sum_{k=1}^{N} \langle \phi_i | \mathbf{r}_k \rangle \langle \mathbf{r}_k | \phi_j \rangle \] We apply the cusp fitting procedure, so the MOs might deviate slightly from orthonormality. #+begin_src c :exports code #include #include #include #include int main(int argc, char** argv) { const char* trexio_filename = "..//share/qmckl/test_data/h2o_5z.h5"; qmckl_exit_code rc = QMCKL_SUCCESS; #+end_src First, we create a context for the QMCkl calculation, and load the wave function stored in =h2o_5z.h5= inside it. It is a Hartree-Fock determinant for the water molecule in the cc-pV5Z basis set. #+begin_src c :exports code qmckl_context context = qmckl_context_create(); rc = qmckl_trexio_read(context, trexio_filename, strlen(trexio_filename)); if (rc != QMCKL_SUCCESS) { fprintf(stderr, "Error reading TREXIO file:\n%s\n", qmckl_string_of_error(rc)); exit(1); } #+end_src We impose the electron-nucleus cusp fitting to occur when the electrons are close to the nuclei. The critical distance is 0.5 atomic units for hydrogens and 0.1 for the oxygen. To identify which atom is an oxygen and which are hydrogens, we fetch the nuclear charges from the context. #+begin_src c :exports code int64_t nucl_num; rc = qmckl_get_nucleus_num(context, &nucl_num); if (rc != QMCKL_SUCCESS) { fprintf(stderr, "Error getting nucl_num:\n%s\n", qmckl_string_of_error(rc)); exit(1); } double nucl_charge[nucl_num]; rc = qmckl_get_nucleus_charge(context, &(nucl_charge[0]), nucl_num); if (rc != QMCKL_SUCCESS) { fprintf(stderr, "Error getting nucl_charge:\n%s\n", qmckl_string_of_error(rc)); exit(1); } double r_cusp[nucl_num]; for (size_t i=0 ; i rmax[j] ? nucl_coord[i][j] : rmax[j]; } } rmin[0] -= shift[0]; rmin[1] -= shift[1]; rmin[2] -= shift[2]; rmax[0] += shift[0]; rmax[1] += shift[1]; rmax[2] += shift[2]; double step[3]; double* linspace[3]; for (int i=0 ; i<3 ; ++i) { linspace[i] = (double*) calloc( sizeof(double), nx[i] ); if (linspace[i] == NULL) { fprintf(stderr, "Allocation failed (linspace)\n"); exit(1); } step[i] = (rmax[i] - rmin[i]) / ((double) (nx[i]-1)); for (size_t j=0 ; j program main use iso_c_binding use qmckl implicit none ! Declare variables integer :: argc character(128) :: trexio_filename, err_msg integer(qmckl_exit_code) :: rc integer(qmckl_context) :: context integer*8 :: nucl_num, mo_num, point_num double precision, allocatable :: nucl_coord(:,:) integer*8 :: nx(3) double precision, dimension(3) :: shift, step, rmin, rmax double precision, allocatable :: mo_value(:,:), overlap(:,:), point(:), linspace(:,:) double precision :: before, after, dr integer*8 :: i,j,k,m ! Initialize variables err_msg = ' ' argc = command_argument_count() if (argc /= 1) then print *, "Usage: ./program " stop -1 end if call get_command_argument(1, trexio_filename) rc = QMCKL_SUCCESS ! Create a QMCkl context context = qmckl_context_create() ! Read the TREXIO file into the context rc = qmckl_trexio_read(context, trim(trexio_filename), len(trexio_filename)*1_8) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error reading TREXIO file:", err_msg stop -1 end if ! Retrieve the number of nuclei rc = qmckl_get_nucleus_num(context, nucl_num) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error getting nucl_num:", err_msg stop -1 end if ! Retrieve the nuclear coordinates allocate(nucl_coord(3, nucl_num)) rc = qmckl_get_nucleus_coord(context, 'N', nucl_coord, nucl_num * 3_8) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error getting nucl_coord:", err_msg stop -1 end if ! Retrieve the number of MOs rc = qmckl_get_mo_basis_mo_num(context, mo_num) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error getting mo_num:", err_msg stop -1 end if ! Initialize grid points for the calculation nx = (/ 120, 120, 120 /) shift = (/ 5.d0, 5.d0, 5.d0 /) point_num = nx(1) * nx(2) * nx(3) ! Initialize rmin and rmax rmin = nucl_coord(:,1) rmax = nucl_coord(:,1) ! Update rmin and rmax based on nucl_coord do i = 1, 3 do j = 1, nucl_num rmin(i) = min(nucl_coord(i,j), rmin(i)) rmax(i) = max(nucl_coord(i,j), rmax(i)) end do end do ! Apply shift rmin = rmin - shift rmax = rmax + shift ! Initialize linspace and step allocate(linspace(3, maxval(nx))) do i = 1, 3 step(i) = (rmax(i) - rmin(i)) / real(nx(i) - 1, 8) do j = 1, nx(i) linspace(i, j) = rmin(i) + (j - 1) * step(i) end do end do ! Initialize point array allocate(point(3 * point_num)) m = 1 do i = 1, nx(1) do j = 1, nx(2) do k = 1, nx(3) point(m) = linspace(1, i); m = m + 1 point(m) = linspace(2, j); m = m + 1 point(m) = linspace(3, k); m = m + 1 end do end do end do deallocate(linspace) ! Set points in QMCKL context rc = qmckl_set_point(context, 'N', point_num, point, point_num * 3) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error setting point:", err_msg stop -1 end if ! Perform the actual calculation and measure the time taken call cpu_time(before) allocate(mo_value(point_num, mo_num)) rc = qmckl_get_mo_basis_mo_value(context, mo_value, point_num * mo_num) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error getting mo_value:", err_msg stop end if call cpu_time(after) write(*,*) "Number of MOs:", mo_num write(*,*) "Number of grid points:", point_num write(*,*) "Execution time:", (after - before), "seconds" ! Compute the overlap matrix dr = step(1) * step(2) * step(3) allocate(overlap(mo_num, mo_num)) rc = qmckl_dgemm(context, 'N', 'T', mo_num, mo_num, point_num, dr, & mo_value, mo_num, mo_value, mo_num, 0.d0, overlap, mo_num) ! Print the overlap matrix do i = 1, mo_num write(*,'(i4)', advance='no') i do j = 1, mo_num write(*,'(f8.4)', advance='no') overlap(i, j) end do write(*,*) end do ! Clean-up and exit deallocate(mo_value, overlap) rc = qmckl_context_destroy(context) if (rc /= QMCKL_SUCCESS) then call qmckl_string_of_error(rc, err_msg) write(*,*) "Error destroying context:", err_msg stop -1 end if end program main #+end_src * Fortran ** Checking errors All QMCkl functions return an error code. A convenient way to handle errors is to write an error-checking function that displays the error in text format and exits the program. #+NAME: qmckl_check_error #+begin_src f90 subroutine qmckl_check_error(rc, message) use qmckl implicit none integer(qmckl_exit_code), intent(in) :: rc character(len=*) , intent(in) :: message character(len=128) :: str_buffer if (rc /= QMCKL_SUCCESS) then print *, message call qmckl_string_of_error(rc, str_buffer) print *, str_buffer call exit(rc) end if end subroutine qmckl_check_error #+end_src ** Computing an atomic orbital on a grid :PROPERTIES: :header-args: :tangle ao_grid.f90 :END: The following program, in Fortran, computes the values of an atomic orbital on a regular 3-dimensional grid. The 100^3 grid points are automatically defined, such that the molecule fits in a box with 5 atomic units in the borders. This program uses the ~qmckl_check_error~ function defined above. To use this program, run #+begin_src bash :tangle no :exports code $ ao_grid #+end_src #+begin_src f90 :noweb yes <> program ao_grid use qmckl implicit none integer(qmckl_context) :: qmckl_ctx ! QMCkl context integer(qmckl_exit_code) :: rc ! Exit code of QMCkl functions character(len=128) :: trexio_filename character(len=128) :: str_buffer integer :: ao_id integer :: point_num_x integer(c_int64_t) :: nucl_num double precision, allocatable :: nucl_coord(:,:) integer(c_int64_t) :: point_num integer(c_int64_t) :: ao_num integer(c_int64_t) :: ipoint, i, j, k double precision :: x, y, z, dr(3) double precision :: rmin(3), rmax(3) double precision, allocatable :: points(:,:) double precision, allocatable :: ao_vgl(:,:,:) #+end_src Start by fetching the command-line arguments: #+begin_src f90 if (iargc() /= 3) then print *, 'Syntax: ao_grid ' call exit(-1) end if call getarg(1, trexio_filename) call getarg(2, str_buffer) read(str_buffer, *) ao_id call getarg(3, str_buffer) read(str_buffer, *) point_num_x if (point_num_x < 0 .or. point_num_x > 300) then print *, 'Error: 0 < point_num < 300' call exit(-1) end if #+end_src Create the QMCkl context and initialize it with the wave function present in the TREXIO file: #+begin_src f90 qmckl_ctx = qmckl_context_create() rc = qmckl_trexio_read(qmckl_ctx, trexio_filename, 1_8*len(trim(trexio_filename))) call qmckl_check_error(rc, 'Read TREXIO') #+end_src We need to check that ~ao_id~ is in the range, so we get the total number of AOs from QMCkl: #+begin_src f90 rc = qmckl_get_ao_basis_ao_num(qmckl_ctx, ao_num) call qmckl_check_error(rc, 'Getting ao_num') if (ao_id < 0 .or. ao_id > ao_num) then print *, 'Error: 0 < ao_id < ', ao_num call exit(-1) end if #+end_src Now we will compute the limits of the box in which the molecule fits. For that, we first need to ask QMCkl the coordinates of nuclei. #+begin_src f90 rc = qmckl_get_nucleus_num(qmckl_ctx, nucl_num) call qmckl_check_error(rc, 'Get nucleus num') allocate( nucl_coord(3, nucl_num) ) rc = qmckl_get_nucleus_coord(qmckl_ctx, 'N', nucl_coord, 3_8*nucl_num) call qmckl_check_error(rc, 'Get nucleus coord') #+end_src We now compute the coordinates of opposite points of the box, and the distance between points along the 3 directions: #+begin_src f90 rmin(1) = minval( nucl_coord(1,:) ) - 5.d0 rmin(2) = minval( nucl_coord(2,:) ) - 5.d0 rmin(3) = minval( nucl_coord(3,:) ) - 5.d0 rmax(1) = maxval( nucl_coord(1,:) ) + 5.d0 rmax(2) = maxval( nucl_coord(2,:) ) + 5.d0 rmax(3) = maxval( nucl_coord(3,:) ) + 5.d0 dr(1:3) = (rmax(1:3) - rmin(1:3)) / dble(point_num_x-1) #+end_src We now produce the list of point coordinates where the AO will be evaluated: #+begin_src f90 point_num = point_num_x**3 allocate( points(point_num, 3) ) ipoint=0 z = rmin(3) do k=1,point_num_x y = rmin(2) do j=1,point_num_x x = rmin(1) do i=1,point_num_x ipoint = ipoint+1 points(ipoint,1) = x points(ipoint,2) = y points(ipoint,3) = z x = x + dr(1) end do y = y + dr(2) end do z = z + dr(3) end do #+end_src We give the points to QMCkl: #+begin_src f90 rc = qmckl_set_point(qmckl_ctx, 'T', point_num, points, size(points)*1_8 ) call qmckl_check_error(rc, 'Setting points') #+end_src We allocate the space required to retrieve the values, gradients and Laplacian of all AOs, and ask to retrieve the values of the AOs computed at the point positions. #+begin_src f90 allocate( ao_vgl(ao_num, 5, point_num) ) rc = qmckl_get_ao_basis_ao_vgl(qmckl_ctx, ao_vgl, ao_num*5_8*point_num) call qmckl_check_error(rc, 'Setting points') #+end_src We finally print the value and Laplacian of the AO: #+begin_src f90 do ipoint=1, point_num print '(3(F10.6,X),2(E20.10,X))', points(ipoint, 1:3), ao_vgl(ao_id,1,ipoint), ao_vgl(ao_id,5,ipoint) end do #+end_src #+begin_src f90 deallocate( nucl_coord, points, ao_vgl ) end program ao_grid #+end_src