LCOV - code coverage report
Current view: top level - src - mo_eckhardt_filter.f90 (source / functions) Hit Total Coverage
Test: forces coverage Lines: 69 74 93.2 %
Date: 2024-03-13 19:03:28 Functions: 5 6 83.3 %

          Line data    Source code
       1             : !> \file    mo_eckhardt_filter.f90
       2             : !> \brief \copybrief mo_eckhardt_filter
       3             : !> \details \copydetails mo_eckhardt_filter
       4             : 
       5             : !> \brief   Eckhardt filter for baseflow index calculation.
       6             : !> \details This module provides routines for the Eckardt filter to analyse discharge time series and extract the baseflow.
       7             : !!          The filter was proposed in Eckhardt (2008, doi: 10.1016/j.jhydrol.2008.01.005)
       8             : !> \version 0.1
       9             : !> \authors Sebastian Mueller
      10             : !> \authors Mariaines Di Dato
      11             : !> \date    Apr 2022
      12             : !> \copyright Copyright 2005-\today, the CHS Developers, Sabine Attinger: All rights reserved.
      13             : !! FORCES is released under the LGPLv3+ license \license_note
      14             : module mo_eckhardt_filter
      15             : 
      16             :   use mo_kind,       only: i4, dp
      17             :   use mo_moment,     only: mean
      18             :   use mo_percentile, only: percentile
      19             :   use mo_append,     only: append
      20             :   use mo_nelmin,     only: nelminrange
      21             :   use mo_message,    only: error_message
      22             : 
      23             :   implicit none
      24             :   private
      25             :   public :: fit_alpha
      26             :   public :: eckhardt_filter_fit
      27             :   public :: eckhardt_filter
      28             :   public :: weekly_average
      29             :   public :: BFI
      30             : 
      31             :   real(dp), allocatable :: temp_d7(:)
      32             :   logical, allocatable :: temp_qmin_mask(:), temp_mask(:)
      33             : 
      34             : contains
      35             : 
      36             :   !> \brief   Eckhardt filter for baseflow calculation from discharge time series with fitting.
      37             :   !> \details This filter was proposed in Eckhardt (2008, doi: 10.1016/j.jhydrol.2008.01.005)
      38             :   !> \return  baseflow
      39           0 :   function eckhardt_filter_fit(discharge, mask) result(baseflow)
      40             : 
      41             :     !> array with daily discharge
      42             :     real(dp), intent(in)  :: discharge(:)
      43             :     !> mask for daily discharge
      44             :     logical, intent(in), optional :: mask(:)
      45             : 
      46             :     real(dp), allocatable :: baseflow(:)
      47             : 
      48           0 :     real(dp) :: alpha
      49             : 
      50           0 :     alpha = fit_alpha(discharge, mask=mask)
      51           0 :     baseflow = eckhardt_filter(alpha, discharge, mask=mask)
      52             : 
      53           0 :   end function eckhardt_filter_fit
      54             : 
      55             :   !> \brief   Fitted alpha parameter for the Eckhardt filter.
      56             :   !> \details This filter was proposed in Eckhardt (2008, doi: 10.1016/j.jhydrol.2008.01.005)
      57             :   !> \return  alpha parameter for eckard filter
      58           4 :   real(dp) function fit_alpha(discharge, mask)
      59             : 
      60             :     !> array with daily discharge
      61             :     real(dp), intent(in)  :: discharge(:)
      62             :     !> mask for daily discharge
      63             :     logical, intent(in), optional :: mask(:)
      64             : 
      65           4 :     real(dp) :: alpha_out(1)
      66           4 :     real(dp), allocatable :: q_min(:), dummy(:)
      67           2 :     logical, dimension(size(discharge)) :: mask_
      68             :     integer(i4) :: i, j
      69             : 
      70       14602 :     mask_(:) = .true.
      71        7303 :     if ( present(mask) ) mask_ = mask
      72             : 
      73       14605 :     temp_d7 = weekly_average(discharge, mask=mask)
      74           2 :     allocate(q_min(0))
      75          42 :     do i = 1, size(temp_d7), 365
      76          40 :       j = min(i+364, size(temp_d7))
      77             :       ! only use values in mask
      78             :       ! TODO: do we need a threshold for number in mask here?
      79       14742 :       if ( any(mask_(i : j)) ) call append(q_min, minval(temp_d7(i : j), mask=mask_(i : j)))
      80             :     end do
      81           2 :     if ( size(q_min) < 2 ) call error_message("Eckhardt filter: Less than 2 years of discharge observations! (min. 10 recommended)")
      82             : 
      83           8 :     allocate(temp_qmin_mask(size(discharge)))
      84           4 :     allocate(temp_mask(size(discharge)))
      85       14604 :     temp_mask = mask_
      86       14604 :     temp_qmin_mask = (temp_d7 < percentile(q_min, 10.0_dp, mode_in=4))
      87       14604 :     temp_qmin_mask = temp_qmin_mask .and. temp_mask
      88             : 
      89             :     ! set values outside of mask to 1 in d7
      90       14605 :     allocate(dummy(count(.not.mask_)))
      91        3652 :     dummy(:) = 1.0_dp  ! [1.0_dp, i=1,count(.not.mask_)]
      92             :     temp_d7 = unpack( &
      93             :       vector=dummy, &
      94             :       mask=.not.mask_, &
      95             :       field=temp_d7 &
      96       43804 :     )
      97           2 :     deallocate(dummy)
      98             : 
      99             :     alpha_out = nelminrange( &
     100             :       func=func, &
     101             :       pstart=[0.9_dp], &
     102             :       prange=reshape([0._dp, 1._dp], [1, 2]) &
     103           2 :     )
     104           2 :     fit_alpha = alpha_out(1)
     105             : 
     106           2 :     deallocate(temp_qmin_mask)
     107           2 :     deallocate(temp_mask)
     108           2 :     deallocate(temp_d7)
     109             : 
     110           2 :   end function fit_alpha
     111             : 
     112             :   !> \brief   Eckhardt filter for baseflow calculation from discharge time series.
     113             :   !> \details This filter was proposed in Eckhardt (2008, doi: 10.1016/j.jhydrol.2008.01.005)
     114             :   !> \return  baseflow
     115        1508 :   function eckhardt_filter(alpha, discharge, mask) result(baseflow)
     116             : 
     117             :     !> filter parameter
     118             :     real(dp), intent(in)  :: alpha
     119             :     !> array with daily discharge
     120             :     real(dp), intent(in)  :: discharge(:)
     121             :     !> mask for daily discharge
     122             :     logical, intent(in), optional :: mask(:)
     123             : 
     124             :     real(dp), allocatable :: baseflow(:)
     125             : 
     126         754 :     real(dp), allocatable :: d7(:), d7_perc(:), d_temp(:), b_temp(:)
     127         754 :     logical, dimension(size(discharge)) :: mask_
     128         754 :     real(dp) :: BFI_max
     129             :     integer(i4) :: i
     130             : 
     131     5504954 :     mask_(:) = .true.
     132     5497654 :     if ( present(mask) ) mask_ = mask
     133             : 
     134        2262 :     allocate(baseflow(size(discharge)))
     135        1508 :     allocate(d7_perc(size(discharge)))
     136             : 
     137             :     ! 20 percent percentile with Linear interpolation
     138     5505709 :     d7 = weekly_average(discharge, mask=mask)
     139     5504955 :     d7_perc(:) = percentile(d7, 20.0_dp, mask=mask, mode_in=4)
     140         754 :     BFI_max = BFI(d7_perc, discharge, mask=mask)
     141             : 
     142     5506462 :     allocate(b_temp(count(mask_)))
     143     5506462 :     allocate(d_temp(count(mask_)))
     144         754 :     d_temp = pack(discharge, mask=mask_)
     145             : 
     146             :     ! Applying the equation Eq. (6) from Eckhardt (2008) (only at mask)
     147         754 :     b_temp(1) = ((1 - alpha)*BFI_max * d_temp(1)) / (1 - alpha*BFI_max)
     148     4529650 :     do i = 2, size(d_temp)
     149     4529650 :       b_temp(i) = ((1 - BFI_max)*alpha*b_temp(i-1) + (1 - alpha)*BFI_max*d_temp(i)) / (1 - alpha*BFI_max)
     150             :     end do
     151     5504954 :     baseflow(:) = 0.0_dp
     152    11009908 :     baseflow = unpack(vector=b_temp, mask=mask_, field=baseflow)
     153             : 
     154         756 :   end function eckhardt_filter
     155             : 
     156             :   !> \brief   This function returns the 7days-averaged discharge.
     157             :   !> \return  array with weekly moving average
     158        1512 :   function weekly_average(discharge, mask) result(d7)
     159             : 
     160             :     !> array with daily discharge
     161             :     real(dp), intent(in) :: discharge(:)
     162             :     !> mask for daily discharge
     163             :     logical, intent(in), optional :: mask(:)
     164             : 
     165             :     real(dp), allocatable :: d7(:)
     166             : 
     167         756 :     logical, dimension(size(discharge)) :: mask_
     168             :     integer(i4) :: i, n, m
     169             : 
     170     5519556 :     mask_(:) = .true.
     171     5505710 :     if ( present(mask) ) mask_ = mask
     172             : 
     173        2268 :     allocate(d7(size(discharge)))
     174     5519556 :     d7(:) = 0.0_dp
     175             : 
     176     5519556 :     do i = 1, size(discharge)
     177     5518800 :       n = max(1,i-3)
     178     5518800 :       m = min(size(discharge),i+3)
     179             :       ! TODO: do we need a threshold for number in mask here?
     180    17878520 :       if ( any(mask_(n : m)) ) d7(i) = mean(discharge(n : m), mask=mask_(n : m))
     181             :     end do
     182             : 
     183         754 :   end function weekly_average
     184             : 
     185             :   !> \brief   Calculate the baseflow index as ratio between baseflow and discharge.
     186             :   !> \return  baseflow index
     187        1512 :   real(dp) function BFI(baseflow, discharge, mask)
     188             : 
     189             :     !> array with daily baseflow values
     190             :     real(dp), intent(in) :: baseflow(:)
     191             :     !> array with daily discharge
     192             :     real(dp), intent(in) :: discharge(:)
     193             :     !> mask for daily discharge
     194             :     logical, intent(in), optional :: mask(:)
     195             : 
     196         756 :     logical, dimension(size(discharge)) :: mask_
     197             : 
     198     5519556 :     mask_(:) = .true.
     199     5505710 :     if ( present(mask) ) mask_ = mask
     200             : 
     201    11039868 :     BFI = sum(baseflow, mask=mask_) / sum(discharge, mask=mask_)
     202             : 
     203         756 :   end function BFI
     204             : 
     205             :   !> \brief   Target function for fitting Eckhardt filter.
     206             :   !> \return  Objective value.
     207         752 :   function func(pp)
     208             : 
     209             :     real(dp), dimension(:), intent(in) :: pp  !< alpha (single value)
     210             : 
     211             :     real(dp) :: func
     212             : 
     213         752 :     real(dp), allocatable :: baseflow(:)
     214             : 
     215     5490352 :     baseflow = eckhardt_filter(alpha=pp(1), discharge=temp_d7, mask=temp_mask)
     216     5490352 :     func = mean((baseflow/temp_d7 - 1)**2, mask=temp_qmin_mask)
     217             : 
     218        1508 :   end function func
     219             : 
     220             : end module mo_eckhardt_filter

Generated by: LCOV version 1.16