Source code for virtual_ecosystem.models.litter.env_factors

"""The ``models.litter.env_factors`` module contains functions that are used to
capture the impact that environmental factors have on litter decay rates. These include
temperature and soil water potential.
"""  # noqa: D205

import numpy as np
from numpy.typing import NDArray
from xarray import DataArray

from virtual_ecosystem.core.core_components import LayerStructure
from virtual_ecosystem.models.litter.model_config import LitterConstants


[docs] def calculate_environmental_factors( air_temperatures: DataArray, soil_temperatures: DataArray, water_potentials: DataArray, layer_structure: LayerStructure, constants: LitterConstants, ): """Calculate the impact of the environment has on litter decay across litter layers. For the above ground layer the impact of temperature is calculated, and for the below ground layer the effect of temperature and soil water potential are both calculated. The relevant above ground temperature is the surface temperature, which can be easily extracted from the temperature data. It's more complex for the below ground temperature and the water potential as the relevant values are averages across the microbially active depth. These are calculated by averaging across the soil layers with each layer weighted by the proportion of the total microbially active depth it represents. If a shallow microbially active depth is used then below ground litter decomposition will be exposed to a high degree of environmental variability. This is representative of the real world, but needs to be kept in mind when comparing to other models. Args: air_temperatures: Air temperatures, for all above ground layers [Celsius] soil_temperatures: Soil temperatures, for all soil layers [Celsius] water_potentials: Water potentials, for all soil layers [kPa] layer_structure: The LayerStructure instance for the simulation. constants: Set of constants for the litter model Returns: A dictionary containing three environmental factors, one for the effect of temperature on above ground litter decay, one for the effect of temperature on below ground litter decay, and one for the effect of soil water potential on below ground litter decay. """ temperatures = { "surface": air_temperatures[layer_structure.index_surface_scalar].to_numpy(), # TODO - This currently takes uses the surface temperature for the first layer. # Once we start change the default to use a thin topsoil layer that should be # used here instead "below_ground": average_temperature_over_microbially_active_layers( soil_temperatures=soil_temperatures, surface_temperature=air_temperatures[ layer_structure.index_surface_scalar ].to_numpy(), layer_structure=layer_structure, ), } water_potential = average_water_potential_over_microbially_active_layers( water_potentials=water_potentials, layer_structure=layer_structure ) temperature_factors = { level: calculate_temperature_effect_on_litter_decomp( temperature=temp, reference_temp=constants.litter_decomp_reference_temp, offset_temp=constants.litter_decomp_offset_temp, temp_response=constants.litter_decomp_temp_response, ) for (level, temp) in temperatures.items() } # Calculate the water factor (relevant for below ground layers) water_factor = calculate_soil_water_effect_on_litter_decomp( water_potential=water_potential, water_potential_halt=constants.litter_decay_water_potential_halt, water_potential_opt=constants.litter_decay_water_potential_optimum, moisture_response_curvature=constants.moisture_response_curvature, ) return { "temp_above": temperature_factors["surface"], "temp_below": temperature_factors["below_ground"], "water": water_factor, }
[docs] def calculate_temperature_effect_on_litter_decomp( temperature: NDArray[np.floating], reference_temp: float, offset_temp: float, temp_response: float, ) -> NDArray[np.floating]: """Calculate the effect that temperature has on litter decomposition rates. This function is taken from :cite:t:`kirschbaum_modelling_2002`. Args: temperature: The temperature of the litter layer [Celsius] reference_temp: The reference temperature for changes in litter decomposition rates with temperature [Celsius] offset_temp: Temperature offset [Celsius] temp_response: Factor controlling response strength to changing temperature [unitless] Returns: A multiplicative factor capturing the impact of temperature on litter decomposition [unitless] """ return np.exp( temp_response * (temperature - reference_temp) / (temperature + offset_temp) )
[docs] def calculate_soil_water_effect_on_litter_decomp( water_potential: NDArray[np.floating], water_potential_halt: float, water_potential_opt: float, moisture_response_curvature: float, ) -> NDArray[np.floating]: """Calculate the effect that soil water potential has on litter decomposition rates. This function is only relevant for the below ground litter pools. Its functional form is taken from :cite:t:`moyano_responses_2013`. Args: water_potential: Soil water potential [kPa] water_potential_halt: Water potential at which all microbial activity stops [kPa] water_potential_opt: Optimal water potential for microbial activity [kPa] moisture_response_curvature: Parameter controlling the curvature of the moisture response function [unitless] Returns: A multiplicative factor capturing the impact of moisture on below ground litter decomposition [unitless] """ # TODO - Need to make sure that this function is properly defined for a plausible # range of matric potentials. # Calculate how much moisture suppresses microbial activity suppression = ( (np.log10(-water_potential) - np.log10(-water_potential_opt)) / (np.log10(-water_potential_halt) - np.log10(-water_potential_opt)) ) ** moisture_response_curvature return 1 - suppression
[docs] def average_temperature_over_microbially_active_layers( soil_temperatures: DataArray, surface_temperature: NDArray[np.floating], layer_structure: LayerStructure, ) -> NDArray[np.floating]: """Average soil temperatures over the microbially active layers. First the average temperature is found for each layer. Then an average across the microbially active depth is taken, weighting by how much of the microbially active depth lies within each layer. Args: soil_temperatures: Soil temperatures to be averaged [Celsius] surface_temperature: Air temperature just above the soil surface [Celsius] layer_structure: The LayerStructure instance for the simulation. Returns: The average temperature across the soil depth considered to be microbially active [Celsius] """ # Find weighting for each layer in the average by dividing the microbially active # depth in each layer by the total depth of microbial activity layer_weights = ( layer_structure.soil_layer_active_thickness / layer_structure.max_depth_of_microbial_activity ) # Find the average for each layer layer_averages = np.empty((layer_weights.shape[0], soil_temperatures.shape[1])) layer_averages[0, :] = ( surface_temperature + soil_temperatures[layer_structure.index_topsoil] ) / 2.0 for index in range(1, len(layer_structure.soil_layer_active_thickness)): layer_averages[index, :] = ( soil_temperatures[layer_structure.index_topsoil_scalar + index - 1] + soil_temperatures[layer_structure.index_topsoil_scalar + index] ) / 2.0 return np.dot(layer_weights, layer_averages)
[docs] def average_water_potential_over_microbially_active_layers( water_potentials: DataArray, layer_structure: LayerStructure, ) -> NDArray[np.floating]: """Average water potentials over the microbially active layers. The average water potential is found for each layer apart from the top layer. This is because for the top layer a sensible average can't be taken as water potential is not defined for the surface layer. In this case, the water potential at the maximum layer height is just treated as the average of the layer. This is a reasonable assumption if the first soil layer is shallow. These water potentials are then averaged across the microbially active depth, weighting by how much of the microbially active depth lies within each layer. Args: water_potentials: Soil water potentials to be averaged [kPa] layer_structure: The LayerStructure instance for the simulation. Returns: The average water potential across the soil depth considered to be microbially active [kPa] """ # Find weighting for each layer in the average by dividing the microbially active # depth in each layer by the total depth of microbial activity layer_weights = ( layer_structure.soil_layer_active_thickness / layer_structure.max_depth_of_microbial_activity ) # Find the average for each layer layer_averages = np.empty((layer_weights.shape[0], water_potentials.shape[1])) # Top layer cannot be averaged layer_averages[0, :] = water_potentials[layer_structure.index_topsoil] for index in range(1, len(layer_structure.soil_layer_active_thickness)): layer_averages[index, :] = ( water_potentials[layer_structure.index_topsoil_scalar + index - 1] + water_potentials[layer_structure.index_topsoil_scalar + index] ) / 2.0 return np.dot(layer_weights, layer_averages)