Writing Programs that Use QDYN

Now that we have installed the QDYN library, we can use it to write our own Fortran programs.

The QDYN project contains a cookiecutter template for generating a Fortran project based on QDYN. In this tutorial, we will walk through using this template to generate a simple “Hello World” program that can easily be expanded into something more useful.

To use the project template, you must install the cookiecutter program into your primary Python environment:

[1]:
! pip install --quiet cookiecutter

Generating a “Hello World” project with cookiecutter

The cookiecutter template is located in the cookiecutter subfolder of the QDYN repository. Thus, assuming that we have a checkout of the QDYN repository in ~/qdyn, we can generate a “Hello World” project with the following command:

[2]:
! cookiecutter $HOME/qdyn/cookiecutter --no-input --output-dir $HOME --overwrite-if-exists

Project settings:
    project_name: hello_world_proj
    prog_name: hello_world
    use_module: y
    module_name: global
    use_precompiler: n

Project settings for 'hello_world' OK
Your project has been created in /home/user/hello_world_proj

The --no-input option uses the default “Project settings” shown above. Without that option, we would have been prompted for settings.

See the template README.md for a summary of the options.

Structure of the “Hello World” project

Let’s switch to the ~/hello_world_proj folder

[3]:
%cd ~/hello_world_proj
/home/user/hello_world_proj

and see which files cookiecutter generated for us:

[4]:
! ls
Makefile.in  README.md  configure  global.f90  hello_world.f90

The actual source code is split into a program file, hello_world.f90 (cf. prog_name setting) and a global.f90 module (cf. module_name setting). The two files look as follows:

hello_world.f90
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
program hello_world

  use qdyn
  use global_mod
  implicit none

  character (len=error_l) :: module_name = 'hello_world'
  character(len=error_l), parameter :: routine_name = 'main'

  write(*,'("***** START OF PROGRAM ",A," ******")') trim(module_name)
  write(*,'(A)') trim(date_string())
  write(*,'("")')
  call print_qdyn_version(1)

  !call get_command_argument(1, runfolder)
  !call read_para(para, runfolder=runfolder, configfile='config')

  !quiet = .false.
  !call init(para, grid, gen, state=state, pulses=pulses, quiet=quiet)

  !call init_prop(gen, grid, prop_work, para, pulses)
  !call prop(state, grid, gen, prop_work, para, pulses)

  write(*,'("")')
  write(*,'(A)') trim(date_string())
  write(*,'("***** END OF PROGRAM ",A," ******")') trim(module_name)

contains

  ! program subroutines

end program hello_world
global.f90
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
module global_mod

  use qdyn
  implicit none

  character(len=file_l) :: runfolder
  type(para_t) :: para
  type(grid_t) :: grid
  type(pulse_t), allocatable :: pulses(:)
  type(dyn_generator_t) :: gen
  type(state_t) :: state
  type(prop_work_t) :: prop_work
  logical :: quiet

contains

  ! module subroutines

end module global_mod

The global_mod module in global.f90 contains all the global variables for the hello_world program. Using a separate module has the advantage of keeping the program source clean, but it also has two significant additional benefits:

  1. Suppose we were to later add additional programs to the project (e.g., one program for time propagation and a second program for optimal control). In that case, we can use global_mod in all of the programs and thus share the variable definitions.

  2. Any subroutines we may add to global.f90 have access to the module variables as global variables. Having global module variables can be extremely useful for, e.g., a prop_info_hook: it allows to access a variable inside the prop_info_hook that is not a direct argument of the prop_info_hook.

If we were sure that the project is only going to consist of a single program, we could have chosen n for the use_module project settings. In this case, the variables would have been declared directly in hello_world.f90.

Looking at the source code for hello_world.f90 in more detail, we see that it simply prints its start and end time and the version information of the QDYN library against which the program will be linked. You should always do this in your programs: it dramatically increases reproducibility to have this information included in any captured output.

The three commented blocks are exemplary for how one would extend the program into something more useful:

  1. Read config file data from a file config inside a runfolder given as the argument to the program (lines 15-16)

  2. Initialize data structures (Hamiltonian, pulses, state) from the data in the config file and data files in the runfolder that config may be referring to (lines 18-19)

  3. Do something with the data, usually either a propagation or optimal control (lines 21-22)

Most programs written on top of QDYN will follow this scheme.

Compiling the hello_world program

The README.md file contains the instructions for compiling the program:

README.md
# hello_world_proj

## Prerequisites

This program depends on the QDYN Fortran library. You must have `libqdyn.a` installed in a location that the `./configure` script can find.

## Compilation and Installation

To compile and install this program, run

    ./configure
    make
    make install

See

    ./configure --help
    make help

for available compilation options.

The first step is to call

[5]:
! ./configure
Using compiler FC=gfortran with FFLAGS=-O2 -fopenmp -ffree-line-length-0
Hello World: True
compiler supports allocatable components: True
compiler supports allocatable dummy arrays: True
compiler has EXECUTE_COMMAND_LINE routine: True
compiler has SYSTEM function: True
compiler has quad precision: True
compiler supports atanh: False
compiler supports ieee_arithmetic: True
./Makefile.in -> ./Makefile

PREFIX         : /home/user
BIN_INSTALL_DIR: $(PREFIX)/bin
QDYN_LIB       : $(PREFIX)/lib/qdyn
QDYN_INCLUDE   : $(PREFIX)/bin
PREP           :
FC             : gfortran
FFLAGS         : -O2 -fopenmp -ffree-line-length-0
LIBS           : -lqdyn -llapack -ldfftpack -llbfgsb -larpack -lblas -lwigxjpf -lfastwigxj
FEATURES       : backtraces, no-debug, no-no-ipo

This Python script will generate a Makefile based on the template in Makefile.in, taking into account compiler settings and the location of the installed QDYN library. This is, in fact, the same configure script we used when compiling the QDYN library itself! Calling it without arguments here uses the default options, which should be compatible with the default options used in the installation of QDYN. Most importantly, the QDYN_LIB and QDYN_INCLUDE variables must point to the folder where the libqdyn.a and the QDYN *.mod files were installed, respectively. The configure script has options for settings these paths. See configure --help for details.

Once the Makefile has been generated, we can then run make to compile the hello_world program:

[6]:
! make
make[1]: Entering directory '/home/user/hello_world_proj'
gfortran -O2 -fopenmp -ffree-line-length-0 -I/home/user/lib/qdyn/mod  -L/home/user/lib/qdyn  -c -o global.o global.f90
gfortran -O2 -fopenmp -ffree-line-length-0 -I/home/user/lib/qdyn/mod  -L/home/user/lib/qdyn  -c -o hello_world.o hello_world.f90
gfortran -O2 -fopenmp -ffree-line-length-0 -I/home/user/lib/qdyn/mod  -L/home/user/lib/qdyn  -o hello_world global.o hello_world.o -lqdyn -llapack -ldfftpack -llbfgsb -larpack -lblas -lwigxjpf -lfastwigxj
make[1]: Leaving directory '/home/user/hello_world_proj'

Running the program produces the following output:

[7]:
! ./hello_world
***** START OF PROGRAM hello_world ******
Sun Mar 14 04:38:24 +0000 2021

QDYN 2.0dev revision 8f9e5a7559eabc2d6987aa2848c799b0cfaf8a09 (installation_docker)
  features: no-check-cheby, no-check-newton, no-parallel-ham, parallel-oct, backtraces, no-debug, no-no-ipo

Sun Mar 14 04:38:24 +0000 2021
***** END OF PROGRAM hello_world ******

Lastly, running

[8]:
! make install
make[1]: Entering directory '/home/user/hello_world_proj'
cp hello_world /home/user/bin/
Finished installation

configure options were

PREFIX         : /home/user
BIN_INSTALL_DIR: $(PREFIX)/bin
QDYN_LIB       : $(PREFIX)/lib/qdyn
QDYN_INCLUDE   : $(PREFIX)/bin
PREP           :
FC             : gfortran
FFLAGS         : -O2 -fopenmp -ffree-line-length-0
LIBS           : -lqdyn -llapack -ldfftpack -llbfgsb -larpack -lblas -lwigxjpf -lfastwigxj
FEATURES       : backtraces, no-debug, no-no-ipo

make[1]: Leaving directory '/home/user/hello_world_proj'

will install the hello_world binary in the BIN_INSTALL_DIR. Assuming that directory is in your $PATH, you will then be able to run hello_world from anywhere.

Run

[9]:
! make help
make[1]: Entering directory '/home/user/hello_world_proj'
help                 Show this help
all                  Compile all binaries
install              Install compiled binaries into $(BIN_INSTALL_DIR)
uninstall            Remove compiled binaries from $(BIN_INSTALL_DIR)
clean                Remove compilation artifacts
distclean            Remove all generated files
make[1]: Leaving directory '/home/user/hello_world_proj'

to see what else you can do with make, e.g.,

[10]:
! make uninstall
make[1]: Entering directory '/home/user/hello_world_proj'
Running removal script
+ cd /home/user/bin
+ rm -f hello_world
Done
make[1]: Leaving directory '/home/user/hello_world_proj'

to remove the previously installed binary.

Extending the “Hello World” project

Going forward from this example, we would fill out the commented section of hello_world.f90 (of course, in a real project, you should probably have chosen a different prog_name in the cookiecutter project settings).

For many projects, we will want to add additional programs, e.g., one program for doing time propagation, one program for optimal control, and maybe more programs for generating input data (diagonalization!) or analyzing outputs. We may also need additional module files.

These are easy to add to Makefile.in. The core of Makefile.in is the following:

OBJ = global.o
ALL = hello_world

all: $(ALL)  ## Compile all binaries

hello_world: $(OBJ) hello_world.o
	$(PREP) $(MPIFORT) $(FFLAGS) $(LDFLAGS) -o $@ $^ $(LIBS)

For any new module file, we simply have to add the corresponding .o file to the OBJ list. For any new program in the project, we have to add the program name (without an extension) to the ALL list, and add two lines like the two lines for hello_world (only the first of those lines needs to be adapted). Everything else is automatically handled by make.

Warning

Make sure to edit Makefile.in, not the generated Makefile. Any changes to Makefile will be overwritten the next time you run the ./configure script.

You should also keep README.md up to date with additional information about the project.