Source code for virtual_ecosystem.models.hydrology.hydrology_model

"""The :mod:`~virtual_ecosystem.models.hydrology.hydrology_model` module
creates a
:class:`~virtual_ecosystem.models.hydrology.hydrology_model.HydrologyModel`
class as a child of the :class:`~virtual_ecosystem.core.base_model.BaseModel` class.

There are still a number of open TODOs related to process implementation and improvement
, time step and model structure, and units and module coordination.

.. TODO:: processes

    * spin up soil moisture
    * set boundaries for river discharge
    * update infiltration process

.. TODO:: time step and model structure

    * find a way to load daily (precipitation) data and loop over daily time_index
    * potentially move `calculate_drainage_map` to core
    * add abiotic constants from config

.. TODO:: units and module coordination

    * change temperature to Kelvin

"""  # noqa: D205

from __future__ import annotations

from math import sqrt
from typing import Any

import numpy as np
from numpy.typing import NDArray
from pyrealm.constants import CoreConst as PyrealmCoreConst
from xarray import DataArray

from virtual_ecosystem.core.base_model import BaseModel
from virtual_ecosystem.core.configuration import CompiledConfiguration
from virtual_ecosystem.core.core_components import CoreComponents
from virtual_ecosystem.core.data import Data
from virtual_ecosystem.core.logger import LOGGER
from virtual_ecosystem.core.model_config import CoreConfiguration
from virtual_ecosystem.models.abiotic.model_config import (
    AbioticConstants,
)
from virtual_ecosystem.models.hydrology import (
    above_ground,
    below_ground,
    hydrology_tools,
)
from virtual_ecosystem.models.hydrology.model_config import (
    HydrologyConfiguration,
    HydrologyConstants,
)


[docs] class HydrologyModel( BaseModel, model_name="hydrology", model_update_bounds=("1 day", "1 month"), vars_required_for_init=( "layer_heights", "elevation", "air_temperature_ref", "atmospheric_pressure_ref", ), vars_updated=( "interception", "canopy_evaporation", "precipitation_surface", "soil_moisture", "surface_runoff", "vertical_flow", "soil_evaporation", "surface_runoff_routed_plus_local", "subsurface_runoff_routed_plus_local", "total_runoff", "river_discharge_rate", "matric_potential", "groundwater_storage", "subsurface_flow", "baseflow", "bypass_flow", "aerodynamic_resistance_soil", ), vars_required_for_update=( "air_temperature", "relative_humidity", "atmospheric_pressure", "vapour_pressure_deficit", "precipitation", "wind_speed", "leaf_area_index", "layer_heights", "soil_moisture", "transpiration", "density_air", "aerodynamic_resistance_canopy", "specific_heat_air", "stomatal_conductance", "net_radiation", "condensation", ), vars_populated_by_init=( "soil_moisture", "matric_potential", "groundwater_storage", "aerodynamic_resistance_soil", "aerodynamic_resistance_canopy", "specific_heat_air", "stomatal_conductance", "latent_heat_vapourisation", "density_air", "condensation", ), vars_populated_by_first_update=( "interception", "precipitation_surface", "surface_runoff", "bypass_flow", "soil_evaporation", "vertical_flow", "subsurface_flow", "baseflow", "surface_runoff_routed_plus_local", "subsurface_runoff_routed_plus_local", "river_discharge_rate", "total_runoff", "canopy_evaporation", ), ): """A class describing the hydrology model. Args: data: The data object to be used in the model. core_components: The core components used across models. initial_soil_moisture: The initial volumetric relative water content [unitless] for all layers. This will be converted to soil moisture in mm. initial_groundwater_saturation: Initial level of groundwater saturation (between 0 and 1) for all layers and grid cells identical. This will be converted to groundwater storage in mm. model_constants: Set of constants for the hydrology model. abiotic_constants: Some abiotic constants are required in the hydrology model. pyrealm_core_constants: Core constants for the pyrealm package. static: Boolean flag indicating if the model should run in static mode. """ def __init__( self, data: Data, core_components: CoreComponents, initial_soil_moisture: float, initial_groundwater_saturation: float, p_wet_wet: float, p_wet_dry: float, rainfall_shape_parameter: float, rainfall_scale_parameter: float, model_constants: HydrologyConstants = HydrologyConstants(), abiotic_constants: AbioticConstants = AbioticConstants(), pyrealm_core_constants: PyrealmCoreConst = PyrealmCoreConst(), static: bool = False, ): """Hydrology init function. The init function is used only to define class attributes. Any logic should be handled in :fun:`~virtual_ecosystem.hydrology.hydrology_model._setup`. """ super().__init__(data, core_components, static) self.initial_soil_moisture: float """Initial volumetric relative water content [unitless] for all layers and grid cells identical.""" self.initial_groundwater_saturation: float """Initial level of groundwater saturation for all layers identical.""" self.p_wet_wet: float """Probability a wet day follows a wet day.""" self.p_wet_dry: float """Probability a wet day follows a dry day.""" self.rainfall_shape_parameter: float """Shape parameter of Gamma distribution controlling rainfall variability.""" self.rainfall_scale_parameter: float """Scale parameter of Gamma distribution controlling magnitude of rainfall.""" self.model_constants: HydrologyConstants """Set of constants for the hydrology model""" self.pyrealm_core_constants: PyrealmCoreConst """Set of core constants for the pyrealm package""" self.drainage_map: dict """Upstream neighbours for the calculation of horizontal flow.""" self.soil_layer_thickness_mm: np.ndarray """Soil layer thickness in mm.""" self.surface_layer_index: int """Surface layer index.""" # Run the setup if the model is not in deep static mode if self._run_setup: self._setup( initial_soil_moisture=initial_soil_moisture, initial_groundwater_saturation=initial_groundwater_saturation, p_wet_wet=p_wet_wet, p_wet_dry=p_wet_dry, rainfall_shape_parameter=rainfall_shape_parameter, rainfall_scale_parameter=rainfall_scale_parameter, model_constants=model_constants, abiotic_constants=abiotic_constants, pyrealm_core_constants=pyrealm_core_constants, ) def _setup( self, initial_soil_moisture: float, initial_groundwater_saturation: float, p_wet_wet: float, p_wet_dry: float, rainfall_shape_parameter: float, rainfall_scale_parameter: float, model_constants: HydrologyConstants = HydrologyConstants(), abiotic_constants: AbioticConstants = AbioticConstants(), pyrealm_core_constants: PyrealmCoreConst = PyrealmCoreConst(), ) -> None: """Function to set up the hydrology model. This function initializes variables that are required to run the first update(). For the within grid cell hydrology, soil moisture is initialised homogeneously for all soil layers and groundwater storage is set to the percentage of it's capacity that was defined in the model configuration. Soil and canopy aerodynamic resistances are set to an initial constant value. Some additional atmospheric variables are initialised to ensure they are available for update when the Virtual Ecosystem is run with the `abiotic_simple` model. See __init__ for argument descriptions. """ self.initial_soil_moisture = initial_soil_moisture self.initial_groundwater_saturation = initial_groundwater_saturation self.p_wet_wet = p_wet_wet self.p_wet_dry = p_wet_dry self.rainfall_shape_parameter = rainfall_shape_parameter self.rainfall_scale_parameter = rainfall_scale_parameter self.model_constants = model_constants self.abiotic_constants = abiotic_constants self.pyrealm_core_constants = pyrealm_core_constants self.grid.set_neighbours(distance=sqrt(self.grid.cell_area)) """Set neighbours.""" self.drainage_map = above_ground.calculate_drainage_map( grid=self.data.grid, elevation=np.array(self.data["elevation"]), ) # Calculate layer thickness for soil moisture unit conversion and set structures # and tile across grid cells self.soil_layer_thickness_mm = np.tile( ( self.layer_structure.soil_layer_thickness * self.core_constants.meters_to_mm )[:, None], self.grid.n_cells, ) # Select aboveground layer for surface evaporation calculation # TODO this needs to be replaced with 2m above ground value self.surface_layer_index = self.layer_structure.index_surface_scalar # Calculate initial soil moisture, [mm] self.data["soil_moisture"] = hydrology_tools.initialise_soil_moisture_mm( soil_layer_thickness=self.soil_layer_thickness_mm, layer_structure=self.layer_structure, initial_soil_moisture=self.initial_soil_moisture, ) # Make initial guess of the matric potential based on the soil moisture soil_moisture_ini = ( self.data["soil_moisture"][self.layer_structure.index_all_soil].to_numpy() / self.soil_layer_thickness_mm ) effective_saturation = hydrology_tools.calculate_effective_saturation( soil_moisture=soil_moisture_ini, soil_moisture_saturation=self.model_constants.soil_moisture_saturation, soil_moisture_residual=self.model_constants.soil_moisture_residual, ) matric_potential = below_ground.calculate_matric_potential( effective_saturation=effective_saturation, air_entry_potential_inverse=self.model_constants.air_entry_potential_inverse, van_genuchten_nonlinearily_parameter=self.model_constants.van_genuchten_nonlinearily_parameter, ) self.data["matric_potential"] = self.layer_structure.from_template() self.data["matric_potential"][self.layer_structure.index_all_soil] = DataArray( matric_potential * self.model_constants.m_to_kpa, dims=["layers", "cell_id"] ) self.data["condensation"] = self.layer_structure.from_template() # Create initial groundwater storage variable with two layers, [mm] # TODO think about including this in config, but we don't want to carry those # layers around with all variables in the data object initial_groundwater_storage = ( self.initial_groundwater_saturation * self.model_constants.groundwater_capacity ) self.data["groundwater_storage"] = DataArray( np.full((2, self.grid.n_cells), initial_groundwater_storage), dims=("groundwater_layers", "cell_id"), name="groundwater_storage", ) # Initialise atmospheric variables required for update atmosphere_setup = hydrology_tools.initialise_atmosphere_for_hydrology( data=self.data, model_constants=self.model_constants, abiotic_constants=self.abiotic_constants, core_constants=self.core_constants, layer_structure=self.layer_structure, ) self.data.add_from_dict(output_dict=atmosphere_setup)
[docs] @classmethod def from_config( cls, data: Data, configuration: CompiledConfiguration, core_components: CoreComponents, ) -> HydrologyModel: """Factory function to initialise the hydrology model from configuration. This function unpacks the relevant information from the configuration file, and then uses it to initialise the model. If any information from the config is invalid rather than returning an initialised model instance an error is raised. Args: data: A :class:`~virtual_ecosystem.core.data.Data` instance. configuration: A validated Virtual Ecosystem model configuration object. core_components: The core components used across models. """ # Extract the validated hydrology and abiotic configuration from the complete # compiled configuration. hydrology_configuration: HydrologyConfiguration = ( configuration.get_subconfiguration("hydrology", HydrologyConfiguration) ) # Extract the pyrealm configuration from the core constants core_configuration: CoreConfiguration = configuration.get_subconfiguration( "core", CoreConfiguration ) # The abiotic constants are currently hardcoded here - the issue is that the # model relies on two abiotic constants: # abiotic_constants.latent_heat_vap_equ_factors # abiotic_constants.saturated_pressure_slope_parameters # These need to be inherited properly from the configuration but at the moment # we're using abiotic_simple in testing and it isn't a simple swap. So, leaving # this hardcoded for now. abiotic_constants: AbioticConstants = AbioticConstants() LOGGER.info( "Information required to initialise the hydrology model successfully " "extracted." ) return cls( data=data, core_components=core_components, static=hydrology_configuration.static, initial_soil_moisture=hydrology_configuration.initial_soil_moisture, initial_groundwater_saturation=hydrology_configuration.initial_groundwater_saturation, p_wet_wet=hydrology_configuration.p_wet_wet, p_wet_dry=hydrology_configuration.p_wet_dry, rainfall_shape_parameter=hydrology_configuration.rainfall_shape_parameter, rainfall_scale_parameter=hydrology_configuration.rainfall_scale_parameter, model_constants=hydrology_configuration.constants, abiotic_constants=abiotic_constants, pyrealm_core_constants=core_configuration.pyrealm.core, )
[docs] def spinup(self) -> None: """Placeholder function to spin up the hydrology model."""
def _update(self, time_index: int, **kwargs: Any) -> None: r"""Function to update the hydrology model. This function calculates the main hydrological components of the Virtual Ecosystem and updates the following variables in the `data` object: * interception, [mm] * canopy_evaporation, [mm] * precipitation_surface, [mm] * soil_moisture, [mm] * matric_potential, [kPa] * surface_runoff, [mm] * subsurface_flow, [mm] * soil_evaporation, [mm] * vertical_flow, [mm d-1] * groundwater_storage, [mm] * subsurface_flow, [mm] * baseflow, [mm] * surface_runoff_routed_plus_local, [mm] * subsurface_runoff_routed_plus_local, [mm] * total_runoff, [mm] * river_discharge_rate, [m3 s-1] * bypass flow, [mm] * aerodynamic_resistance_soil, [s m-1] Many of the underlying processes are problematic at a monthly timestep, which is currently the only supported update interval. As a short-term work around, the input precipitation is randomly distributed over 30 days and input canopy transpiration is divided by 30, and the return variables are monthly means or monthly accumulated values. Precipitation that reaches the surface is defined as incoming precipitation minus canopy interception, which is estimated using a stroage-based approach, see :func:`~virtual_ecosystem.models.hydrology.above_ground.calculate_interception` . The water from the canopy interception pool either evaporated back to the atmosphere or drips through the canopy reaching the surface with a delay. Surface runoff is calculated with a simple bucket model based on :cite:t:`davis_simple_2017`, see :func:`~virtual_ecosystem.models.hydrology.above_ground.calculate_surface_runoff` : if precipitation exceeds top soil moisture capacity , the excess water is added to runoff and top soil moisture is set to soil moisture capacity value; if the top soil is not saturated, precipitation is added to the current topsoil moisture level and runoff is set to zero. The local contribution of a cell to the river channel is calculated as its own surface and subsurface runoff for the current timestep. Potential soil evaporation is calculated with classical bulk aerodynamic formulation, following the so-called ':math:`\alpha` method', see :func:`~virtual_ecosystem.models.hydrology.above_ground.calculate_soil_evaporation` , and reduced to actual evaporation as a function of leaf area index. Vertical flow between soil layers is calculated combining Richards' equation and Darcy's law for unsaturated flow :func:`~virtual_ecosystem.models.hydrology.below_ground.calculate_vertical_flow` . Here, the mean vertical flow in mm per day that goes though the top soil layer is returned to the data object. Note that there are severe limitations to this approach on the temporal and spatial scale of this model and this can only be treated as a very rough approximation! Soil moisture is updated by iteratively updating the soil moisture of individual layers under consideration of the vertical flow in and out of each layer, see :func:`~virtual_ecosystem.models.hydrology.below_ground.update_soil_moisture` Groundwater storage and flows are modelled using two parallel linear reservoirs, see :func:`~virtual_ecosystem.models.hydrology.below_ground.update_groundwater_storage` . The horizontal flow between grid cells currently uses the same function as the above ground runoff. Total runoff is calculated as the sum of above- and below ground runoff and converted to river discharge rate in [m3 s-1]. The function requires the following input variables from the data object: * air temperature, [C] * relative humidity, [] * atmospheric pressure, [kPa] * vapour pressure deficit, [kPa] * precipitation, [mm] * wind speed, [m s-1] * leaf area index, [m m-1] * layer heights, [m] * Soil moisture (previous time step), [mm] * transpiration (current time step), [mm] * density of air, [kg m-3] * specific heat of air, [kJ kg-1 K-1] * stomatal conductance, [mol m-2 s-1] * aerodynamic resistance canopy, [s m-1] * net radiation, [W m-2] * condensation, [mm] and a number of parameters that as described in detail in :class:`~virtual_ecosystem.models.hydrology.model_config.HydrologyConstants`. """ # Determine number of days days_float: float = ( self.model_timing.update_interval_seconds / self.core_constants.seconds_to_day ) days: int = int(days_float // 1) # Check if the number of days is exact and warn if not if not np.allclose(days_float % 1, 0): LOGGER.warning( f"Update interval is not a whole number of days ({days_float})," f" partitioning inputs among {days} days." ) # Set seed for random rainfall generator seed: None | int = kwargs.pop("seed", None) # Select variables at relevant heights for current time step hydro_input = hydrology_tools.setup_hydrology_input_current_timestep( data=self.data, time_index=time_index, days=days, seed=seed, layer_structure=self.layer_structure, soil_layer_thickness_mm=self.soil_layer_thickness_mm, soil_moisture_saturation=self.model_constants.soil_moisture_saturation, soil_moisture_residual=self.model_constants.soil_moisture_residual, p_wet_wet=self.p_wet_wet, p_wet_dry=self.p_wet_dry, shape_parameter=self.rainfall_shape_parameter, scale_parameter=self.rainfall_scale_parameter, ) # Calculate psychrometric constant psychrometric_constant = hydrology_tools.calculate_psychrometric_constant( atmospheric_pressure=self.data["atmospheric_pressure"].to_numpy(), latent_heat_vapourization=self.data["latent_heat_vapourisation"].to_numpy(), specific_heat_air=self.data["specific_heat_air"].to_numpy() / 1000.0, molecular_weight_ratio_water_to_dry_air=( self.core_constants.molecular_weight_ratio_water_to_dry_air ), ) # Create lists for output variables to store daily data daily_lists: dict = {name: [] for name in self.vars_updated} for day in np.arange(days): # Interception of water in canopy, [mm] interception = above_ground.calculate_interception( leaf_area_index=self.data["leaf_area_index"].to_numpy(), precipitation=hydro_input["current_precipitation"][:, day], intercept_parameters=self.model_constants.intercept_parameters, veg_density_param=self.model_constants.veg_density_param, ) daily_lists["interception"].append(interception) # Calculate canopy evaporation, [mm day-1] canopy_evaporation = above_ground.calculate_canopy_evaporation( leaf_area_index=self.data["leaf_area_index"].to_numpy(), interception=interception, net_radiation=self.data["net_radiation"].to_numpy(), vapour_pressure_deficit=self.data["vapour_pressure_deficit"].to_numpy(), air_temperature=self.data["air_temperature"].to_numpy(), density_air_kg=self.data["density_air"].to_numpy(), specific_heat_air=self.data["specific_heat_air"].to_numpy() / 1000, aerodynamic_resistance_canopy=self.data[ "aerodynamic_resistance_canopy" ].to_numpy(), stomatal_resistance=( self.core_constants.conductance_to_resistance_conversion_factor / self.data["stomatal_conductance"].to_numpy() ), latent_heat_vapourisation=self.data[ "latent_heat_vapourisation" ].to_numpy(), psychrometric_constant=psychrometric_constant, saturated_pressure_slope_parameters=( self.abiotic_constants.saturated_pressure_slope_parameters ), time_interval=self.core_constants.seconds_to_day, extinction_coefficient_global_radiation=( self.model_constants.extinction_coefficient_global_radiation ), ) daily_lists["canopy_evaporation"].append(canopy_evaporation) # Precipitation that reaches the surface per day, [mm] precipitation_surface = hydro_input["current_precipitation"][ :, day ] - np.minimum( np.nansum(canopy_evaporation, axis=0), hydro_input["current_precipitation"][:, day], +hydro_input["condensation"], ) hydrology_tools.check_precipitation_surface( precipitation_surface=precipitation_surface ) daily_lists["precipitation_surface"].append(precipitation_surface) # Calculate daily surface runoff of each grid cell, [mm] surface_runoff_local = above_ground.calculate_surface_runoff( precipitation_surface=precipitation_surface, top_soil_moisture=hydro_input["current_soil_moisture"][0], top_soil_moisture_saturation=hydro_input[ "top_soil_moisture_saturation" ], ) daily_lists["surface_runoff"].append(surface_runoff_local) # Calculate preferential bypass flow, [mm] bypass_flow = above_ground.calculate_bypass_flow( top_soil_moisture=hydro_input["current_soil_moisture"][0], sat_top_soil_moisture=hydro_input["top_soil_moisture_saturation"], available_water=precipitation_surface - surface_runoff_local, bypass_flow_coefficient=(self.model_constants.bypass_flow_coefficient), ) daily_lists["bypass_flow"].append(bypass_flow) # Calculate top soil moisture after infiltration, [mm] soil_moisture_infiltrated = np.clip( ( hydro_input["current_soil_moisture"][0] + precipitation_surface - surface_runoff_local - bypass_flow, ), 0, hydro_input["top_soil_moisture_saturation"], ).squeeze() # Prepare inputs for soil evaporation function # TODO currently surface layer, needs to be replaced with 2m above ground top_soil_moisture_vol = ( soil_moisture_infiltrated / self.soil_layer_thickness_mm[0] ) soil_evaporation = above_ground.calculate_soil_evaporation( temperature=hydro_input["surface_temperature"], relative_humidity=hydro_input["surface_humidity"], atmospheric_pressure=hydro_input["surface_pressure"], soil_moisture=top_soil_moisture_vol, soil_moisture_residual=self.model_constants.soil_moisture_residual, soil_moisture_saturation=self.model_constants.soil_moisture_saturation, leaf_area_index=hydro_input["leaf_area_index_sum"], wind_speed_surface=hydro_input["surface_wind_speed"], density_air=self.data["density_air"][ self.surface_layer_index ].to_numpy(), latent_heat_vapourisation=self.data["latent_heat_vapourisation"][ self.surface_layer_index ].to_numpy(), gas_constant_water_vapour=self.core_constants.gas_constant_water_vapour / 1000.0, drag_coefficient_evaporation=( self.model_constants.drag_coefficient_evaporation ), extinction_coefficient_global_radiation=( self.model_constants.extinction_coefficient_global_radiation ), time_interval=self.core_constants.seconds_to_day, pyrealm_core_constants=self.pyrealm_core_constants, ) daily_lists["soil_evaporation"].append(soil_evaporation["soil_evaporation"]) daily_lists["aerodynamic_resistance_soil"].append( soil_evaporation["aerodynamic_resistance_soil"] ) # Calculate top soil moisture after evap and combine with lower layers, [mm] soil_moisture_evap_mm: NDArray[np.floating] = np.concatenate( ( np.expand_dims( np.clip( ( soil_moisture_infiltrated - soil_evaporation["soil_evaporation"] ), hydro_input["top_soil_moisture_residual"], hydro_input["top_soil_moisture_saturation"], ), axis=0, ), hydro_input["current_soil_moisture"][1:], ) ) # Calculate vertical flow between soil layers in mm per day and soil matric # potential in m (later converted to kPa for data object). # Note that there are severe limitations to this approach on the temporal # spatial scale of this model and this can only be treated as a very rough # approximation to discuss nutrient leaching. vertical_flow = below_ground.calculate_vertical_flow( soil_moisture=soil_moisture_evap_mm / self.soil_layer_thickness_mm, # vol soil_layer_thickness=self.soil_layer_thickness_mm / 1000.0, # m soil_layer_depth=np.abs(self.layer_structure.soil_layer_depths), # m soil_moisture_saturation=( self.model_constants.soil_moisture_saturation ), # vol soil_moisture_residual=( self.model_constants.soil_moisture_residual ), # vol saturated_hydraulic_conductivity=( self.model_constants.saturated_hydraulic_conductivity ), # m/s air_entry_potential_inverse=( self.model_constants.air_entry_potential_inverse ), # m/m van_genuchten_nonlinearily_parameter=( self.model_constants.van_genuchten_nonlinearily_parameter ), pore_connectivity_parameter=( self.model_constants.pore_connectivity_parameter ), groundwater_capacity=self.model_constants.groundwater_capacity / 1000.0, seconds_to_day=self.core_constants.seconds_to_day, ) daily_lists["matric_potential"].append( vertical_flow["matric_potential"] * self.model_constants.m_to_kpa ) daily_lists["vertical_flow"].append(vertical_flow["vertical_flow"]) # Update soil moisture by +/- vertical flow to each layer and remove root # water uptake by plants (transpiration), [mm] soil_moisture_updated = below_ground.update_soil_moisture( soil_moisture=soil_moisture_evap_mm, # mm vertical_flow=vertical_flow["vertical_flow"], # mm day-1 transpiration=hydro_input["current_transpiration"], # mm soil_moisture_saturation=( # mm self.model_constants.soil_moisture_saturation * self.soil_layer_thickness_mm ), soil_moisture_residual=( # mm self.model_constants.soil_moisture_residual * self.soil_layer_thickness_mm ), ) daily_lists["soil_moisture"].append(soil_moisture_updated) # calculate below ground horizontal flow and update ground water below_ground_flow = below_ground.update_groundwater_storage( groundwater_storage=hydro_input["groundwater_storage"], vertical_flow_to_groundwater=vertical_flow["vertical_flow"][-1], bypass_flow=bypass_flow, max_percolation_rate_uzlz=( self.model_constants.max_percolation_rate_uzlz ), groundwater_loss=self.model_constants.groundwater_loss, reservoir_const_upper_groundwater=( self.model_constants.reservoir_const_upper_groundwater ), reservoir_const_lower_groundwater=( self.model_constants.reservoir_const_lower_groundwater ), ) for var in ["groundwater_storage", "subsurface_flow", "baseflow"]: daily_lists[var].append(below_ground_flow[var]) # Calculate horizontal flows between grid cells individually for output # Surface runoff routed to each cell + local surface runoff surface_runoff_routed_plus_local = above_ground.route_horizontal_flow( drainage_map=self.drainage_map, surface_runoff=surface_runoff_local, subsurface_runoff=np.zeros_like( surface_runoff_local ), # only surface here ) daily_lists["surface_runoff_routed_plus_local"].append( surface_runoff_routed_plus_local ) # Subsurface runoff routed to each cell + local subsurface runoff subsurface_flow = np.array( below_ground_flow["subsurface_flow"] + below_ground_flow["baseflow"] ) subsurface_runoff_routed_plus_local = above_ground.route_horizontal_flow( drainage_map=self.drainage_map, surface_runoff=np.zeros_like(subsurface_flow), # only subsurface here subsurface_runoff=subsurface_flow, ) daily_lists["subsurface_runoff_routed_plus_local"].append( subsurface_runoff_routed_plus_local ) # Total runoff at each cell = surface + subsurface contributions total_runoff = ( surface_runoff_routed_plus_local + subsurface_runoff_routed_plus_local ) daily_lists["total_runoff"].append(total_runoff) # Convert total runoff [mm] to river discharge rate [m³/s] river_discharge_rate = above_ground.convert_mm_flow_to_m3_per_second( river_discharge_mm=total_runoff, area=self.grid.cell_area, days=days, seconds_to_day=self.core_constants.seconds_to_day, meters_to_millimeters=self.core_constants.meters_to_mm, ) daily_lists["river_discharge_rate"].append(river_discharge_rate) # Update other model states for next day hydro_input["current_soil_moisture"] = soil_moisture_updated hydro_input["groundwater_storage"] = below_ground_flow[ "groundwater_storage" ] # create output dict as intermediate step to not overwrite data directly soil_hydrology = {} # Calculate monthly accumulated/mean values for hydrology variables for var in [ "precipitation_surface", "surface_runoff", "soil_evaporation", "subsurface_flow", "baseflow", "bypass_flow", "surface_runoff_routed_plus_local", "subsurface_runoff_routed_plus_local", "total_runoff", ]: soil_hydrology[var] = DataArray( np.nansum(np.stack(daily_lists[var], axis=1), axis=1), dims="cell_id", coords={"cell_id": self.grid.cell_id}, ) # Canopy evaporation/intercept is accumulated over days, [mm] for var in ["canopy_evaporation", "interception"]: soil_hydrology[var] = self.layer_structure.from_template() soil_hydrology[var][:,] = np.where( np.isnan(self.data["leaf_area_index"]), np.nan, np.nansum(daily_lists[var], axis=0), ) # Calculate monthly mean values for river discharge rate and soil resistance for var in ["river_discharge_rate", "aerodynamic_resistance_soil"]: soil_hydrology[var] = DataArray( np.mean(np.stack(daily_lists[var], axis=1), axis=1), dims="cell_id", coords={"cell_id": self.grid.cell_id}, ) # Return mean soil moisture, [mm], and soil matric potential, [kPa], and add # atmospheric layers (nan) for var in ["soil_moisture", "matric_potential", "vertical_flow"]: soil_hydrology[var] = self.layer_structure.from_template() soil_hydrology[var][self.layer_structure.index_all_soil] = np.mean( np.stack(daily_lists[var], axis=0), axis=0 ) # Save last state of groundwater storage, [mm] soil_hydrology["groundwater_storage"] = DataArray( daily_lists["groundwater_storage"][day], dims=self.data["groundwater_storage"].dims, ) # Update data object self.data.add_from_dict(output_dict=soil_hydrology)
[docs] def cleanup(self) -> None: """Placeholder function for hydrology model cleanup."""