Example: Wrapping a C++ model

In this example, we’ll use the babelizer to wrap the heat model from the bmi-example-cxx repository, allowing it to be run in Python. The model and its BMI are written in C++. To simplify package management in the example, we’ll use conda. We’ll also use git to obtain the model source code.

Here are the steps we’ll take to complete this example:

  1. Create a conda environment that includes software to compile the model and wrap it with the babelizer

  2. Clone the bmi-example-cxx repository from GitHub and build the heat model from source

  3. Create a babelizer configuration file describing the heat model

  4. Run the babelizer to generate a Python package, then build and install the package

  5. Show the heat model running in Python through pymt

Before we begin, create a directory to hold our work:

mkdir example-cxx && cd example-cxx

This directory is a starting point; we’ll add files and directories to it as we proceed through the example. The final directory structure of example-c should look similar to that below.

example-cxx/
├── babel_heatcxx.toml
├── bmi-example-cxx/
├── environment-cxx.yml
├── pymt_heatcxx/
└── test/

Set up a conda environment

Start by setting up a conda environment that includes the babelizer, as well as a toolchain to build and install the model. The necessary packages are listed in the conda environment file environment-cxx.yml:

# A conda environment file for the babelizer C++ example
name: wrap-cxx
channels:
  - conda-forge
dependencies:
  - python=3
  - make
  - cmake
  - pkg-config
  - cxx-compiler
  - bmi-cxx
  - babelizer
  - bmi-tester
  - pymt>=1.3

Download this file and place it in the example-cxx directory you created above. Create the new environment with:

conda env create --file environment-cxx.yml

When this command completes, activate the environment (on Linux and macOS, you may have to use source instead of conda):

conda activate wrap-cxx

The wrap-cxx environment now contains all the dependencies needed to build, install, and wrap the heat model.

Build the heat model from source

From the example-cxx directory, clone the bmi-example-cxx repository from GitHub:

git clone https://github.com/csdms/bmi-example-cxx

There are general instructions in the repository for building and installing this package on Linux, macOS, and Windows. We’ll augment those instructions with the note that we’re installing into the wrap-cxx conda environment, so the CONDA_PREFIX environment variable should be used to specify the install path.

Linux and macOS

On Linux and macOS, use these commands to build and install the heat model:

cd bmi-example-cxx
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX
make install

Verify the install by testing for the existence of the header of the library containing the compiled heat model:

test -f $CONDA_PREFIX/include/bmi_heat.hxx ; echo $?

A return of zero indicates success.

Windows

Building on Windows requires Microsoft Visual Studio 2019 or Microsoft Build Tools for Visual Studio 2019. To build and install the heat model, the following commands must be run in a Developer Command Prompt:

cd bmi-example-cxx
mkdir build && cd build
cmake .. ^
  -G "NMake Makefiles" ^
  -DCMAKE_INSTALL_PREFIX=%CONDA_PREFIX% ^
  -DCMAKE_BUILD_TYPE=Release
cmake --build . --target install --config Release

Verify the install by testing for the existence of the header of the library containing the compiled heat model:

if not exist %LIBRARY_INC%\\bmi_heat.hxx exit 1

Create a babelizer configuration file

A babelizer configuration file provides information to the babelizer about the model to be wrapped.

Typically, we would use the babelize sample-config command to create a sample configuration file that could then be edited. However, to simplify this example, we have provided a completed configuration file for the heat model. Download the file babel_heatcxx.toml and copy it to the example-cxx directory.

The configuration file looks like this:

# See https://babelizer.readthedocs.io/ for more information

# Describe the library being wrapped.
[library.HeatCxx]
language = "c++"
library = "bmiheatcxx"
header = "bmi_heat.hxx"
entry_point = "BmiHeat"

# Describe compiler options need to build the library being wrapped.
[build]
undef_macros = []
define_macros = []
libraries = []
library_dirs = []
include_dirs = []
extra_compile_args = []

# Describe the newly wrapped package.
[package]
name = "pymt_heatcxx"
requirements = []

[info]
github_username = "pymt-lab"
package_author = "csdms"
package_author_email = "csdms@colorado.edu"
package_license = "MIT License"
summary = "PyMT component for the C++ heat model"

[ci]
python_version = [
    "3.10",
    "3.11",
    "3.12",
]
os = [
    "linux",
    "mac",
    "windows",
]

For more information on the entries and sections of the babelizer configuration file, see Configuration file.

Wrap the model with the babelizer

From the example-cxx directory, generate a Python package for the model with the babelize init command:

babelize init babel_heatcxx.toml

The results are placed in a new directory, pymt_heatcxx, under the current directory.

Build and install the wrapped model

Change to the pymt_heatcxx directory, then build and install the Python package with pip:

cd pymt_heatcxx
pip install ."[dev]"

The pip install command sets off a long list of messages, at the end of which you’ll hopefully see:

Successfully installed pymt-heatcxx

Pause a moment to see what we’ve done. Change back to the initial example-cxx directory, make a new test directory, and change to it:

cd ..
mkdir test && cd test

Start a Python session (e.g., run python) and try the following commands:

from pymt_heatcxx import HeatCxx
m = HeatCxx()
m.get_component_name()

You should see:

The 2D Heat Equation

We’ve imported the heat model, written in C++, into Python! Exit the Python session (e.g. type exit()).

Test the BMI

At this point, it’s a good idea to run the bmi-tester (documentation) over the model. The bmi-tester exercises each BMI method exposed through Python, ensuring it works correctly. However, before running the bmi-tester, one last piece of information is needed. Like all models equipped with a BMI, heat uses a configuration file to specify initial parameter values. Create a configuration file for heat at the command line with:

echo "1.5, 8.0, 6, 5" > config.txt

or download the file config.txt, making sure to place it in the test directory.

From the test directory, run the bmi-tester:

bmi-test pymt_heatcxx:HeatCxx --config-file=config.txt --root-dir=. -vvv

This command sets off a long list of messages, ending with

🎉 All tests passed!

if everything has been built correctly.

Make a pymt component

The final step in wrapping the heat model is to add metadata used by the Python Modeling Tool, pymt. CSDMS develops a set of standards, the CSDMS Model Metadata, that provides a detailed and formalized description of a model. The metadata allow heat to be run and be coupled with other models that expose a BMI and have been similarly wrapped with the babelizer.

Recall the babelizer outputs the wrapped heat model to the directory pymt_heatcxx. Under this directory, the babelizer created a directory for heat model metadata, meta/HeatCxx. Change back to the pymt_heatcxx directory and view the current metadata:

cd ../pymt_heatcxx
ls meta/HeatCxx/

which gives:

api.yaml

The file api.yaml is automatically generated by the babelizer. To complete the description of the heat model, other metadata files are needed, including:

Descriptions of these files and their roles are given in the CSDMS Model Metadata repository. Download each of the files using the links in the list above and place them in the pymt_heatcxx/meta/HeatCxx directory alongside api.yaml. The structure of the meta directory should look like:

meta/
└── HeatCxx/
    ├── api.yaml
    ├── heat.txt
    ├── info.yaml
    ├── parameters.yaml
    └── run.yaml

Run the babelized model in pymt

Start a Python session and show that the heat model can be called through pymt:

from pymt.models import HeatCxx
m = HeatCxx()
m.name

You should see:

The 2D Heat Equation

A longer example, pymt_heatcxx_ex.py, is included in the documentation. For easy viewing, it’s reproduced here verbatim:

"""Run the C++ heat model in pymt."""

from pymt.models import HeatCxx

# Instantiate the component and get its name.
m = HeatCxx()
print(m.name)

# Call setup, then initialize the model.
args = m.setup(".")
m.initialize(*args)

# List the model's exchange items.
print("Number of input vars:", len(m.input_var_names))
for var in m.input_var_names:
    print(f" - {var}")
print("Number of output vars:", len(m.output_var_names))
for var in m.output_var_names:
    print(f" - {var}")

# Get variable info.
var_name = m.output_var_names[0]
print(f"Variable {var_name}")
print(" - variable type:", m.var_type(var_name))
print(" - units:", m.var_units(var_name))
print(" - itemsize:", m.var_itemsize(var_name))
print(" - nbytes:", m.var_nbytes(var_name))
print(" - location:", m.var_location(var_name))

# Get grid info for variable.
grid_id = m.var_grid(var_name)
print(" - grid id:", grid_id)
print(" - grid type:", m.grid_type(grid_id))
print(" - rank:", m.grid_ndim(grid_id))
print(" - size:", m.grid_node_count(grid_id))
print(" - shape:", m.grid_shape(grid_id))

# Get time information from the model.
print("Start time:", m.start_time)
print("End time:", m.end_time)
print("Current time:", m.time)
print("Time step:", m.time_step)
print("Time units:", m.time_units)

# Get the initial values of the variable.
print(f"Get values of {var_name}...")
val = m.var[var_name].data
print(f" - values at time {m.time}:")
print(val)

# Advance the model by one time step.
m.update()
print("Update: current time:", m.time)

# Advance the model until a later time.
m.update_until(5.0)
print("Update: current time:", m.time)

# Finalize the model.
m.finalize()
print("Done.")

Download this Python script, then run it with:

python pymt_heatcxx_ex.py

Summary

Using the babelizer, we wrapped the heat model, which is written in C++. It can now be called as a pymt component in Python.

The steps for wrapping a model with the babelizer outlined in this example can also be applied to models written in C and Fortran.