BMI best practices¶
BMI is a simple concept—it’s just a set of functions with rules for the function names, arguments, and returns. However, when implementing a BMI, the devil is often in the details. In no particular order, here are some tips to help when writing a BMI for a model.
All functions in the BMI must be implemented. For example, even if a model operates on a uniform rectilinear grid, a get_grid_x function has to be written. This function can be empty and simply return the
BMI_FAILURE
status code or raise aNotImplemented
exception, depending on the language.The BMI functions listed in the documentation are the minimum required. Optional functions that act as helpers can be added to a model’s BMI. For example, an
update_frac
function that updates a model’s state by a fractional time step is a common addition to a BMI.Implementation details are left up to the model developer. All that the BMI specifies are the names of the functions, their arguments, and their return values.
Standard Names are not required for naming a model’s exchange items. However, the use of standardized names makes it easier for a framework (or a human) to match input and output variables between models.
Don’t change the variable names for exchange items you currently use within your model to Standard Names. Instead, find a matching Standard Name for each variable and then write your BMI functions to accept the Standard Names and map them to your model’s internal names.
Constructs and features that are natural for the language should be used when implementing a BMI. BMI strives to be developer-friendly.
BMI functions always use flattened, one-dimensional arrays. This avoids any issues stemming from row/column-major indexing when coupling models written in different languages. It’s the developer’s responsibility to ensure that array information is flattened/redimensionalized in the correct order.
Recall that models can have multiple grids. This can be particularly useful for defining exchange items that don’t vary over the model domain; e.g., a diffusivity – just define the variable on a separate scalar grid.
Avoid using global variables, if possible. This isn’t strictly a BMI requirement, but if a model only uses local variables, its BMI will be self-contained. This may allow multiple instances of the model to be run simultaneously, possibly permitting the model to be coupled with itself.
Boundary conditions, including boundary conditions that change with the model state, can be represented with exchange items.
Configuration files are typically text (e.g., YAML is preferred by CSDMS), but this isn’t a strict requirement; binary data models such as HDF5 and netCDF also work well for storing configuration data.
Before fitting a model with a BMI, the model code may have to be refactored into modular initialize-run-finalize (IRF) steps in order to interact with the BMI functions. This is often the most difficult part of adding a BMI, but the modularization tends to improve the quality of the code.
Avoid allocating memory within a BMI function. Memory allocation is typically the responsibility of the model. This helps keep the BMI middleware layer thin.
Be sure to check out the examples: C, C++, Fortran, Java, Python. Although they wrap a very simple model, they give useful insights into how a BMI can be implemented in each language.
Return codes (C and Fortran) and exceptions (C++, Java, and Python) can help with debugging a BMI, and can provide useful information to a user.