CSDMS Basic Model Interface

MNiShed includes a CSDMS Basic Model Interface (BMI) wrapper that enables it to be driven by any BMI-compliant coupling framework and to exchange variables with other BMI models.

Overview

The BMI wrapper exposes MNiShed as a scalar (lumped) model with a single grid of rank 0 and size 1. All variables are scalars representing catchment-integrated quantities.

The wrapper supports two usage modes:

File-driven (standard workflow)

The YAML configuration file points to a CSV containing all forcing data. The framework calls update() repeatedly to step through the record. No set_value() calls are needed.

Online coupled

An upstream model provides forcing each timestep via set_value() before calling update(). The CSV file still provides the initial time series (used for spin-up and as a default if a variable is not overridden).

Installation

The BMI wrapper requires bmipy. Install it with the bmi optional-dependency group:

pip install 'MNiShed[bmi]'

Usage

File-driven

from mnished import BmiMNiShed

bmi = BmiMNiShed()
bmi.initialize("config.yml")

while bmi.get_current_time() < bmi.get_end_time():
    bmi.update()

bmi.finalize()

Use update_until() to advance to a specific time without writing the loop yourself:

bmi.update_until(365.0)   # advance one year

Online coupled

import numpy as np
from mnished import BmiMNiShed

bmi = BmiMNiShed()
bmi.initialize("config.yml")        # CSV values loaded for spin-up

while bmi.get_current_time() < bmi.get_end_time():
    # Override forcing from an upstream model
    bmi.set_value(
        "atmosphere_water__liquid_equivalent_precipitation_rate",
        np.array([p_from_upstream])
    )
    bmi.set_value("atmosphere_bottom_air__temperature",
                  np.array([t_from_upstream]))
    bmi.update()

    # Pass volumetric discharge [m³ s⁻¹] to a downstream channel or
    # sediment model.  Specific discharge [mm d⁻¹] is also available
    # via "land_surface_water__runoff_volume_flux".
    q_m3s = np.empty(1, dtype=np.float64)
    bmi.get_value("channel_exit_water_x-section__volume_flow_rate", q_m3s)
    downstream_model.set_value(
        "channel_exit_water_x-section__volume_flow_rate", q_m3s)

bmi.finalize()

Converting specific discharge to volumetric flow

land_surface_water__runoff_volume_flux is area-normalised specific discharge in mm d⁻¹. To convert to volumetric discharge Q [m³ s⁻¹]:

area_km2 = bmi._model.drainage_basin_area__km2
Q_m3s = q_mm_d * 1e-3 * area_km2 * 1e6 / 86400  # mm→m, km²→m², day→s

Exposed Variables

All variables are scalar (grid rank 0, size 1, location node). Types are float64; time unit is d (days).

Input variables

These variables are read from the CSV by default. In online-coupled mode, call set_value() before each update() to override them.

Temperature and ET inputs are declared even when those columns are absent from the CSV. Calling set_value() for an absent column raises KeyError.

CSDMS Standard Name

Units

MNiShed column

atmosphere_water__liquid_equivalent_precipitation_rate

mm d⁻¹

Precipitation [mm/day]

atmosphere_bottom_air__temperature

°C

Mean Temperature [C]

atmosphere_bottom_air__time_min_of_temperature

°C

Minimum Temperature [C]

atmosphere_bottom_air__time_max_of_temperature

°C

Maximum Temperature [C]

land_surface_water__uncorrected_evapotranspiration_volume_flux

mm d⁻¹

Evapotranspiration [mm/day] (ET forcing before water-balance correction; see note below)

Note

Uncorrected vs. corrected ET. The input …__uncorrected_evapotranspiration_volume_flux is the ET forcing as supplied (Thornthwaite–Chang, or a user series in datafile mode), before water-balance correction. The output …__evapotranspiration_volume_flux (below) is that forcing after a bulk multiplier — one constant, or one per water year — closes P − Q − ET over the record: the post-correction ET target, which equals the ET actually removed except under et_reservoir_draw / et_water_stress, where storage availability reduces it further. The multiplier is a coarse stand-in for moisture limitation, not a physical potential-to-actual conversion; input and output coincide only when enforce_water_balance='none'. uncorrected is an MNiShed-specific name (CSDMS has no pre-/post-correction modifier), shaped like a standard name as are the tile-drain, multipath-drain, and frozen-ground quantities.

Output variables

These variables are updated by each call to update() and retrieved via get_value().

CSDMS Standard Name

Units

Source

land_surface_water__runoff_volume_flux

mm d⁻¹

Modelled specific discharge (area-normalised)

channel_exit_water_x-section__volume_flow_rate

m³ s⁻¹

Volumetric discharge (specific discharge × catchment area)

snowpack__liquid_equivalent_depth

mm

Snowpack SWE; 0.0 if no snowpack

subsurface_water__depth

mm

Total subsurface storage (all reservoirs)

land_surface_water__evapotranspiration_volume_flux

mm d⁻¹

Model evapotranspiration flux (after water-balance scaling)

land_surface_water__direct_runoff_volume_flux

mm d⁻¹

Hortonian-style fast bypass (direct_runoff module)

land_surface_water__baseflow_volume_flux

mm d⁻¹

Constant regional baseflow (baseflow_Q); see note below

land_surface_water__tile_drain_volume_flux

mm d⁻¹

Tile-drain sub-reservoir discharge (tile_fractions)

land_surface_water__multipath_drain_volume_flux

mm d⁻¹

Threshold-activated parallel drain (multipath_thresholds__mm)

land_surface__frozen_ground_index

degC d

Frozen-ground index (FGI) state

subsurface_water_reservoir_0__depth

mm

Reservoir 0 storage (shallowest)

subsurface_water_reservoir_1__depthsubsurface_water_reservoir_9__depth

mm

Reservoirs 1–9 storage (deeper); nan for indices ≥ number of configured reservoirs

Note

Flux partition. The four *_volume_flux components above (direct runoff, baseflow, tile drain, multipath drain) decompose the fast-flow contributions to discharge and are recorded by each update(). They are diagnostic: the primary cascade discharge is reported by land_surface_water__runoff_volume_flux. baseflow is the constant regional-import term (baseflow_Q); it is not part of the reservoir cascade, so the streaming BMI keeps it separate and does not fold it into land_surface_water__runoff_volume_flux. A coupled model that wants total discharge including regional import should add the two.

This differs from run_and_score(), whose scored discharge does include baseflow_Q — and Nash flow routing (routing_K) — applied as an output-layer post-process on the full series. The per-step BMI applies neither (routing is an inherently batch convolution), so a configuration calibrated with routing and/or baseflow will not reproduce its scored hydrograph through the BMI unless the coupler reapplies those terms.

The CSDMS Standard Names for evapotranspiration, direct runoff, and baseflow extend the registered land_surface_water__…_volume_flux family; tile_drain, multipath_drain, and frozen_ground_index are MNiShed-specific quantities named to follow the same convention.

Note

MNiShed itself places no limit on the number of reservoirs — you can add as many as you like to the reservoirs: block in the YAML configuration. The BMI wrapper caps exposed reservoir outputs at 10 (indices 0–9) because the BMI specification requires variable names to be fixed at import time. If you configure more than 10 reservoirs, initialize() will raise a ValueError with instructions pointing to the four constants in mnished/bmi.py that need updating: _OUTPUT_VAR_NAMES, _VAR_UNITS, _RESERVOIR_DEPTH_NAMES, and _BMI_MAX_RESERVOIRS. The total subsurface storage across all reservoirs is always available via subsurface_water__depth regardless of reservoir count.

Grid and Time

Grid ID

0 (all variables share one scalar grid)

Grid rank

0 (scalar — no spatial dimensions)

Grid size

1

Grid type

"scalar"

Time unit

"d" (days)

Timestep

1.0 day (MNiShed is a daily model)

Start time

0.0

End time

Number of days in the input record

Shape, spacing, origin, coordinate, and connectivity methods raise NotImplementedError — these are not defined for rank-0 scalar grids.

Caveats

get_value_ptr is a snapshot, not a live pointer. MNiShed stores scalar state as Python floats rather than numpy arrays. get_value_ptr() therefore returns a fresh length-1 array each call; the array does not update automatically when the model advances. Call get_value() after each update() to retrieve current values.

Spin-up is internal. Spin-up cycles specified in the YAML config run inside initialize(); the BMI time counter starts at 0.0 after spin-up completes.

finalize() does not plot. Calling finalize() discards the internal model object but does not call finalize() on it, which would trigger an NSE print and a plot pop-up unsuitable for headless coupling runs.

Calling update() past the end of the record raises an error. The file-driven loop while bmi.get_current_time() < bmi.get_end_time() terminates correctly. Calling update() after all rows have been consumed will raise a KeyError from the internal pandas DataFrame. Guard against this in custom loops by checking get_current_time() < get_end_time() before each call.

API Reference

class mnished.BmiMNiShed[source]

CSDMS Basic Model Interface wrapper for MNiShed.

Wraps a Buckets instance so that MNiShed can participate in a CSDMS-compliant coupling framework.

Two usage modes are supported:

File-driven (standard MNiShed workflow) — the YAML config points to a CSV containing all forcing data; the framework steps through the record by calling update() repeatedly:

from mnished import BmiMNiShed
import numpy as np

bmi = BmiMNiShed()
bmi.initialize("config.yml")
while bmi.get_current_time() < bmi.get_end_time():
    bmi.update()
bmi.finalize()

Online coupled — an upstream model provides forcing each step via set_value() before calling update():

bmi.initialize("config.yml")
bmi.set_value(
    "atmosphere_water__liquid_equivalent_precipitation_rate",
    np.array([5.2])
)
bmi.set_value("atmosphere_bottom_air__temperature", np.array([3.1]))
bmi.update()
q = np.empty(1, dtype=np.float64)
bmi.get_value("land_surface_water__runoff_volume_flux", q)
print(f"Discharge: {q[0]:.3f} mm d-1")

Notes

Specific discharge: land_surface_water__runoff_volume_flux is area-normalised specific discharge in mm d⁻¹, not volumetric flux. To convert to volumetric discharge Q [m³ s⁻¹]:

Q_m3s = q_mm_d * area_km2 * 1e3 / 86400

where area_km2 = bmi._model.drainage_basin_area__km2.

Difference from run_and_score discharge: the streaming BMI reports the raw per-step reservoir-cascade discharge. It does not apply the two output-layer post-processing steps that mnished.calibration.run_and_score() performs on the full series: Nash-cascade flow routing (routing_K), which is an inherently batch convolution unavailable to a per-step interface, and the constant regional baseflow term (baseflow_Q). Baseflow is instead exposed as its own output (land_surface_water__baseflow_volume_flux) so a coupler can add it explicitly. A configuration calibrated with routing and/or baseflow will therefore not reproduce its scored hydrograph through the BMI unless the coupler reapplies those terms.

get_value_ptr: MNiShed stores scalar state as Python floats, not numpy arrays. get_value_ptr() therefore returns a fresh length-1 array rather than a live pointer into model memory. Values in the returned array do not update when the model advances; call get_value() after each update() call to retrieve current values.

Per-reservoir depths: subsurface_water_reservoir_0__depth through subsurface_water_reservoir_9__depth correspond to reservoirs 0–9 (shallowest to deepest) in the configuration. Depths for reservoir indices that do not exist in the current configuration are returned as np.nan.

Optional input variables: the five input Standard Names are always declared. Temperature and ET inputs will raise KeyError from set_value() if the corresponding column is absent from the loaded time series.

Evapotranspiration, input vs. output: the input …__uncorrected_evapotranspiration_volume_flux is the ET forcing as supplied (Thornthwaite–Chang, or a user series in datafile mode), before water-balance correction. The output …__evapotranspiration_volume_flux is that forcing after a bulk multiplier — one constant, or one per water year — closes P − Q − ET over the record: the post-correction ET target, which equals the ET actually removed except under et_reservoir_draw / et_water_stress, where storage availability reduces it further. The multiplier is a coarse stand-in for moisture limitation, not a physical potential-to-actual conversion; input and output coincide only when enforce_water_balance='none'. uncorrected is an MNiShed-specific name (CSDMS has no pre-/post-correction modifier).

initialize(config_file: str) None[source]

Load a MNiShed YAML configuration file and prepare the model.

Reads the configuration, loads the input CSV time series, builds the reservoir stack, and runs spin-up cycles. After this call, update() steps through the record one day at a time.

Parameters:

config_file (str) – Path to a MNiShed YAML configuration file.

update() None[source]

Advance the model by one day.

If set_value() was called for any input variable before this step, those values override the CSV values for the current row.

update_until(time: float) None[source]

Advance the model until get_current_time() >= time.

Parameters:

time (float) – Target time [days since start of record].

finalize() None[source]

Release internal resources.

Discards the Buckets instance. Does not call finalize() on the inner model (which would trigger an NSE print and a plot pop-up).

get_component_name() str[source]

Name of the component.

Returns:

The name of the component.

Return type:

str

get_input_item_count() int[source]

Count of a model’s input variables.

Returns:

The number of input variables.

Return type:

int

get_output_item_count() int[source]

Count of a model’s output variables.

Returns:

The number of output variables.

Return type:

int

get_input_var_names()[source]

List of a model’s input variables.

Input variable names must be CSDMS Standard Names, also known as long variable names.

Returns:

The input variables for the model.

Return type:

list of str

Notes

Standard Names enable the CSDMS framework to determine whether an input variable in one model is equivalent to, or compatible with, an output variable in another model. This allows the framework to automatically connect components.

Standard Names do not have to be used within the model.

get_output_var_names()[source]

List of a model’s output variables.

Output variable names must be CSDMS Standard Names, also known as long variable names.

Returns:

The output variables for the model.

Return type:

list of str

get_var_grid(name: str) int[source]

Get grid identifier for the given variable.

Parameters:

name (str) – An input or output variable name, a CSDMS Standard Name.

Returns:

The grid identifier.

Return type:

int

get_var_type(name: str) str[source]

Get data type of the given variable.

Parameters:

name (str) – An input or output variable name, a CSDMS Standard Name.

Returns:

The Python variable type; e.g., str, int, float.

Return type:

str

get_var_units(name: str) str[source]

Get units of the given variable.

Standard unit names, in lower case, should be used, such as meters or seconds. Standard abbreviations, like m for meters, are also supported. For variables with compound units, each unit name is separated by a single space, with exponents other than 1 placed immediately after the name, as in m s-1 for velocity, W m-2 for an energy flux, or km2 for an area.

Parameters:

name (str) – An input or output variable name, a CSDMS Standard Name.

Returns:

The variable units.

Return type:

str

Notes

CSDMS uses the UDUNITS standard from Unidata.

get_var_itemsize(name: str) int[source]

Get memory use for each array element in bytes.

Parameters:

name (str) – An input or output variable name, a CSDMS Standard Name.

Returns:

Item size in bytes.

Return type:

int

get_var_nbytes(name: str) int[source]

Get size, in bytes, of the given variable.

Parameters:

name (str) – An input or output variable name, a CSDMS Standard Name.

Returns:

The size of the variable, counted in bytes.

Return type:

int

get_var_location(name: str) str[source]

Get the grid element type that the a given variable is defined on.

The grid topology can be composed of nodes, edges, and faces.

node

A point that has a coordinate pair or triplet: the most basic element of the topology.

edge

A line or curve bounded by two nodes.

face

A plane or surface enclosed by a set of edges. In a 2D horizontal application one may consider the word “polygon”, but in the hierarchy of elements the word “face” is most common.

Parameters:

name (str) – An input or output variable name, a CSDMS Standard Name.

Returns:

The grid location on which the variable is defined. Must be one of “node”, “edge”, or “face”.

Return type:

str

Notes

CSDMS uses the ugrid conventions to define unstructured grids.

get_start_time() float[source]

Start time of the model.

Model times should be of type float.

Returns:

The model start time.

Return type:

float

get_end_time() float[source]

End time of the model.

Returns:

The maximum model time.

Return type:

float

get_current_time() float[source]

Return the current time of the model.

Returns:

The current model time.

Return type:

float

get_time_step() float[source]

Return the current time step of the model.

The model time step should be of type float.

Returns:

The time step used in model.

Return type:

float

get_time_units() str[source]

Time units of the model.

Returns:

The model time unit; e.g., days or s.

Return type:

str

Notes

CSDMS uses the UDUNITS standard from Unidata.

get_grid_rank(grid_id: int) int[source]

Get number of dimensions of the computational grid.

Parameters:

grid (int) – A grid identifier.

Returns:

Rank of the grid.

Return type:

int

get_grid_size(grid_id: int) int[source]

Get the total number of elements in the computational grid.

Parameters:

grid (int) – A grid identifier.

Returns:

Size of the grid.

Return type:

int

get_grid_type(grid_id: int) str[source]

Get the grid type as a string.

Parameters:

grid (int) – A grid identifier.

Returns:

Type of grid as a string.

Return type:

str

get_value(name: str, dest: ndarray) ndarray[source]

Copy the current scalar value of name into dest and return it.

For input variables, returns the value at the pending row (the value that will be consumed by the next update() call). For output variables, returns the value written by the most recent update() call.

Parameters:
  • name (str) – CSDMS Standard Name.

  • dest (numpy.ndarray) – Pre-allocated length-1 array of dtype float64.

Returns:

dest, filled in place.

Return type:

numpy.ndarray

get_value_ptr(name: str) ndarray[source]

Return a length-1 float64 array containing the current value.

Returns a snapshot, not a live pointer — see class docstring.

get_value_at_indices(name: str, dest: ndarray, inds: ndarray) ndarray[source]

Get value at specific indices (scalar: only index 0 is valid).

set_value(name: str, src: ndarray) None[source]

Override an input variable for the current (next) timestep.

Writes src[0] into the hydrodata DataFrame at the pending row (_timestep_i), replacing the value read from the CSV. The written value is consumed by the next update() call. This is the online-coupling path; in file-driven mode this method need not be called.

Parameters:
  • name (str) – CSDMS Standard Name of an input variable.

  • src (numpy.ndarray) – Length-1 float64 array containing the new value.

Raises:

KeyError – If name is not a recognised input variable, or if the corresponding DataFrame column does not exist in the loaded time series.

set_value_at_indices(name: str, inds: ndarray, src: ndarray) None[source]

Set value at specific indices (scalar: only index 0 is valid).