Example: Wrapping a C model

In this example, we’ll use the babelizer to wrap the heat model from the bmi-example-c 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.

This is a somewhat long example. To break it up, here are the steps we’ll take:

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

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

  3. Create a babelizer input file describing the heat model

  4. Run the babelizer to generate Python bindings, then build the bindings

  5. Show the heat model running in Python through pymt

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

$ mkdir build && cd build

This directory is a starting point; we’ll make new directories under it as we proceed through the example. In the end, the directory structure under build should look like that in Figure 1.

Directory structure after completing example

Figure 1: Directory structure after completing example.

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.yml:

# A conda environment file for the babelizer example
name: wrap
channels:
  - conda-forge
dependencies:
  - python=3
  - make
  - cmake
  - pkg-config
  - c-compiler
  - bmi-c
  - babelizer

Download this file and create the new environment with:

$ conda env create --file=environment.yml

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

$ conda activate wrap

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

Build the heat model from source

Clone the bmi-example-c repository from GitHub:

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

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 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-c
$ 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.h ; echo $?

A return of zero indicates success.

Windows

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

> cd bmi-example-c
> 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.h exit 1

Create the babelizer input file

The babelizer input file provides information to the babelizer about the model to be wrapped. The input file is created with the babelize generate subcommand.

Return to our initial build directory and call babelize generate with:

$ cd ../..
$ babelize generate \
    --package=pymt_heatc \
    --summary="PyMT plugin for the C heat model" \
    --language=c \
    --library=bmiheatc \
    --header=bmi_heat.h \
    --entry-point=register_bmi_heat \
    --name=HeatModel \
    --requirement="" > babel_heatc.toml

In this call, the babelizer will also fill in default values; e.g., author name, author email, GitHub username, and license.

The resulting file, babel_heatc.toml, will look something like this:

[library]
[library.HeatModel]
language = "c"
library = "bmiheatc"
header = "bmi_heat.h"
entry_point = "register_bmi_heat"

[build]
undef_macros = []
define_macros = []
libraries = []
library_dirs = []
include_dirs = []
extra_compile_args = []

[package]
name = "pymt_heatc"
requirements = [""]

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

[ci]
python_version = ["3.9"]
os = ["linux", "mac", "windows"]

For more information on the entries and sections of the babelizer input file, see Input file.

Wrap the model with the babelizer

Generate Python bindings for the model with the babelize init subcommand:

$ babelize init babel_heatc.toml

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

$ ls -aF pymt_heatc
./                        MANIFEST.in               recipe/
../                       Makefile                  requirements-build.txt
.git/                     README.rst                requirements-library.txt
.github/                  babel.toml                requirements-testing.txt
.gitignore                docs/                     requirements.txt
CHANGES.rst               meta/                     setup.cfg
CREDITS.rst               pymt_heatc/               setup.py
LICENSE                   pyproject.toml

Before we can build the Python bindings, we must ensure that the dependencies required by the toolchain, as well as any required by the model, as specified in the babelizer input file (none in this case), are satisfied.

Change to the pymt_heatc directory and install dependencies into the conda environment:

$ cd pymt_heatc
$ conda install -c conda-forge \
    --file=requirements-build.txt \
    --file=requirements-testing.txt \
    --file=requirements-library.txt \
    --file=requirements.txt

Now build the Python bindings with:

$ make install

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

Successfully installed pymt-heatc

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

$ cd ..
$ mkdir test && cd test

Start a Python session and try the following commands:

>>> from pymt_heatc import HeatModel
>>> m = HeatModel()
>>> print(m.get_component_name())
The 2D Heat Equation

We’ve imported the heat model, written in C, into Python! Exit the Python session.

At this point, it’s a good idea to run the bmi-tester (GitHub repo) 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).

Run the bmi-tester:

$ bmi-test pymt_heatc:HeatModel --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.

Add metadata to 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 and 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_heatc. Under this directory, the babelizer created a directory for heat model metadata, meta/HeatModel. Change back to the pymt_heatc directory and view the current metadata:

$ cd ../pymt_heatc
$ ls meta/HeatModel/
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_heatc/meta/HeatModel directory alongside api.yaml.

Next, install pymt:

$ conda install -c conda-forge pymt

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

>>> from pymt.models import HeatModel
>>> m = HeatModel()
>>> print(m.name)
The 2D Heat Equation

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

"""Run the heat model in pymt."""
import numpy as np
from pymt.models import HeatModel


# Instantiate the component and get its name.
m = HeatModel()
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(" - {}".format(var))
print("Number of output vars:", len(m.output_var_names))
for var in m.output_var_names:
    print(" - {}".format(var))

# Get variable info.
var_name = m.output_var_names[0]
print("Variable {}".format(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("Get values of {}...".format(var_name))
val = m.var[var_name].data
print(" - values at time {}:".format(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_heatc_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.