Canoe as a vessel for models
- CUP:
2
- Author:
Cheng Li
- Type:
Feature
- Created:
01-05-2024
Overview
We are trying to solve the following three problems:
How to build a numerical model as a lego toy, so that it can be easily disintegrated into small pieces and reassembled into a new toy.
How to make personal modifications to an existing model, without interfering with the original development process.
How to leverage the experience and knowledge from the industry, so that an academic model is built with the same quality as an industrial model.
Here comes Canoe, a vessel for building
your own numerical models. Canoe was originally developed for atmospheric
simulations, but it has evolved into a general purpose tool for building
any numerical models. It provides a collection of tools and a system for integrating
existing numerical models into a new model. Each component in Canoe
can be tested independently, or the model can be used as a whole. Adding a new
component to the model is as easy as adding a new lego block to a toy.
This CUP discusses the design of Canoe and how to use it to build your own
numerical model.
What is a build system
We primarily target the numerical model that requires a “build” process.
The “build” proccess compiles a set of source files into an executable program.
Therefore, programs written in C, C++, Fortran, rust, and other compiled languages
belongs to this category. Programs written in Python, Matlab, and other interpreted
languages do not require a build process, and they are not the target of this CUP.
However, Canoe can be used to build a Python interface whose backend is written
in C or C++.
A build system is a set of rules and tools that automate the build process. The most
one is make, which is a tool for building programs from instructions written in
a makefile. make is good for small projects, but it is not scalable for large
projects that consist of many source codes and dependencies. cmake is a more
powerful tool for building large projects. It is a meta-build system that generates
makefile for make. cmake is also a cross-platform, cross-language tool
that can generate makefile for Linux, Mac, and Windows and for code written in
C, C++, Fortran, and other languages. Many popular open-source projects use cmake
as their build system. Moreover, if a model is built with cmake, it can be easily
integrated into other projects that use cmake as their build system. Therefore,
we choose to use cmake as the build system Canoe.
How to cmake
At the core of cmake is the CMakeLists.txt file, a configuration file which
defines build settings. Every folder in a cmake project that contains source code
to be built should contain a CMakeLists.txt file. CMakeLists.txt is a text
file containing a set of commands. To illustrate, consider a simple project with two source
files, main.cpp and functions.cpp. The CMakeLists.txt for this project may look like
this:
cmake_minimum_required(VERSION 3.10)
project(SimpleProject)
add_executable(App main.cpp functions.cpp)
Here, cmake_minimum_required specifies the minimum CMake version required. project
names the project, and add_executable tells CMake to create an executable named
App from main.cpp and functions.cpp.
To internal build this project, you would create a build directory at the root
directory of the source code, change to the build directory, and
run cmake followed by the path to the project folder that contains the CMakeLists.txt
file. For example, on Linux or Mac:
Internal build method
mkdir build
cd build
cmake ..
make
In which, .. refers to the parent directory of the current directory (build).
You can use also an external build method, in which you create the build directory
outside the project folder and run cmake followed by the path to the project folder:
External build method
mkdir build
cd build
cmake /path/to/project
make
For simple tests, an internal build is sufficient. However, for large projects that may contain many different builds, versions, and configurations, an external build is preferred.
Regardless of internal or external builds, the cmake processes the CMakeLists.txt file
to generate standard build files suitable for your environment (e.g., Makefiles on Unix,
project files on Windows, etc). When you run cmake followed by the path to
CMakeLists.txt, it detects your environment, compiler, and available libraries,
adapting the build process accordingly. This abstraction allows developers to write code
without worrying about platform-specific build details.
If your computer has multiple cores, you can use the -j option to speed up the build
process by parallelizing the compilation. For example,
make -j4
uses four cores to build the project in parallel.
Canoe’s build system
Canoe uses cmake as its build system and manages the dependencies using commands
in the CMakeLists.txt files. The CMakeLists.txt file at the root
directory of Canoe contains the following commands:
include(${CMAKE_SOURCE_DIR}/cmake/yamlpp.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/gtest.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/athena.cmake)
In which, the include command includes the yamlpp.cmake, gtest.cmake, and
athena.cmake files, which are located in the cmake directory. These files
contain the commands for downloading and building the dependencies. For example,
the yamlpp.cmake file contains the following commands:
FetchContent_Declare(
yamlcpp
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
URL https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.7.0.tar.gz
)
FetchContent_MakeAvailable(yamlcpp)
When Canoe builds, it downloads the yaml-cpp source code from the URL specified
in the yamlcpp.cmake file and builds the dependency. The FetchContent_MakeAvailable
command makes the yaml-cpp library available to any other sources inside the Canoe
project. Because yaml-cpp is a header-only library, the library only contains the
header files. If a dependency is a library that requires compilation, Canoe will
build the library and link it to the Canoe project. For example, the gtest.cmake
downloads the google test source code and builds the gtest library with headers
and binary files.
Since dependent libraries are downloaded and built on the fly, Canoe requires internet
connection to build. However, once the dependencies are built, Canoe can be built
again offline. The Canoe build system is designed to be cross-platform and cross-language.
It is currently tested on Linu and Mac, and it can build C, C++, and Fortran programs.
All cmake related commands are placed in the cmake folder at the root directory.
One of them is the compiler.cmake file, which contains the commands for setting the
compiler flags. By default, Canoe detects the system compiler and sets the appropriate
flags. You can override the default setting by command line options. For example, to
set the compiler to clang and enable the -Wall flag, you can run the following
cmake -DCMAKE_CXX_COMPILER=clang -DCMAKE_CXX_FLAGS="-Wall" ..
The command line options are passed to the cmake via the -D flag.
You can chain multiple options together by repeating the -D flags.
Here is a table of commonly used command line options:
Flag |
Options |
Description |
|---|---|---|
|
|
set the build type |
|
|
set the installation directory |
|
[ |
set the paths to the dependencies, separated by |
Canoe provides a convenient command line option to configure your project. The
TASK flag allows you to invoke a specific configuration. For example, to configure
the test case for simulating a moist rising bubble (bryan) [1], you can run the following command:
cmake -DTASK=bryan ..
When the TASK flag is set, Canoe will look for <name>.cmake in the cmake directory
where <name> is the value of the TASK flag.
For example, the bryan.cmake file is located at cmake/bryan.cmake and it contains
the following commands:
set(NVAPOR 1)
set(NCLOUD 1)
set(NPHASE_LEGACY 2)
set(NETCDF ON)
set(PNETCDF ON)
set(MPI ON)
set(TASKLIST ImplicitHydroTasks)
set(RSOLVER lmars)
These settings are specific to the “bryan” test case. They tell that the “bryan” test case should activate 1 vapor, 1 cloud, and 2 legacy phases. Turn on the NetCDF and PNetCDF outputs. Turn on the MPI parallelization. Use vertical implicit method [2] for hydrodynamics and use the LMARS Riemann solver [3].
What if I want to use a package that is currently under heavy development? For example,
Canoe uses the Athena++
code for hydrodynamics. The Athena++ code frequently updates and new features are
added from time to time. Canoe wishes to use the latest version of Athena++ but
also wants to make custom changes to the code.
To achieve this, Canoe uses “patch” files to patch the most recent Athena++ code.
A “patch” file is a text file that contains a list of differences between two source codes.
You can create a “patch” file using the git diff command. The format of the patch
file looks like:
#ifdef MPI_PARALLEL
#include <mpi.h>
@@ -112,9 +116,21 @@ void Hydro::NewBlockTimeStep() {
Real speed1 = std::max(cspeed, (std::abs(wi[IVX]) + cs));
Real speed2 = std::max(cspeed, (std::abs(wi[IVY]) + cs));
Real speed3 = std::max(cspeed, (std::abs(wi[IVZ]) + cs));
- dt1(i) /= (speed1);
- dt2(i) /= (speed2);
- dt3(i) /= (speed3);
+ int implicit_flag = pmb->pimpl->phevi->GetImplicitFlag();
+ if ((implicit_flag & 1) && (pmb->block_size.nx2 > 1))
+ dt1(i) /= pmb->pmy_mesh->cfl_number * std::abs(wi[IVX]);
+ else
+ dt1(i) /= (speed1);
+
+ if (implicit_flag & 2)
+ dt2(i) /= pmb->pmy_mesh->cfl_number * std::abs(wi[IVY]);
+ else
+ dt2(i) /= (speed2);
+
+ if (implicit_flag & 4)
+ dt3(i) /= pmb->pmy_mesh->cfl_number * std::abs(wi[IVZ]);
+ else
+ dt3(i) /= (speed3);
}
} else { // FluidFormulation::background or disabled. Assume scalar advection:
dt1(i) /= (std::abs(wi[IVX]));
In the example above, the “patch” file tells that the Hydro::NewBlockTimeStep() function
has been modified. The @@ -112,9 +116,21 @@ line
tells that the modified code starts at line 112 and ends at line 116. The - sign indicates
that the code is removed and the + sign indicates that the code is added.
The change reflects that when the implicit_flag is set, the model time step is calculated
differently.
The “patch” file can be applied to the source code by using the git apply command after
fetching the latest Athena++ code.
set(patch_command
git apply ${CMAKE_CURRENT_SOURCE_DIR}/patches/21.new_blockdt.patch
...
)
FetchContent_Declare(
athenapp
GIT_REPOSITORY https://github.com/chengcli/athenapp/
PATCH_COMMAND ${patch_command}
UPDATE_DISCONNECTED TRUE)
When git apply is executed, git will look for the athena/src/hydro.cpp file and
try to apply the patch. If the patch is successfully applied, the git apply command will
return success. Otherwise, it will fail and the build process will halt. If the upstream
Athena++ code changes at places other than the NewBlockTimeStep function, the
git apply command will succeed and there will be no error. However, if the NewBlockTimeStep
function itself is changed, the git apply command will likely fail. In this case, you will
need to examine the new function and update the “patch” file accordingly.