Source code for pcse.soil.soil_profile

from math import sqrt
import numpy as np
from ..traitlets import Float, Int, Instance, Enum, Unicode, Bool, HasTraits, List
from ..util import limit, Afgen, merge_dict, DotMap

from .. import exceptions as exc


class pFCurve(Afgen):
    """Pf curve should check that:
    - length of array is even
    - has at least 3 xy pairs
    - pF should start at pF = -1 and end at pF=6
    - SM/cond values should decrease with increase pF

    """
    pass


class MFPCurve(Afgen):
    """Computes Matrix Flow Potential using a Gaussian integration over the pfCurve

    """
    elog10 = 2.302585092994
    Pgauss = (0.0469100770, 0.2307653449, 0.5000000000, 0.7692346551, 0.9530899230)
    Wgauss = (0.1184634425, 0.2393143352, 0.2844444444, 0.2393143352, 0.1184634425)

    def __init__(self, SMfromPF, CONDfromPF):
        SMfromPF = np.array(SMfromPF)
        CONDfromPF = pFCurve(CONDfromPF)
        MFPfromPF = np.zeros_like(SMfromPF)

        # zero MFP at highest pF
        MFPfromPF[-1] = 0.
        MFPfromPF[-2] = SMfromPF[-2]

        for ip in range(len(SMfromPF) - 3, 0, -2):
            # Copy corresponding pF value
            MFPfromPF[ip - 1] = SMfromPF[ip - 1]
            # get integral over PF range
            add = 0.0
            DeltaPF = SMfromPF[ip + 1] - SMfromPF[ip - 1]
            for i in range(len(self.Pgauss)):
                PFg = SMfromPF[ip - 1] + self.Pgauss[i] * DeltaPF
                CON = 10.0 ** CONDfromPF(PFg)
                add += CON * 10.0 ** PFg * self.elog10 * self.Wgauss[i]
            MFPfromPF[ip] = add * DeltaPF + MFPfromPF[ip + 2]
        Afgen.__init__(self, MFPfromPF)


[docs] class SoilLayer(HasTraits): """Contains the intrinsic and derived properties for each soil layers as required by the multilayer waterbalance and SNOMIN. :param layer: a soil layer definition providing parameter values for this layer, see table below. :param PFFieldcapacity: the pF value for defining Field Capacity :param PFWiltingPoint: the pF value for defining the Wilting Point The following properties have to be defined for each layer. =============== ================================================================ ======================= Name Description Unit =============== ================================================================ ======================= CONDfromPF Table function of the 10-base logarithm of the unsaturated hydraulic conductivity as a function of pF. log10(cm water d-1), - SMfromPF Table function that describes the soil moisture content as a function of pF m3 water m-3 soil, - Thickness Layer thickness cm FSOMI Initial fraction of soil organic matter in soil kg OM kg-1 soil CNRatioSOMI Initial C:N ratio of soil organic matter kg C kg-1 N RHOD Bulk density of the soil g soil cm-3 soil Soil_pH pH of the soil layer - CRAIRC Critical air content for aeration for root system m3 air m-3 soil =============== ================================================================ ======================= Based on the soil layer definition the following properties are derived from the parameters in the table above. =============== ================================================================ ======================= Name Description Unit =============== ================================================================ ======================= PFfromSM Afgen table providing the inverted SMfromPF curve m3 water m-3 soil, - MFPfromPF AfTen table describing the Matric Flux Potential as a cm2 d-1 function of the hydraulic head (pF). SM0 The volumetric soil moisture content at saturation (pF = -1) m3 water m-3 soil SMW The volumetric soil moisture content at wilting point m3 water m-3 soil SMFCF The volumetric soil moisture content at field capacity m3 water m-3 soil WC0 The soil moisture amount (cm) at saturation (pF = -1) cm water WCW The soil moisture amount (cm) at wilting point cm water WCFC The soil moisture amount (cm) at field capacity cm water CondFC Soil hydraulic conductivity at field capacity cm water d-1 CondK0 Soil hydraulic conductivity at saturation cm water d-1 =============== ================================================================ ======================= Finally `rooting_status` is initialized to None (not yet known at initialization). """ SMfromPF = Instance(pFCurve) # soil moisture content as function of pF CONDfromPF = Instance(pFCurve) # conductivity as function of pF PFfromSM = Instance(Afgen) # Inverse from SMfromPF MFPfromPF = Instance(MFPCurve) # Matrix Flux Potential as function of pF CNRatioSOMI = Float() # Initial C:N ratio of soil organic matter FSOMI = Float() # Initial fraction of soil organic matter in soil RHOD = Float() # Bulk density of the soil Soil_pH = Float() # pH of the soil layer CRAIRC = Float() # Critical air content Thickness = Float() rooting_status = Enum(["rooted","partially rooted","potentially rooted","never rooted",]) def __init__(self, layer, PFFieldCapacity, PFWiltingPoint): self.SMfromPF = pFCurve(layer.SMfromPF) self.CONDfromPF = pFCurve(layer.CONDfromPF) self.PFfromSM = self._invert_pF(layer.SMfromPF) self.MFPfromPF = MFPCurve(layer.SMfromPF, layer.CONDfromPF) self.CNRatioSOMI = layer.CNRatioSOMI self.FSOMI = layer.FSOMI self.RHOD = layer.RHOD self.CRAIRC = layer.CRAIRC self.Soil_pH = layer.Soil_pH if 5 <= layer.Thickness <= 250: self.Thickness = layer.Thickness else: msg = "Soil layer should have thickness between 5 and 250 cm. Current value: %f" % layer.Thickness raise exc.PCSEError(msg) self.SM0 = self.SMfromPF(-1.0) self.SMFCF = self.SMfromPF(PFFieldCapacity) self.SMW = self.SMfromPF(PFWiltingPoint) self.WC0 = self.SM0 * self.Thickness self.WCW = self.SMW * self.Thickness self.WCFC = self.SMFCF * self.Thickness self.CondFC = 10.0 ** self.CONDfromPF(PFFieldCapacity) self.CondK0 = 10.0 ** self.CONDfromPF(-1.0) self.rooting_status = None # compute hash value of this layer based on pF curves for conductivity and SM self._hash = hash((tuple(layer.SMfromPF), tuple(layer.CONDfromPF))) @property def Thickness_m(self): return self.Thickness * 1e-2 @property def RHOD_kg_per_m3(self): return self.RHOD * 1e-3 * 1e06 def _invert_pF(self, SMfromPF): """Inverts the SMfromPF table to get pF from SM """ l = [] for t in zip(reversed(SMfromPF[1::2]), reversed(SMfromPF[0::2])): l.extend(t) return Afgen(l) def __hash__(self): return self._hash def __eq__(self, other): return ( self.__class__ == other.__class__ and self._hash == other._hash )
[docs] class SoilProfile(list): """A component that represents the soil column as required by the multilayer waterbalance and SNOMIN. :param parvalues: a ParameterProvider instance that will be used to retrieve the description of the soil profile which is assumed to be available under the key `SoilProfileDescription`. This class is basically a container that stores `SoilLayer` instances with some additional logic mainly related to root growth, root status and root water extraction. For detailed information on the properties of the soil layers have a look at the description of the `SoilLayer` class. The description of the soil profile can be defined in YAML format with an example of the structure given below. In this case first three soil physical types are defined under the section `SoilLayerTypes`. Next, these SoilLayerTypes are used to define an actual soil profile using two upper layers of 10 cm of type `TopSoil`, three layers of 10, 20 and 30 cm of type `MidSoil`, a lower layer of 45 cm of type `SubSoil` and finally a SubSoilType with a thickness of 200 cm. Only the top 3 layers contain a certain amaount of organic carbon (FSOMI). An example of a data structure for the soil profile:: SoilLayerTypes: TopSoil: &TopSoil SMfromPF: [-1.0, 0.366, 1.0, 0.338, 1.3, 0.304, 1.7, 0.233, 2.0, 0.179, 2.3, 0.135, 2.4, 0.123, 2.7, 0.094, 3.0, 0.073, 3.3, 0.059, 3.7, 0.046, 4.0, 0.039, 4.17, 0.037, 4.2, 0.036, 6.0, 0.02] CONDfromPF: [-1.0, 1.8451, 1.0, 1.02119, 1.3, 0.51055, 1.7, -0.52288, 2.0, -1.50864, 2.3, -2.56864, 2.4, -2.92082, 2.7, -4.01773, 3.0, -5.11919, 3.3, -6.22185, 3.7, -7.69897, 4.0, -8.79588, 4.17, -9.4318, 4.2, -9.5376, 6.0, -11.5376] CRAIRC: 0.090 CNRatioSOMI: 9.0 RHOD: 1.406 Soil_pH: 7.4 SoilID: TopSoil MidSoil: &MidSoil SMfromPF: [-1.0, 0.366, 1.0, 0.338, 1.3, 0.304, 1.7, 0.233, 2.0, 0.179, 2.3, 0.135, 2.4, 0.123, 2.7, 0.094, 3.0, 0.073, 3.3, 0.059, 3.7, 0.046, 4.0, 0.039, 4.17, 0.037, 4.2, 0.036, 6.0, 0.02] CONDfromPF: [-1.0, 1.8451, 1.0, 1.02119, 1.3, 0.51055, 1.7, -0.52288, 2.0, -1.50864, 2.3, -2.56864, 2.4, -2.92082, 2.7, -4.01773, 3.0, -5.11919, 3.3, -6.22185, 3.7, -7.69897, 4.0, -8.79588, 4.17, -9.4318, 4.2, -9.5376, 6.0, -11.5376] CRAIRC: 0.090 CNRatioSOMI: 9.0 RHOD: 1.406 Soil_pH: 7.4 SoilID: MidSoil_10 SubSoil: &SubSoil SMfromPF: [-1.0, 0.366, 1.0, 0.338, 1.3, 0.304, 1.7, 0.233, 2.0, 0.179, 2.3, 0.135, 2.4, 0.123, 2.7, 0.094, 3.0, 0.073, 3.3, 0.059, 3.7, 0.046, 4.0, 0.039, 4.17, 0.037, 4.2, 0.036, 6.0, 0.02] CONDfromPF: [-1.0, 1.8451, 1.0, 1.02119, 1.3, 0.51055, 1.7, -0.52288, 2.0, -1.50864, 2.3, -2.56864, 2.4, -2.92082, 2.7, -4.01773, 3.0, -5.11919, 3.3, -6.22185, 3.7, -7.69897, 4.0, -8.79588, 4.17, -9.4318, 4.2, -9.5376, 6.0, -11.5376] CRAIRC: 0.090 CNRatioSOMI: 9.0 RHOD: 1.406 Soil_pH: 7.4 SoilID: SubSoil_10 SoilProfileDescription: PFWiltingPoint: 4.2 PFFieldCapacity: 2.0 SurfaceConductivity: 70.0 # surface conductivity cm / day SoilLayers: - <<: *TopSoil Thickness: 10 FSOMI: 0.02 - <<: *TopSoil Thickness: 10 FSOMI: 0.02 - <<: *MidSoil Thickness: 10 FSOMI: 0.01 - <<: *MidSoil Thickness: 20 FSOMI: 0.00 - <<: *MidSoil Thickness: 30 FSOMI: 0.00 - <<: *SubSoil Thickness: 45 FSOMI: 0.00 SubSoilType: <<: *SubSoil Thickness: 200 GroundWater: null """ def __init__(self, parvalues): list.__init__(self) sp = DotMap(parvalues["SoilProfileDescription"]) for layer_properties in sp.SoilLayers: layer = SoilLayer(layer_properties, sp.PFFieldCapacity, sp.PFWiltingPoint) self.append(layer) for attr, value in sp.items(): if attr == "SoilLayers": continue if attr == "SubSoilType": value = SoilLayer(value, sp.PFFieldCapacity, sp.PFWiltingPoint) setattr(self, attr, value) def determine_rooting_status(self, RD, RDM): """Determines the rooting status of the soil layers and update layer weights. Soil layers can be rooted, partially rooted, potentially rooted or never rooted. This is stored in layer.rooting_status Note that this routine expected that the maximum rooting depth coincides with a layer boundary. :param RD:the current rooting depth :param RDM: the maximum rooting depth """ upper_layer_boundary = 0 lower_layer_boundary = 0 for layer in self: lower_layer_boundary += layer.Thickness if lower_layer_boundary <= RD: layer.rooting_status = "rooted" elif upper_layer_boundary < RD < lower_layer_boundary: layer.rooting_status = "partially rooted" elif RD < lower_layer_boundary <= RDM: layer.rooting_status = "potentially rooted" else: layer.rooting_status = "never rooted" upper_layer_boundary = lower_layer_boundary self._compute_layer_weights(RD) def _compute_layer_weights(self, RD): """computes the layer weights given the current rooting depth. :param RD: The current rooting depth """ lower_layer_boundary = 0 for layer in self: lower_layer_boundary += layer.Thickness if layer.rooting_status == "rooted": layer.Wtop = 1.0 layer.Wpot = 0.0 layer.Wund = 0.0 elif layer.rooting_status == "partially rooted": layer.Wtop = 1.0 - (lower_layer_boundary - RD) / layer.Thickness layer.Wpot = 1.0 - layer.Wtop layer.Wund = 0.0 elif layer.rooting_status == "potentially rooted": layer.Wtop = 0.0 layer.Wpot = 1.0 layer.Wund = 0.0 elif layer.rooting_status == "never rooted": layer.Wtop = 0.0 layer.Wpot = 0.0 layer.Wund = 1.0 else: msg = "Unknown rooting status: %s" % layer.rooting_status raise exc.PCSEError(msg) def validate_max_rooting_depth(self, RDM): """Validate that the maximum rooting depth coincides with a layer boundary. :param RDM: The maximum rootable depth :return: True or False """ tiny = 0.01 lower_layer_boundary = 0 for layer in self: lower_layer_boundary += layer.Thickness if abs(RDM - lower_layer_boundary) < tiny: break else: # no break msg = "Current maximum rooting depth (%f) does not coincide with a layer boundary!" % RDM raise exc.PCSEError(msg) def get_max_rootable_depth(self): """Returns the maximum soil rootable depth. here we assume that the max rootable depth is equal to the lower boundary of the last layer. :return: the max rootable depth in cm """ LayerThickness = [l.Thickness for l in self] LayerLowerBoundary = list(np.cumsum(LayerThickness)) return max(LayerLowerBoundary)