# Driver Development Tutorial ## Starting a New Driver Project The easiest way to start work on a new driver is to use the MDI Driver Cookiecutter, which will automatically do most of the preparatory work for you. Using the cookiecutter will require that you have Python and Cookiecutter on your machine. To install Cookiecutter, you can use `pip`: :::{tab-set-code} ```bash pip install cookiecutter ``` ::: If you are running on an external machine and do not have write permission to the system directories, you may need to add the \c --user option: :::{tab-set-code} ```bash pip install --user cookiecutter ``` ::: Now use Cookiecutter to create a new driver project: :::{tab-set-code} ```bash cookiecutter git@github.com:MolSSI/MDI_Driver_Cookiecutter.git ``` ::: When prompted for the \c repo_name, type \c aimd, and then when prompted to select a language, type \c 1 for C++. This will create a new directory called \c aimd and populate it with some of the things you will need for the driver, including a copy of the MDI Library. The overall directory structure is: :::{tab-set-code} ```bash . └── aimd ├── CMakeLists.txt ├── aimd ├── CMakeLists.txt ├── STUBS_MPI │   └── mpi.h └── aimd.cpp └── mdi ... ``` ::: ## Writing the Driver Open the file called `aimd.cpp`. It contains the following code: :::{tab-set-code} ```c++ #include #include #include #include #include "mdi.h" using namespace std; int main(int argc, char **argv) { // Initialize the MPI environment MPI_Comm world_comm; MPI_Init(&argc, &argv); // Initialize MDI if ( MDI_Init(&argc, &argv) ) { throw std::runtime_error("The MDI library was not initialized correctly."); } // Confirm that MDI was initialized successfully int initialized_mdi; if ( MDI_Initialized(&initialized_mdi) ) { throw std::runtime_error("MDI_Initialized failed."); } if ( ! initialized_mdi ) { throw std::runtime_error("MDI not initialized: did you provide the -mdi option?."); } // Get the correct MPI intra-communicator for this code if ( MDI_MPI_get_world_comm(&world_comm) ) { throw std::runtime_error("MDI_MPI_get_world_comm failed."); } // Connect to the engines // // Perform the simulation // // Send the "EXIT" command to each of the engines // // Synchronize all MPI ranks MPI_Barrier(world_comm); MPI_Finalize(); return 0; } ``` ::: The first few lines of code simply initialize both MPI and and MDI. Don't worry if you don't have access to an MPI library - the code will just fall back to a set of dummy MPI functions provided in `STUBS_MPI`. The call to the `MDI_MPI_get_world_comm()` function obtains from MDI an MPI intra-communicator that spans all MPI ranks associated with the driver. This call is important when using the MPI protocol for MDI communication, because in that context, `MPI_COMM_WORLD` spans all ranks associated with both the driver and the engine(s). In general, an MDI-enabled code should call `MDI_MPI_get_world_comm()` immediately after calling `MDI_Init()`, and the resulting MPI intra-communicator should be used instead of `MPI_COMM_WORLD` throughout the remainder of the code. After initializing MDI, we need to connect the driver to its engines. For this particular tutorial, we will use a QM engine to calculate forces and an MM engine to update the atomic coordinates each timestep. In the MDI standard, engines request a connection to a driver. The driver will need to call the `MDI_Accept_communicator()` function to accept each connection. The general format of this functional call looks like this: :::{tab-set-code} ```c++ MDI_Comm comm; MDI_Accept_communicator(&comm); ``` ::: This will assign a new MDI communicator to \c comm, which functions very similarly to an MPI communicator and is used in certain MDI function calls to identify which engine is expected to send/receive data to/from the driver. Our AIMD driver will connect to two different engines, so we will be calling \c MDI_Accept_communicator() twice. We don't know the order in which the engines will request a connection to the driver, so we will need some way to determine which engine is the QM code and which engine is the MM code. This can be accomplished through the use of the `COORDS", qm_comm); MDI_Send(&coords, 3*natoms, MDI_DOUBLE, qm_comm); ``` ::: :::{admonition} Prerequisites As we will discuss later, this driver assumes that the engines have both been initialized with the same number of atoms, the same atom types and ordering of atom types, and the same cell dimensions. ::: The following code will handle all of the work associated with driving an AIMD simulation Replace the comment that reads `// Perform the simulation` with: :::{tab-set-code} ```c++ // Perform the simulation int niterations = 10; // Number of MD iterations int natoms; double qm_energy; double mm_energy; // Receive the number of atoms from the MM engine MDI_Send_command("COORDS", qm_comm); MDI_Send(&coords, 3*natoms, MDI_DOUBLE, qm_comm); // Have the MM engine proceed to the @FORCES node MDI_Send_command("@FORCES", mm_comm); // Get the QM energy MDI_Send_command("FORCES", mm_comm); MDI_Send(&forces, 3*natoms, MDI_DOUBLE, mm_comm); // Have the MM engine proceed to the @COORDS node, which completes the timestep MDI_Send_command("@COORDS", mm_comm); cout << "timestep: " << iiteration << " " << mm_energy << " " << qm_energy << endl; } ``` ::: The above code does the following: * Queries the number of atoms from the MM engine and allocates appropriately sized arrays for the coordinates and forces * Orders the MM engine to initialize a new MD simulation * Begins an iterative loop over MD iterations * Receives the atomic coordinates from the MM engine and updates the QM engine with them * Receives the energy of the QM and MM systems * Receives the atomic forces from the QM engine and sends them to the MM engine * Orders the MM engine update the atomic coordinates Finally, we should send an MDI command to cause the engines to exit. Replace the comment that reads `// Send the "EXIT" command to each of the engines` with the following: :::{tab-set-code} ```c++ // Send the "EXIT" command to each of the engines MDI_Send_command("EXIT", mm_comm); MDI_Send_command("EXIT", qm_comm); ``` ::: ## Driver Compilation The cookiecutter came with everything you need to build with CMake. To compile, simply navigate to the directory where you want the driver to be built, then do the following, replacing `` with the path to top directory of your driver repository. :::{tab-set-code} ```c++ cmake make ``` ::: ## Using the Driver To test the driver, you will need access to a QM engine and an MM engine. The user tutorial describes how to compile QE and LAMMPS for this purpose. You will also need appropriate input files for a test simulation. The test files in the `MDI_AIMD_Driver` repository (see the user tutorial) will work fine, as will the scripts. You can simply edit `MDI_AIMD_Driver/tests/locations/MDI_AIMD_Driver` to point to your new repository and run the scripts in the manner described in the user tutorial. ## Final Notes Note that although we used QE as the QM code and LAMMPS as the MM code, we swap out QE for any QM engine with MDI support or LAMMPS for any MM engine with MDI support. Furthermore, nothing about our AIMD driver strictly requires that the code named "QM" actually corresponds to a quantum mechanics code. You could, for example, use LAMMPS as the QM code, while another instance of LAMMPS serves as the MM code. Doing so would allow you to run a calculation that will produce to same results as simply running an MD simulation entirely within a single instance of LAMMPS. Although not generally useful for production runs, this can be a good way to benchmark the cost of the computational overhead introduced by using our driver as a middleman between two codes. In other cases, it may be desirable to use two different MM codes as the engines if, for example, you wish to use a force field from one MM code and a thermostat from another MM code. The only requirement on the engines is that the "QM" code supports all of the MDI commands sent by the AIMD driver to the "QM" engine, and that the "MM" code supports all of the MDI commands sent by the AIMD driver to the "MM" engine.