Example: Wrapping a Fortran model

In this example, we’ll use the babelizer to wrap the heat model from the bmi-example-fortran repository, allowing it to be run in Python. The model and its BMI are written in Fortran. 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-fortran 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 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-f && cd example-f

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-f/
├── babel_heatf.toml
├── bmi-example-fortran/
├── environment-fortran.yml
├── pymt_heatf/
└── 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-fortran.yml:

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

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

conda env create --file environment-fortran.yml

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

conda activate wrap-f

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

Build the heat model from source

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

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

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.

Note that if you build the model with the Fortran Package Manager (fpm), you will end up with a static library (.a on Unix, .lib on Windows) instead of the dynamic library (.so on Unix, .dll on Windows) that the CMake build creates. We are aware of issues linking to the compiler runtime libraries from this static library, and for this reason we recommend using the CMake build routine, as detailed below.

Linux and macOS

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

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

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

test -f $CONDA_PREFIX/include/bmiheatf.mod ; echo $?

A return of zero indicates success.

Windows

Building the heat model on Windows requires either:

  • A Unix-like system, such as Cygwin or Windows Subsystem for Linux, in which case you can follow the above Linux and macOS instructions.

  • Microsoft Visual Studio 2017 or later, or Microsoft Build Tools for Visual Studio 2017 or later, in which case the following instructions should be followed.

Open a Developer Command Prompt and run:

cd bmi-example-fortran
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 module file of the library containing the compiled heat model:

if exist %CONDA_PREFIX%\include\bmiheatf.mod echo File exists

Note that on Windows systems, the conda package we specified called fortran-compiler installs a fairly old version of the Flang compiler, and there are few options for more modern compilers available via conda (as opposed to for Unix, where modern versions of GFortran are available). This is fine for the example heat model, but code bases leveraging newer features of Fortran may need a more modern compiler. In this case, it might be necessary to install a Fortran compiler separately to conda, for example using the binaries provided by equation.com. The BMI bindings (installed here via conda) should be compiled with the same compiler as the model that uses them, to avoid incompatibility issues, and so if you choose a different compiler than provided by fortran-compiler, you will likely have to compile the BMI bindings with this compiler as well.

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, which could then be edited. However, to simplify this example, we have provided a completed configuration file for the heat model. Download the file babel_heatf.toml and copy it to the example-f directory.

The configuration file looks like this:

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

# Describe the library being wrapped.
[library.HeatF]
language = "fortran"
library = "bmiheatf"
header = ""
entry_point = "bmi_heat"

# 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_heatf"
requirements = []

[info]
github_username = "pymt-lab"
package_author = "csdms"
package_author_email = "csdms@colorado.edu"
package_license = "MIT License"
summary = "PyMT component for the Fortran 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 Input file.

Wrap the model with the babelizer

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

babelize init babel_heatf.toml

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

Build and install the wrapped model

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

cd pymt_heatf
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_heatf

Pause a moment to see what we’ve done. Change back to the initial example-f 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_heatf import HeatF
m = HeatF()
m.get_component_name()

You should see:

The 2D Heat Equation

We’ve imported the heat model, written in Fortran, 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_heatf:HeatF --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_heatf. Under this directory, the babelizer created a directory for heat model metadata, meta/HeatF. Change back to the pymt_heatf directory and view the current metadata:

cd ../pymt_heatf
ls meta/HeatModel/

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_heatf/meta/HeatF directory alongside api.yaml. The structure of the meta directory should look like:

meta/
└── HeatF/
    ├── 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 HeatF
m = HeatF()
m.name

You should see:

The 2D Heat Equation

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

"""Run the Fortran heat model in pymt."""

from pymt.models import HeatF

# Instantiate the component and get its name.
m = HeatF()
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, make sure we’re still in the test directory we just created, then run it with:

python pymt_heatf_ex.py

Summary

Using the babelizer, we wrapped the heat model, which is written in Fortran. 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 (see the example) and C++.