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. Noset_value()calls are needed.- Online coupled
An upstream model provides forcing each timestep via
set_value()before callingupdate(). 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 |
|---|---|---|
|
mm d⁻¹ |
|
|
°C |
|
|
°C |
|
|
°C |
|
|
mm d⁻¹ |
|
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 |
|---|---|---|
|
mm d⁻¹ |
Modelled specific discharge (area-normalised) |
|
m³ s⁻¹ |
Volumetric discharge (specific discharge × catchment area) |
|
mm |
Snowpack SWE; 0.0 if no snowpack |
|
mm |
Total subsurface storage (all reservoirs) |
|
mm d⁻¹ |
Model evapotranspiration flux (after water-balance scaling) |
|
mm d⁻¹ |
Hortonian-style fast bypass ( |
|
mm d⁻¹ |
Constant regional baseflow ( |
|
mm d⁻¹ |
Tile-drain sub-reservoir discharge ( |
|
mm d⁻¹ |
Threshold-activated parallel drain ( |
|
degC d |
Frozen-ground index (FGI) state |
|
mm |
Reservoir 0 storage (shallowest) |
|
mm |
Reservoirs 1–9 storage (deeper); |
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 |
|
Time unit |
|
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
Bucketsinstance 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 callingupdate():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_fluxis 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; callget_value()after eachupdate()call to retrieve current values.Per-reservoir depths:
subsurface_water_reservoir_0__depththroughsubsurface_water_reservoir_9__depthcorrespond to reservoirs 0–9 (shallowest to deepest) in the configuration. Depths for reservoir indices that do not exist in the current configuration are returned asnp.nan.Optional input variables: the five input Standard Names are always declared. Temperature and ET inputs will raise
KeyErrorfromset_value()if the corresponding column is absent from the loaded time series.Evapotranspiration, input vs. output: the input
…__uncorrected_evapotranspiration_volume_fluxis the ET forcing as supplied (Thornthwaite–Chang, or a user series indatafilemode), before water-balance correction. The output…__evapotranspiration_volume_fluxis 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 underet_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 whenenforce_water_balance='none'.uncorrectedis 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
Bucketsinstance. Does not callfinalize()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:
- get_input_item_count() int[source]
Count of a model’s input variables.
- Returns:
The number of input variables.
- Return type:
- get_output_item_count() int[source]
Count of a model’s output variables.
- Returns:
The number of output variables.
- Return type:
- 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.
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.
- get_var_units(name: str) str[source]
Get units of the given variable.
Standard unit names, in lower case, should be used, such as
metersorseconds. Standard abbreviations, likemfor 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 inm s-1for velocity,W m-2for an energy flux, orkm2for an area.- Parameters:
name (str) – An input or output variable name, a CSDMS Standard Name.
- Returns:
The variable units.
- Return type:
Notes
CSDMS uses the UDUNITS standard from Unidata.
- 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:
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:
- get_current_time() float[source]
Return the current time of the model.
- Returns:
The current model time.
- Return type:
- 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:
- get_time_units() str[source]
Time units of the model.
- Returns:
The model time unit; e.g., days or s.
- Return type:
Notes
CSDMS uses the UDUNITS standard from Unidata.
- get_grid_size(grid_id: int) int[source]
Get the total number of elements in the computational grid.
- 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 recentupdate()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:
- 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 nextupdate()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.