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:
- Create a conda environment that includes software to compile the model and wrap it with the babelizer
- Clone the bmi-example-c repository from GitHub and build the heat model from source
- Create a babelizer input file describing the heat model
- Run the babelizer to generate Python bindings, then build the bindings
- Show the heat model running in Python through pymt
Before we begin, create a directory to hold our work:
$ mkdir build && cd build
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
$ 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
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 ~/build
$ babelize generate \
--package=pymt_heatc \
--summary="PyMT plugin for 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 for 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 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 ~/build
$ 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!
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.
Download the file config.txt
for use here.
Run the bmi-tester with:
$ 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 ~/build/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.