#!/usr/bin/env python # -*- coding: utf-8 -*- # # PCR-GLOBWB (PCRaster Global Water Balance) Global Hydrological Model # # Copyright (C) 2016, Ludovicus P. H. (Rens) van Beek, Edwin H. Sutanudjaja, Yoshihide Wada, # Joyce H. C. Bosmans, Niels Drost, Inge E. M. de Graaf, Kor de Jong, Patricia Lopez Lopez, # Stefanie Pessenteiner, Oliver Schmitz, Menno W. Straatsma, Niko Wanders, Dominik Wisser, # and Marc F. P. Bierkens, # Faculty of Geosciences, Utrecht University, Utrecht, The Netherlands # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import types import math import types from pcraster.framework import * import pcraster as pcr import logging logger = logging.getLogger(__name__) import virtualOS as vos from ncConverter import * import waterBodies class Routing(object): #TODO: remove def getPseudoState(self): result = {} return result #TODO: remove def getVariables(self, names): result = {} return result def getState(self): result = {} result['timestepsToAvgDischarge'] = self.timestepsToAvgDischarge # day result['channelStorage'] = self.channelStorage # m3 ; channel storage, including lake and reservoir storage result['readAvlChannelStorage'] = self.readAvlChannelStorage # m3 ; readily available channel storage that can be extracted to satisfy water demand result['avgDischargeLong'] = self.avgDischarge # m3/s ; long term average discharge result['m2tDischargeLong'] = self.m2tDischarge # (m3/s)^2 result['avgBaseflowLong'] = self.avgBaseflow # m3/s ; long term average baseflow result['riverbedExchange'] = self.riverbedExchange # m3/day : river bed infiltration (from surface water bdoies to groundwater) result['waterBodyStorage'] = self.waterBodyStorage # m3 ; storages of lakes and reservoirs # values given are per water body id (not per cell) result['avgLakeReservoirOutflowLong'] = self.avgOutflow # m3/s ; long term average lake & reservoir outflow # values given are per water body id (not per cell) result['avgLakeReservoirInflowShort'] = self.avgInflow # m3/s ; short term average lake & reservoir inflow # values given are per water body id (not per cell) result['avgDischargeShort'] = self.avgDischargeShort # m3/s ; short term average discharge # This variable needed only for kinematic wave methods (i.e. kinematicWave and simplifiedKinematicWave) result['subDischarge'] = self.subDischarge # m3/s ; sub-time step discharge (needed for kinematic wave methods/approaches) return result def __init__(self,iniItems,initialConditions,lddMap): object.__init__(self) self.lddMap = lddMap self.cloneMap = iniItems.cloneMap self.tmpDir = iniItems.tmpDir self.inputDir = iniItems.globalOptions['inputDir'] # option to activate water balance check self.debugWaterBalance = True if iniItems.routingOptions['debugWaterBalance'] == "False": self.debugWaterBalance = False self.method = iniItems.routingOptions['routingMethod'] # option to include lakes and reservoirs self.includeWaterBodies = True if 'includeWaterBodies' in iniItems.routingOptions.keys(): if iniItems.routingOptions['includeWaterBodies'] == "False" or\ iniItems.routingOptions['includeWaterBodies'] == "None": self.includeWaterBodies = False # local drainage direction: self.lddMap = vos.readPCRmapClone(iniItems.routingOptions['lddMap'], self.cloneMap,self.tmpDir,self.inputDir,True) self.lddMap = pcr.lddrepair(pcr.ldd(self.lddMap)) self.lddMap = pcr.lddrepair(self.lddMap) # landmask: if iniItems.globalOptions['landmask'] != "None": self.landmask = vos.readPCRmapClone(\ iniItems.globalOptions['landmask'], self.cloneMap,self.tmpDir,self.inputDir) else: self.landmask = pcr.defined(self.lddMap) self.landmask = pcr.ifthen(pcr.defined(self.lddMap), self.landmask) self.landmask = pcr.cover(self.landmask, pcr.boolean(0)) # ldd mask self.lddMap = pcr.lddmask(self.lddMap, self.landmask) # cell area (unit: m2) self.cellArea = vos.readPCRmapClone(\ iniItems.routingOptions['cellAreaMap'], self.cloneMap,self.tmpDir,self.inputDir) # model resolution in arc-degree unit self.cellSizeInArcDeg = vos.getMapAttributes(self.cloneMap,"cellsize") # maximum number of days (timesteps) to calculate long term average flow values (default: 5 years = 5 * 365 days = 1825) self.maxTimestepsToAvgDischargeLong = 1825. # maximum number of days (timesteps) to calculate short term average values (default: 1 month = 1 * 30 days = 30) self.maxTimestepsToAvgDischargeShort = 30. routingParameters = ['gradient','manningsN'] for var in routingParameters: input = iniItems.routingOptions[str(var)] vars(self)[var] = vos.readPCRmapClone(input,\ self.cloneMap,self.tmpDir,self.inputDir) # parameters needed to estimate channel dimensions/parameters # - used in the method/function 'getRoutingParamAvgDischarge' self.eta = 0.25 self.nu = 0.40 self.tau = 8.00 self.phi = 0.58 # option to use minimum channel width (m) self.minChannelWidth = pcr.scalar(0.0) if "minimumChannelWidth" in iniItems.routingOptions.keys(): if iniItems.routingOptions['minimumChannelWidth'] != "None":\ self.minChannelWidth = pcr.cover(vos.readPCRmapClone(\ iniItems.routingOptions['minimumChannelWidth'], self.cloneMap,self.tmpDir,self.inputDir), 0.0) # option to use constant/pre-defined channel width (m) self.predefinedChannelWidth = None if "constantChannelWidth" in iniItems.routingOptions.keys(): if iniItems.routingOptions['constantChannelWidth'] != "None":\ self.predefinedChannelWidth = pcr.cover(vos.readPCRmapClone(\ iniItems.routingOptions['constantChannelWidth'], self.cloneMap,self.tmpDir,self.inputDir), 0.0) # option to use constant/pre-defined channel depth (m) self.predefinedChannelDepth = None if "constantChannelDepth" in iniItems.routingOptions.keys(): if iniItems.routingOptions['constantChannelDepth'] != "None":\ self.predefinedChannelDepth = pcr.cover(vos.readPCRmapClone(\ iniItems.routingOptions['constantChannelDepth'], self.cloneMap,self.tmpDir,self.inputDir), 0.0) # an assumption for broad sheet flow in kinematic wave methods/approaches self.beta = 0.6 # channelLength = approximation of channel length (unit: m) # This is approximated by cell diagonal. cellSizeInArcMin = self.cellSizeInArcDeg*60. verticalSizeInMeter = cellSizeInArcMin*1852. # self.cellLengthFD = ((self.cellArea/verticalSizeInMeter)**(2)+\ (verticalSizeInMeter)**(2))**(0.5) self.channelLength = self.cellLengthFD # # channel length (unit: m) if "channelLength" in iniItems.routingOptions.keys(): if iniItems.routingOptions['channelLength'] != "None":\ self.channelLength = pcr.cover( vos.readPCRmapClone(\ iniItems.routingOptions['channelLength'], self.cloneMap,self.tmpDir,self.inputDir), self.channelLength) # dist2celllength in m/arcDegree (needed in the accuTravelTime function): nrCellsDownstream = pcr.ldddist(self.lddMap,\ self.lddMap == 5,1.) distanceDownstream = pcr.ldddist(self.lddMap,\ self.lddMap == 5,\ self.channelLength) channelLengthDownstream = \ (self.channelLength + distanceDownstream)/\ (nrCellsDownstream + 1) # unit: m self.dist2celllength = channelLengthDownstream /\ self.cellSizeInArcDeg # unit: m/arcDegree # the channel gradient must be >= minGradient minGradient = 0.00005 # 0.000005 self.gradient = pcr.max(minGradient,\ pcr.cover(self.gradient, minGradient)) # initiate/create WaterBody class self.WaterBodies = waterBodies.WaterBodies(iniItems,self.landmask) # crop evaporation coefficient for surface water bodies self.no_zero_crop_water_coefficient = True if iniItems.routingOptions['cropCoefficientWaterNC'] == "None": self.no_zero_crop_water_coefficient = False else: self.fileCropKC = vos.getFullPath(\ iniItems.routingOptions['cropCoefficientWaterNC'],\ self.inputDir) # courantNumber criteria for numerical stability in kinematic wave methods/approaches self.courantNumber = 0.50 # empirical values for minimum number of sub-time steps: design_flood_speed = 5.00 # m/s design_length_of_sub_time_step = pcr.cellvalue( pcr.mapminimum( self.courantNumber * self.channelLength / design_flood_speed),1)[0] self.limit_num_of_sub_time_steps = np.ceil( vos.secondsPerDay() / design_length_of_sub_time_step) # # minimum number of sub-time steps: 24 ; hourly resolution as used in Van Beek et al. (2011) self.limit_num_of_sub_time_steps = max(24.0, self.limit_num_of_sub_time_steps) # minimum number of a sub time step based on the configuration/ini file: if 'maxiumLengthOfSubTimeStep' in iniItems.routingOptions.keys(): maxiumLengthOfSubTimeStep = float(iniItems.routingOptions['maxiumLengthOfSubTimeStep']) minimum_number_of_sub_time_step = np.ceil( vos.secondsPerDay() / maxiumLengthOfSubTimeStep ) self.limit_num_of_sub_time_steps = max(\ minimum_number_of_sub_time_step, \ self.limit_num_of_sub_time_steps) # self.limit_num_of_sub_time_steps = np.int(self.limit_num_of_sub_time_steps) # critical water height (m) used to select stable length of sub time step in kinematic wave methods/approaches self.critical_water_height = 0.25; # used in Van Beek et al. (2011) # assumption for the minimum fracwat value used for calculating water height self.min_fracwat_for_water_height = 0.001 # dimensionless # assumption for minimum crop coefficient for surface water bodies self.minCropWaterKC = 0.00 if 'minCropWaterKC' in iniItems.routingOptions.keys(): self.minCropWaterKC = float(iniItems.routingOptions['minCropWaterKC']) # get the initialConditions self.getICs(iniItems, initialConditions) # flood plain options: ################################################################################# self.floodPlain = iniItems.routingOptions['dynamicFloodPlain'] == "True" if self.floodPlain: logger.info("Flood plain extents can vary during the simulation.") # get ManningsN for the flood plain areas input = iniItems.routingOptions['floodplainManningsN'] self.floodplainManN = vos.readPCRmapClone(input,\ self.cloneMap, self.tmpDir, self.inputDir) # reduction parameter of smoothing interval and error threshold self.reductionKK = 0.5 if 'reductionKK' in iniItems.routingOptions.keys(): self.reductionKK= float(iniItems.routingOptions['reductionKK']) self.criterionKK = 40.0 if 'criterionKK' in iniItems.routingOptions.keys(): self.criterionKK= float(iniItems.routingOptions['criterionKK']) # get relative elevation (above floodplain) profile per grid cell (including smoothing parameters) self.nrZLevels, self.areaFractions, self.relZ, self.floodVolume, self.kSlope, self.mInterval = \ self.getElevationProfile(iniItems) # get bankfull capacity (unit: m3) self.predefinedBankfullCapacity = None self.usingFixedBankfullCapacity = False if iniItems.routingOptions['bankfullCapacity'] != "None" : self.usingFixedBankfullCapacity = True self.predefinedBankfullCapacity = vos.readPCRmapClone(iniItems.routingOptions['bankfullCapacity'],\ self.cloneMap, self.tmpDir, self.inputDir) else: msg = "The bankfull channel storage capacity is NOT defined in the configuration file. " if isinstance(self.predefinedChannelWidth, types.NoneType) or\ isinstance(self.predefinedChannelDepth, types.NoneType): msg += "The bankfull capacity is estimated from average discharge (5 year long term average)." else: msg += "The bankfull capacity is estimated from the given channel depth and channel width." self.usingFixedBankfullCapacity = True self.predefinedBankfullCapacity = self.estimateBankfullCapacity(self.predefinedChannelWidth,\ self.predefinedChannelDepth) logger.info(msg) # covering the value self.predefinedBankfullCapacity = pcr.cover(self.predefinedBankfullCapacity, 0.0) # zero fracwat assumption (used for debugging to the version 1) self.zeroFracWatAllAndAlways = False if iniItems.debug_to_version_one: self.zeroFracWatAllAndAlways = True # initiate old style reporting # This is still very useful during the 'debugging' process. self.initiate_old_style_routing_reporting(iniItems) def getICs(self,iniItems,iniConditions = None): if iniConditions == None: # read initial conditions from pcraster maps listed in the ini file (for the first time step of the model; when the model just starts) self.timestepsToAvgDischarge = vos.readPCRmapClone(iniItems.routingOptions['timestepsToAvgDischargeIni'] ,self.cloneMap,self.tmpDir,self.inputDir) self.channelStorage = vos.readPCRmapClone(iniItems.routingOptions['channelStorageIni'] ,self.cloneMap,self.tmpDir,self.inputDir) self.readAvlChannelStorage = vos.readPCRmapClone(iniItems.routingOptions['readAvlChannelStorageIni'] ,self.cloneMap,self.tmpDir,self.inputDir) self.avgDischarge = vos.readPCRmapClone(iniItems.routingOptions['avgDischargeLongIni'] ,self.cloneMap,self.tmpDir,self.inputDir) self.m2tDischarge = vos.readPCRmapClone(iniItems.routingOptions['m2tDischargeLongIni'] ,self.cloneMap,self.tmpDir,self.inputDir) self.avgBaseflow = vos.readPCRmapClone(iniItems.routingOptions['avgBaseflowLongIni'] ,self.cloneMap,self.tmpDir,self.inputDir) self.riverbedExchange = vos.readPCRmapClone(iniItems.routingOptions['riverbedExchangeIni'] ,self.cloneMap,self.tmpDir,self.inputDir) # New initial condition variable introduced in the version 2.0.2: avgDischargeShort self.avgDischargeShort = vos.readPCRmapClone(iniItems.routingOptions['avgDischargeShortIni'] ,self.cloneMap,self.tmpDir,self.inputDir) # Initial conditions needed for kinematic wave methods self.subDischarge = vos.readPCRmapClone(iniItems.routingOptions['subDischargeIni'],self.cloneMap,self.tmpDir,self.inputDir) else: # read initial conditions from the memory self.timestepsToAvgDischarge = iniConditions['routing']['timestepsToAvgDischarge'] self.channelStorage = iniConditions['routing']['channelStorage'] self.readAvlChannelStorage = iniConditions['routing']['readAvlChannelStorage'] self.avgDischarge = iniConditions['routing']['avgDischargeLong'] self.m2tDischarge = iniConditions['routing']['m2tDischargeLong'] self.avgBaseflow = iniConditions['routing']['avgBaseflowLong'] self.riverbedExchange = iniConditions['routing']['riverbedExchange'] self.avgDischargeShort = iniConditions['routing']['avgDischargeShort'] self.subDischarge = iniConditions['routing']['subDischarge'] self.channelStorage = pcr.ifthen(self.landmask, pcr.cover(self.channelStorage, 0.0)) self.readAvlChannelStorage = pcr.ifthen(self.landmask, pcr.cover(self.readAvlChannelStorage, 0.0)) self.avgDischarge = pcr.ifthen(self.landmask, pcr.cover(self.avgDischarge, 0.0)) self.m2tDischarge = pcr.ifthen(self.landmask, pcr.cover(self.m2tDischarge, 0.0)) self.avgDischargeShort = pcr.ifthen(self.landmask, pcr.cover(self.avgDischargeShort, 0.0)) self.avgBaseflow = pcr.ifthen(self.landmask, pcr.cover(self.avgBaseflow, 0.0)) self.riverbedExchange = pcr.ifthen(self.landmask, pcr.cover(self.riverbedExchange, 0.0)) self.subDischarge = pcr.ifthen(self.landmask, pcr.cover(self.subDischarge , 0.0)) self.readAvlChannelStorage = pcr.min(self.readAvlChannelStorage, self.channelStorage) self.readAvlChannelStorage = pcr.max(self.readAvlChannelStorage, 0.0) # make sure that timestepsToAvgDischarge is consistent (or the same) for the entire map: try: self.timestepsToAvgDischarge = pcr.mapmaximum(self.timestepsToAvgDischarge) except: pass # We have to use 'try/except' because 'pcr.mapmaximum' cannot handle scalar value # for netcdf reporting, we have to make sure that timestepsToAvgDischarge is spatial and scalar (especially while performing pcr2numpy operations) self.timestepsToAvgDischarge = pcr.spatial(pcr.scalar(self.timestepsToAvgDischarge)) self.timestepsToAvgDischarge = pcr.ifthen(self.landmask, self.timestepsToAvgDischarge) # Initial conditions needed for water bodies: # - initial short term average inflow (m3/s) and # long term average outflow (m3/s) if iniConditions == None: # read initial conditions from pcraster maps listed in the ini file (for the first time step of the model; when the model just starts) self.avgInflow = vos.readPCRmapClone(iniItems.routingOptions['avgLakeReservoirInflowShortIni'],self.cloneMap,self.tmpDir,self.inputDir) self.avgOutflow = vos.readPCRmapClone(iniItems.routingOptions['avgLakeReservoirOutflowLongIni'],self.cloneMap,self.tmpDir,self.inputDir) if not isinstance(iniItems.routingOptions['waterBodyStorageIni'],types.NoneType): self.waterBodyStorage = vos.readPCRmapClone(iniItems.routingOptions['waterBodyStorageIni'], self.cloneMap,self.tmpDir,self.inputDir) self.waterBodyStorage = pcr.ifthen(self.landmask, pcr.cover(self.waterBodyStorage, 0.0)) else: self.waterBodyStorage = None else: # read initial conditions from the memory self.avgInflow = iniConditions['routing']['avgLakeReservoirInflowShort'] self.avgOutflow = iniConditions['routing']['avgLakeReservoirOutflowLong'] self.waterBodyStorage = iniConditions['routing']['waterBodyStorage'] self.avgInflow = pcr.ifthen(self.landmask, pcr.cover(self.avgInflow , 0.0)) self.avgOutflow = pcr.ifthen(self.landmask, pcr.cover(self.avgOutflow, 0.0)) if not isinstance(self.waterBodyStorage, types.NoneType): self.waterBodyStorage = pcr.ifthen(self.landmask, pcr.cover(self.waterBodyStorage, 0.0)) def estimateBankfullDischarge(self, bankfullWidth, factor = 4.8): # bankfull discharge (unit: m3/s) # - from Lacey formula: P = B = 4.8 * (Qbf)**0.5 bankfullDischarge = (bankfullWidth / factor ) ** (2.0) return bankfullDischarge def estimateBankfullDepth(self, bankfullDischarge): # bankfull depth (unit: m) # - from the Manning formula # - assuming rectangular channel bankfullDepth = self.manningsN * ((bankfullDischarge)**(0.50)) bankfullDepth = bankfullDepth / (4.8 * ((self.gradient)**(0.50))) bankfullDepth = bankfullDepth**(3.0/5.0) return bankfullDepth def estimateBankfullCapacity(self, width, depth, minWidth = 5.0, minDepth = 1.0): # bankfull capacity (unit: m3) bankfullCapacity = pcr.max(minWidth, width) * \ pcr.max(minDepth, depth) * \ self.channelLength return bankfullCapacity def getElevationProfile(self, iniItems): # get the profile of relative elevation above the floodplain (per grid cell) # output: dictionaries kSlope, mInterval, relZ and floodVolume with the keys iCnt (index, dimensionless) # - nrZLevels : number of intervals/levels # - areaFractions (dimensionless) : percentage/fraction of flooded/innundated area # - relZ (m) : relative elevation above floodplain # - floodVolume (m3) : flood volume above the channel bankfull capacity # - kSlope (dimensionless) : slope used during the interpolation # - mInterval (m3) : smoothing interval (used in the interpolation) msg = 'Get the profile of relative elevation (relZ, unit: m) !!!' logger.info(msg) relativeElevationFileNC = None # TODO define relative elevation files in a netdf file. if relativeElevationFileNC != None: pass # TODO: using a netcdf file if relativeElevationFileNC == None: relZFileName = vos.getFullPath(iniItems.routingOptions['relativeElevationFiles'],\ iniItems.globalOptions['inputDir']) # a dictionary contains areaFractions (dimensionless): fractions of flooded/innundated areas areaFractions = map(float, iniItems.routingOptions['relativeElevationLevels'].split(',')) # number of levels/intervals nrZLevels = len(areaFractions) # - TODO: Read areaFractions and nrZLevels automatically. ######################################################################################################## # # patch elevations: those that are part of sills are updated on the basis of the floodplain gradient # using local distances deltaX per increment upto z[N] and the sum over sills # - fill all lists (including smoothing interval and slopes) relZ = [0.] * nrZLevels for iCnt in range(0, nrZLevels): if relativeElevationFileNC == None: inputName = relZFileName %(areaFractions[iCnt] * 100) relZ[iCnt] = vos.readPCRmapClone(inputName, self.cloneMap, self.tmpDir, self.inputDir) if relativeElevationFileNC != None: pass # TODO: using a netcdf file # covering elevation values relZ[iCnt] = pcr.ifthen(self.landmask, pcr.cover(relZ[iCnt], 0.0)) # make sure that relZ[iCnt] >= relZ[iCnt-1] (added by Edwin) if iCnt > 0: relZ[iCnt] = pcr.max(relZ[iCnt], relZ[iCnt-1]) # - minimum slope of floodplain # being defined as the longest sill, # first used to retrieve longest cumulative distance deltaX = [self.cellArea**0.5] * nrZLevels deltaX[0] = 0.0 sumX = deltaX[:] minSlope = 0.0 for iCnt in range(nrZLevels): if iCnt < nrZLevels-1: deltaX[iCnt] = (areaFractions[iCnt+1]**0.5 - areaFractions[iCnt]**0.5) * deltaX[iCnt] else: deltaX[iCnt] = (1.0 - areaFractions[iCnt-1]**0.5)*deltaX[iCnt] if iCnt > 0: sumX[iCnt] = pcr.ifthenelse(relZ[iCnt] == relZ[iCnt-1], sumX[iCnt-1] + deltaX[iCnt], 0.0) minSlope = pcr.ifthenelse(relZ[iCnt] == relZ[iCnt-1], pcr.max( sumX[iCnt], minSlope), minSlope) # - the maximum value for the floodplain slope is channel gradient (flow velocity is slower in the floodplain) minSlope = pcr.min(self.gradient, 0.5* pcr.max(deltaX[1], minSlope)**-1.) # - add small increment to elevations to each sill (except in the case of lakes, #TODO: verify this) for iCnt in range(nrZLevels): relZ[iCnt] = relZ[iCnt] + sumX[iCnt] * pcr.ifthenelse(relZ[nrZLevels-1] > 0., minSlope, 0.0) # make sure that relZ[iCnt] >= relZ[iCnt-1] (added by Edwin) if iCnt > 0: relZ[iCnt] = pcr.max(relZ[iCnt], relZ[iCnt-1]) # ######################################################################################################## ######################################################################################################## # - set slope and smoothing interval between dy= y(i+1)-y(i) and dx= x(i+1)-x(i) # on the basis of volume # floodVolume = [0.] * (nrZLevels) # volume (unit: m3) mInterval = [0.] * (nrZLevels) # smoothing interval (unit: m3) kSlope = [0.] * (nrZLevels) # slope (dimensionless) # for iCnt in range(1, nrZLevels): floodVolume[iCnt] = floodVolume[iCnt-1] + \ 0.5 * (areaFractions[iCnt] + areaFractions[iCnt-1]) * \ (relZ[iCnt] - relZ[iCnt-1]) * self.cellArea kSlope[iCnt-1] = (areaFractions[iCnt] - areaFractions[iCnt-1])/\ pcr.max(0.001, floodVolume[iCnt] - floodVolume[iCnt-1]) for iCnt in range(1, nrZLevels): if iCnt < (nrZLevels-1): mInterval[iCnt] = 0.5 * self.reductionKK * pcr.min(floodVolume[iCnt+1] - floodVolume[iCnt], \ floodVolume[iCnt] - floodVolume[iCnt-1]) else: mInterval[iCnt] = 0.5 * self.reductionKK *(floodVolume[iCnt] - floodVolume[iCnt-1]) # ######################################################################################################## return nrZLevels, areaFractions, relZ, floodVolume, kSlope, mInterval def getRoutingParamAvgDischarge(self, avgDischarge, dist2celllength = None): # obtain routing parameters based on average (longterm) discharge # output: channel dimensions and # characteristicDistance (for accuTravelTime input) yMean = self.eta * pow (avgDischarge, self.nu ) # avgDischarge in m3/s wMean = self.tau * pow (avgDischarge, self.phi) wMean = pcr.max(wMean,0.01) # average flow width (m) - this could be used as an estimate of channel width (assuming rectangular channels) wMean = pcr.cover(wMean,0.01) yMean = pcr.max(yMean,0.01) # average flow depth (m) - this should NOT be used as an estimate of channel depth yMean = pcr.cover(yMean,0.01) # option to use constant channel width (m) if not isinstance(self.predefinedChannelWidth,types.NoneType):\ wMean = pcr.cover(self.predefinedChannelWidth, wMean) # # minimum channel width (m) wMean = pcr.max(self.minChannelWidth, wMean) return (yMean, wMean) def getCharacteristicDistance(self, yMean, wMean): # Manning's coefficient: usedManningsN = self.manningsN # corrected Manning's coefficient: if self.floodPlain: # wetted perimeter flood_only_wetted_perimeter = self.floodDepth * (2.0) + \ pcr.max(0.0, self.innundatedFraction*self.cellArea/self.channelLength - self.channelWidth) channel_only_wetted_perimeter = \ pcr.min(self.channelDepth, vos.getValDivZero(self.channelStorage, self.channelLength*self.channelWidth, 0.0)) * 2.0 + \ self.channelWidth # total channel wetted perimeter (unit: m) channel_wetted_perimeter = channel_only_wetted_perimeter + \ flood_only_wetted_perimeter # minimum channel wetted perimeter = 10 cm channel_wetted_perimeter = pcr.max(0.1, channel_wetted_perimeter) usedManningsN = ((channel_only_wetted_perimeter/channel_wetted_perimeter) * self.manningsN**(1.5) + \ ( flood_only_wetted_perimeter/channel_wetted_perimeter) * self.floodplainManN**(1.5))**(2./3.) # characteristicDistance (dimensionless) # - This will be used for accutraveltimeflux & accutraveltimestate # - discharge & storage = accutraveltimeflux & accutraveltimestate # - discharge = the total amount of material flowing through the cell (m3/s) # - storage = the amount of material which is deposited in the cell (m3) # characteristicDistance = \ ( (yMean * wMean)/ \ (wMean + 2*yMean) )**(2./3.) * \ ((self.gradient)**(0.5))/ \ usedManningsN * \ vos.secondsPerDay() # meter/day characteristicDistance = \ pcr.max((self.cellSizeInArcDeg)*0.000000001,\ characteristicDistance/self.dist2celllength) # arcDeg/day # charateristicDistance for each lake/reservoir: lakeReservoirCharacteristicDistance = pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., pcr.areaaverage(characteristicDistance, self.WaterBodies.waterBodyIds)) # # - make sure that all outflow will be released outside lakes and reservoirs outlets = pcr.cover(pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyOut) > 0, pcr.boolean(1)), pcr.boolean(0)) distance_to_outlets = pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., pcr.ldddist(self.lddMap, outlets, pcr.scalar(1.0))) #~ lakeReservoirCharacteristicDistance = pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., #~ pcr.max(distance_to_outlets + pcr.downstreamdist(self.lddMap)*1.50, lakeReservoirCharacteristicDistance)) lakeReservoirCharacteristicDistance = pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., pcr.max(distance_to_outlets + pcr.downstreamdist(self.lddMap)*2.50, lakeReservoirCharacteristicDistance)) lakeReservoirCharacteristicDistance = pcr.areamaximum(lakeReservoirCharacteristicDistance, self.WaterBodies.waterBodyIds) # # TODO: calculate lakeReservoirCharacteristicDistance while obtaining lake & reservoir parameters characteristicDistance = pcr.cover(lakeReservoirCharacteristicDistance, characteristicDistance) # PS: In accutraveltime function: # If characteristicDistance (velocity) = 0 then: # - accutraveltimestate will give zero # - accutraveltimeflux will be very high # TODO: Consider to use downstreamdist function. # current solution: using the function "roundup" to ignore # zero and very small values characteristicDistance = \ pcr.roundup(characteristicDistance*100.)/100. # arcDeg/day # and set minimum value of characteristicDistance: characteristicDistance = pcr.cover(characteristicDistance, 0.1*self.cellSizeInArcDeg) characteristicDistance = pcr.max(0.100*self.cellSizeInArcDeg, characteristicDistance) # TODO: check what the minimum distance for accutraveltime function return characteristicDistance def accuTravelTime(self): # accuTravelTime ROUTING OPERATIONS ##############n############################################################################################################ # route only non negative channelStorage (otherwise stay): channelStorageThatWillNotMove = pcr.ifthenelse(self.channelStorage < 0.0, self.channelStorage, 0.0) self.channelStorage = pcr.max(0.0, self.channelStorage) # also at least 1.0 m3 of water will stay - this is to minimize numerical errors due to float_32 pcraster implementations channelStorageThatWillNotMove += self.channelStorage - pcr.rounddown(self.channelStorage) self.channelStorage = pcr.rounddown(self.channelStorage) # channelStorage that will be given to the ROUTING operation: channelStorageForAccuTravelTime = pcr.max(0.0, self.channelStorage) channelStorageForAccuTravelTime = pcr.cover(channelStorageForAccuTravelTime,0.0) # TODO: check why do we have to use the "cover" operation? characteristicDistance = self.getCharacteristicDistance(self.yMean, self.wMean) # estimating channel discharge (m3/day) self.Q = pcr.accutraveltimeflux(self.lddMap,\ channelStorageForAccuTravelTime,\ pcr.max(0.0, characteristicDistance)) self.Q = pcr.cover(self.Q, 0.0) # for very small velocity (i.e. characteristicDistanceForAccuTravelTime), discharge can be missing value. # see: http://sourceforge.net/p/pcraster/bugs-and-feature-requests/543/ # http://karssenberg.geo.uu.nl/tt/TravelTimeSpecification.htm # # and make sure that no negative discharge self.Q = pcr.max(0.0, self.Q) # unit: m3/day # updating channelStorage (after routing) self.channelStorage = pcr.accutraveltimestate(self.lddMap,\ channelStorageForAccuTravelTime,\ pcr.max(0.0, characteristicDistance)) # unit: m3 # return channelStorageThatWillNotMove to channelStorage: self.channelStorage += channelStorageThatWillNotMove # unit: m3 # for non kinematic wave approaches, set subDishcarge Q in m3/s self.subDischarge = self.Q / vos.secondsPerDay() self.subDischarge = pcr.ifthen(self.landmask, self.subDischarge) def estimate_length_of_sub_time_step(self): # estimate the length of sub-time step (unit: s): # - the shorter is the better # - estimated based on the initial or latest sub-time step discharge (unit: m3/s) # length_of_sub_time_step = pcr.ifthenelse(self.subDischarge > 0.0, self.water_height * self.dynamicFracWat * self.cellArea / \ self.subDischarge, vos.secondsPerDay()) # TODO: Check this logic with Rens! # determine the number of sub time steps (based on Rens van Beek's method) # critical_condition = (length_of_sub_time_step < vos.secondsPerDay()) & \ (self.water_height > self.critical_water_height) & \ (self.lddMap != pcr.ldd(5)) # number_of_sub_time_steps = vos.secondsPerDay() /\ pcr.cover( pcr.areaminimum(\ pcr.ifthen(critical_condition, \ length_of_sub_time_step),self.landmask),\ vos.secondsPerDay()/self.limit_num_of_sub_time_steps) number_of_sub_time_steps = 1.25 * number_of_sub_time_steps + 1 number_of_sub_time_steps = pcr.roundup(number_of_sub_time_steps) # number_of_loops = max(1.0, pcr.cellvalue(pcr.mapmaximum(number_of_sub_time_steps),1)[1]) # minimum number of sub_time_steps = 1 number_of_loops = int(max(self.limit_num_of_sub_time_steps, number_of_loops)) # actual length of sub-time step (s) length_of_sub_time_step = vos.secondsPerDay() / number_of_loops return (length_of_sub_time_step, number_of_loops) def simplifiedKinematicWave(self): """ The 'simplifiedKinematicWave': 1. First, assume that all local fluxes has been added to 'channelStorage'. This is done outside of this function/method. 2. Then, the 'channelStorage' is routed by using 'pcr.kinematic function' with 'lateral_inflow' = 0.0. """ ########################################################################################################################## # TODO: REMOVE THIS METHOD AS THIS IS IRRELEVANT. logger.info("Using the simplifiedKinematicWave method ! ") # route only non negative channelStorage (otherwise stay): channelStorageThatWillNotMove = pcr.ifthenelse(self.channelStorage < 0.0, self.channelStorage, 0.0) # channelStorage that will be given to the ROUTING operation: channelStorageForRouting = pcr.max(0.0, self.channelStorage) # unit: m3 # estimate of water height (m) # - needed to estimate the length of sub-time step and # also to estimate the channel wetted area (for the calculation of alpha and dischargeInitial) self.water_height = channelStorageForRouting /\ (pcr.max(self.min_fracwat_for_water_height, self.dynamicFracWat) * self.cellArea) # estimate the length of sub-time step (unit: s): length_of_sub_time_step, number_of_loops = self.estimate_length_of_sub_time_step() for i_loop in range(number_of_loops): msg = "sub-daily time step "+str(i_loop+1)+" from "+str(number_of_loops) logger.info(msg) # alpha parameter and initial discharge variable needed for kinematic wave alpha, dischargeInitial = \ self.calculate_alpha_and_initial_discharge_for_kinematic_wave(channelStorageForRouting, \ self.water_height, \ self.innundatedFraction, self.floodDepth) # at the lake/reservoir outlets, use the discharge of water bofy outflow waterBodyOutflowInM3PerSec = pcr.cover( pcr.ifthen(\ self.WaterBodies.waterBodyOut,\ self.WaterBodies.waterBodyOutflow), 0.0) / vos.secondsPerDay() waterBodyOutflowInM3PerSec = pcr.ifthen(\ pcr.scalar(self.WaterBodies.waterBodyIds) > 0.0, \ waterBodyOutflowInM3PerSec) dischargeInitial = pcr.cover(waterBodyOutflowInM3PerSec, dischargeInitial) # discharge (m3/s) based on kinematic wave approximation #~ logger.debug('start pcr.kinematic') self.subDischarge = pcr.kinematic(self.lddMap, dischargeInitial, 0.0, alpha, self.beta, \ 1, length_of_sub_time_step, self.channelLength) self.subDischarge = pcr.cover(self.subDischarge, 0.0) self.subDischarge = pcr.max(0.0, pcr.cover(self.subDischarge, 0.0)) #~ logger.debug('done') # make sure that we do not get negative channel storage self.subDischarge = pcr.min(self.subDischarge * length_of_sub_time_step, \ pcr.max(0.0, channelStorageForRouting + pcr.upstream(self.lddMap, self.subDischarge * length_of_sub_time_step)))/length_of_sub_time_step # update channelStorage (m3) storage_change_in_volume = pcr.upstream(self.lddMap, self.subDischarge * length_of_sub_time_step) - \ self.subDischarge * length_of_sub_time_step channelStorageForRouting += storage_change_in_volume # # route only non negative channelStorage (otherwise stay): channelStorageThatWillNotMove += pcr.ifthenelse(channelStorageForRouting < 0.0, channelStorageForRouting, 0.0) channelStorageForRouting = pcr.max(0.000, channelStorageForRouting) # update flood fraction and flood depth self.inundatedFraction, self.floodDepth = self.returnInundationFractionAndFloodDepth(channelStorageForRouting) # update dynamicFracWat: fraction of surface water bodies (dimensionless) including lakes and reservoirs # - lake and reservoir surface water fraction self.dynamicFracWat = pcr.cover(\ pcr.min(1.0, self.WaterBodies.fracWat), 0.0) # - fraction of channel (including its excess above bankfull capacity) self.dynamicFracWat += pcr.max(0.0, 1.0 - self.dynamicFracWat) * pcr.max(self.channelFraction, self.innundatedFraction) # - maximum value of dynamicFracWat is 1.0 self.dynamicFracWat = pcr.ifthen(self.landmask, pcr.min(1.0, self.dynamicFracWat)) # estimate water_height for the next loop # - needed to estimate the channel wetted area (for the calculation of alpha and dischargeInitial) self.water_height = channelStorageForRouting / (pcr.max(self.min_fracwat_for_water_height, self.dynamicFracWat) * self.cellArea) # TODO: Check whether the usage of dynamicFracWat provides any problems? # total discharge_volume (m3) until this present i_loop if i_loop == 0: discharge_volume = pcr.scalar(0.0) discharge_volume += self.subDischarge * length_of_sub_time_step # channel discharge (m3/day) = self.Q self.Q = discharge_volume # updating channelStorage (after routing) self.channelStorage = channelStorageForRouting # return channelStorageThatWillNotMove to channelStorage: self.channelStorage += channelStorageThatWillNotMove def update(self,landSurface,groundwater,currTimeStep,meteo): logger.info("routing in progress") # waterBodies: # - get parameters at the beginning of each year or simulation # - note that the following function should be called first, specifically because # we have to define initial conditions at the beginning of simulaution, # if currTimeStep.timeStepPCR == 1: initial_conditions_for_water_bodies = self.getState() self.WaterBodies.getParameterFiles(currTimeStep,\ self.cellArea,\ self.lddMap,\ initial_conditions_for_water_bodies) # the last line is for the initial conditions of lakes/reservoirs if (currTimeStep.doy == 1) and (currTimeStep.timeStepPCR > 1): self.WaterBodies.getParameterFiles(currTimeStep,\ self.cellArea,\ self.lddMap) # if self.includeWaterBodies == False: self.WaterBodies.waterBodyIds = pcr.ifthen(self.landmask, pcr.nominal(-1)) # ignoring all lakes and reservoirs # downstreamDemand (m3/s) for reservoirs # - this one must be called before updating timestepsToAvgDischarge # - estimated based on environmental flow discharge self.downstreamDemand = self.estimate_discharge_for_environmental_flow(self.channelStorage) # get routing/channel parameters/dimensions (based on avgDischarge) # and estimating water bodies fraction ; this is needed for calculating evaporation from water bodies self.yMean, self.wMean = \ self.getRoutingParamAvgDischarge(self.avgDischarge) # channel width (unit: m) self.channelWidth = self.wMean # channel depth (unit: m) self.channelDepth = pcr.max(0.0, self.yMean) # # option to use constant channel depth (m) if not isinstance(self.predefinedChannelDepth, types.NoneType):\ self.channelDepth = pcr.cover(self.predefinedChannelDepth, self.channelDepth) # channel bankfull capacity (unit: m3) if self.floodPlain: if self.usingFixedBankfullCapacity: self.channelStorageCapacity = self.predefinedBankfullCapacity else: self.channelStorageCapacity = self.estimateBankfullCapacity(self.channelWidth, \ self.channelDepth) # fraction of channel (dimensionless) # - mininum inundated fraction self.channelFraction = pcr.max(0.0, pcr.min(1.0,\ self.channelWidth * self.channelLength / (self.cellArea))) # fraction of innundation due to flood (dimensionless) and flood/innundation depth (m) self.innundatedFraction, self.floodDepth = self.returnInundationFractionAndFloodDepth(self.channelStorage) # fraction of surface water bodies (dimensionless) including lakes and reservoirs # - lake and reservoir surface water fraction self.dynamicFracWat = pcr.cover(\ pcr.min(1.0, self.WaterBodies.fracWat), 0.0) # - fraction of channel (including its excess above bankfull capacity) self.dynamicFracWat += pcr.max(0.0, 1.0 - self.dynamicFracWat) * pcr.max(self.channelFraction, self.innundatedFraction) # - maximum value of dynamicFracWat is 1.0 self.dynamicFracWat = pcr.ifthen(self.landmask, pcr.min(1.0, self.dynamicFracWat)) # routing methods if self.method == "accuTravelTime" or self.method == "simplifiedKinematicWave": \ self.simple_update(landSurface, groundwater, currTimeStep, meteo) # if self.method == "kinematicWave": \ self.kinematic_wave_update(landSurface, groundwater, currTimeStep, meteo) # NOTE that this method require abstraction from fossil groundwater. # infiltration from surface water bodies (rivers/channels, as well as lakes and/or reservoirs) to groundwater bodies # - this exchange fluxes will be handed in the next time step # - in the future, this will be the interface between PCR-GLOBWB & MODFLOW (based on the difference between surface water levels & groundwater heads) # self.calculate_exchange_to_groundwater(groundwater, currTimeStep) # volume water released in pits (losses: to the ocean / endorheic basin) self.outgoing_volume_at_pits = pcr.ifthen(self.landmask, pcr.cover( pcr.ifthen(self.lddMap == pcr.ldd(5), self.Q), 0.0)) # TODO: accumulate water in endorheic basins that are considered as lakes/reservoirs # estimate volume of water that can be extracted for abstraction in the next time step self.readAvlChannelStorage = pcr.max(0.0, self.estimate_available_volume_for_abstraction(self.channelStorage)) # old-style reporting self.old_style_routing_reporting(currTimeStep) # TODO: remove this one def calculate_potential_evaporation(self,landSurface,currTimeStep,meteo,definedDynamicFracWat = None): if self.no_zero_crop_water_coefficient == False: self.waterKC = 0.0 # potential evaporation from water bodies # current principle: # - if landSurface.actualET < waterKC * meteo.referencePotET * self.fracWat # then, we add more evaporation # if (currTimeStep.day == 1) or (currTimeStep.timeStepPCR == 1) and self.no_zero_crop_water_coefficient: waterKC = vos.netcdf2PCRobjClone(self.fileCropKC,'kc', \ currTimeStep.fulldate, useDoy = 'month',\ cloneMapFileName = self.cloneMap) self.waterKC = pcr.ifthen(self.landmask,\ pcr.cover(waterKC, 0.0)) self.waterKC = pcr.max(self.minCropWaterKC, self.waterKC) # potential evaporation from water bodies (m/day)) - reduced by evaporation that has been calculated in the landSurface module waterBodyPotEvapOvesSurfaceWaterArea = pcr.ifthen(self.landmask, \ pcr.max(0.0,\ self.waterKC * meteo.referencePotET -\ landSurface.actualET )) # These values are NOT over the entire cell area. # potential evaporation from water bodies over the entire cell area (m/day) if definedDynamicFracWat == None: dynamicFracWat = self.dynamicFracWat waterBodyPotEvap = waterBodyPotEvapOvesSurfaceWaterArea * dynamicFracWat return waterBodyPotEvap def calculate_evaporation(self,landSurface,groundwater,currTimeStep,meteo): # calculate potential evaporation from water bodies OVER THE ENTIRE CELL AREA (m/day) ; not only over surface water bodies self.waterBodyPotEvap = self.calculate_potential_evaporation(landSurface,currTimeStep,meteo) # evaporation volume from water bodies (m3) # - not limited to available channelStorage volLocEvapWaterBody = self.waterBodyPotEvap * self.cellArea # - limited to available channelStorage volLocEvapWaterBody = pcr.min(\ pcr.max(0.0,self.channelStorage), volLocEvapWaterBody) # update channelStorage (m3) after evaporation from water bodies self.channelStorage = self.channelStorage -\ volLocEvapWaterBody self.local_input_to_surface_water -= volLocEvapWaterBody # evaporation (m) from water bodies self.waterBodyEvaporation = volLocEvapWaterBody / self.cellArea self.waterBodyEvaporation = pcr.ifthen(self.landmask, self.waterBodyEvaporation) def calculate_exchange_to_groundwater(self,groundwater,currTimeStep): if self.debugWaterBalance:\ preStorage = self.channelStorage # unit: m3 # riverbed infiltration (m3/day): # # - current implementation based on Inge's principle (later, will be based on groundater head (MODFLOW) and can be negative) # - happening only if 0.0 < baseflow < total_groundwater_abstraction # - total_groundwater_abstraction: from fossil and non fossil # - infiltration rate will be based on aquifer saturated conductivity # - limited to fracWat # - limited to available channelStorage # - this infiltration will be handed to groundwater in the next time step # - References: de Graaf et al. (2014); Wada et al. (2012); Wada et al. (2010) # - TODO: This concept should be IMPROVED. # if groundwater.useMODFLOW: # river bed exchange have been calculated within the MODFLOW (via baseflow variable) self.riverbedExchange = pcr.scalar(0.0) else: riverbedConductivity = groundwater.riverBedConductivity # unit: m/day riverbedConductivity = pcr.min(0.1, riverbedConductivity) # maximum conductivity is 0.1 m/day (as recommended by Marc Bierkens: resistance = 1 day for 0.1 m river bed thickness) total_groundwater_abstraction = pcr.max(0.0, groundwater.nonFossilGroundwaterAbs + groundwater.fossilGroundwaterAbstr) # unit: m self.riverbedExchange = pcr.max(0.0,\ pcr.min(pcr.max(0.0,self.channelStorage),\ pcr.ifthenelse(groundwater.baseflow > 0.0, \ pcr.ifthenelse(total_groundwater_abstraction > groundwater.baseflow, \ riverbedConductivity * self.dynamicFracWat * self.cellArea, \ 0.0), 0.0))) self.riverbedExchange = pcr.cover(self.riverbedExchange, 0.0) factor = 0.25 # to avoid flip flop self.riverbedExchange = pcr.min(self.riverbedExchange, (1.0-factor)*pcr.max(0.0,self.channelStorage)) self.riverbedExchange = pcr.ifthenelse(self.channelStorage < 0.0, 0.0, self.riverbedExchange) self.riverbedExchange = pcr.cover(self.riverbedExchange, 0.0) self.riverbedExchange = pcr.ifthen(self.landmask, self.riverbedExchange) # update channelStorage (m3) after riverbedExchange (m3) self.channelStorage -= self.riverbedExchange self.local_input_to_surface_water -= self.riverbedExchange if self.debugWaterBalance:\ vos.waterBalanceCheck([pcr.scalar(0.0)],\ [self.riverbedExchange/self.cellArea],\ [ preStorage/self.cellArea],\ [ self.channelStorage/self.cellArea],\ 'channelStorage after surface water infiltration',\ True,\ currTimeStep.fulldate,threshold=1e-4) def simple_update(self,landSurface,groundwater,currTimeStep,meteo): # updating timesteps to calculate long and short term statistics values of avgDischarge, avgInflow, avgOutflow, etc. self.timestepsToAvgDischarge += 1. if self.debugWaterBalance:\ preStorage = self.channelStorage # unit: m3 # the following variable defines total local change (input) to surface water storage bodies # unit: m3 # - only local processes; therefore not considering any routing processes self.local_input_to_surface_water = pcr.scalar(0.0) # initiate the variable, start from zero # runoff from landSurface cells (unit: m/day) self.runoff = landSurface.landSurfaceRunoff +\ groundwater.baseflow # update channelStorage (unit: m3) after runoff self.channelStorage += self.runoff * self.cellArea self.local_input_to_surface_water += self.runoff * self.cellArea # update channelStorage (unit: m3) after actSurfaceWaterAbstraction self.channelStorage -= landSurface.actSurfaceWaterAbstract * self.cellArea self.local_input_to_surface_water -= landSurface.actSurfaceWaterAbstract * self.cellArea # reporting channelStorage after surface water abstraction (unit: m3) self.channelStorageAfterAbstraction = pcr.ifthen(self.landmask, self.channelStorage) # return flow from (m) non irrigation water demand # - calculated in the landSurface.py module nonIrrReturnFlowVol = landSurface.nonIrrReturnFlow*self.cellArea self.channelStorage += nonIrrReturnFlowVol self.local_input_to_surface_water += nonIrrReturnFlowVol # water consumption for non irrigation water demand (m) - this water is removed from the system/water balance self.nonIrrWaterConsumption = pcr.max(0.0,\ landSurface.nonIrrGrossDemand - \ landSurface.nonIrrReturnFlow) # calculate evaporation from water bodies - this will return self.waterBodyEvaporation (unit: m) self.calculate_evaporation(landSurface, groundwater, currTimeStep, meteo) if self.debugWaterBalance:\ vos.waterBalanceCheck([self.runoff,\ landSurface.nonIrrReturnFlow],\ [landSurface.actSurfaceWaterAbstract,self.waterBodyEvaporation],\ [ preStorage/self.cellArea],\ [ self.channelStorage/self.cellArea],\ 'channelStorage (unit: m) before lake/reservoir outflow',\ True,\ currTimeStep.fulldate,threshold=5e-3) # LAKE AND RESERVOIR OPERATIONS ########################################################################################################################## if self.debugWaterBalance: \ preStorage = self.channelStorage # unit: m3 # at cells where lakes and/or reservoirs defined, move channelStorage to waterBodyStorage # storageAtLakeAndReservoirs = \ pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., self.channelStorage) storageAtLakeAndReservoirs = pcr.cover(storageAtLakeAndReservoirs,0.0) # # - move only non negative values and use rounddown values storageAtLakeAndReservoirs = pcr.max(0.00, pcr.rounddown(storageAtLakeAndReservoirs)) self.channelStorage -= storageAtLakeAndReservoirs # unit: m3 # update waterBodyStorage (inflow, storage and outflow) self.WaterBodies.update(storageAtLakeAndReservoirs,\ self.timestepsToAvgDischarge,\ self.maxTimestepsToAvgDischargeShort,\ self.maxTimestepsToAvgDischargeLong,\ currTimeStep,\ self.avgDischarge,\ vos.secondsPerDay(),\ self.downstreamDemand) # waterBodyStorage (m3) after outflow: # values given are per water body id (not per cell) self.waterBodyStorage = pcr.ifthen(self.landmask, self.WaterBodies.waterBodyStorage) # transfer outflow from lakes and/or reservoirs to channelStorages waterBodyOutflow = pcr.cover(\ pcr.ifthen(\ self.WaterBodies.waterBodyOut, self.WaterBodies.waterBodyOutflow), 0.0) # unit: m3/day if self.method == "accuTravelTime": # distribute outflow to water body storage # - this is to avoid 'waterBodyOutflow' skipping cells # - this is done by distributing waterBodyOutflow within lake/reservoir cells # waterBodyOutflow = pcr.areaaverage(waterBodyOutflow, self.WaterBodies.waterBodyIds) waterBodyOutflow = pcr.ifthen(\ pcr.scalar(self.WaterBodies.waterBodyIds) > 0.0, waterBodyOutflow) self.waterBodyOutflow = pcr.cover(waterBodyOutflow, 0.0) # unit: m3/day # update channelStorage (m3) after waterBodyOutflow (m3) self.channelStorage += self.waterBodyOutflow # Note that local_input_to_surface_water does not include waterBodyOutflow if self.debugWaterBalance:\ vos.waterBalanceCheck([self.waterBodyOutflow/self.cellArea],\ [storageAtLakeAndReservoirs/self.cellArea],\ [ preStorage/self.cellArea],\ [ self.channelStorage/self.cellArea],\ 'channelStorage (unit: m) after lake reservoir/outflow fluxes (errors here are most likely due to pcraster implementation in float_32)',\ True,\ currTimeStep.fulldate,threshold=1e-3) # ROUTING OPERATION: ########################################################################################################################## # - this will return new self.channelStorage (but still without waterBodyStorage) # - also, this will return self.Q which is channel discharge in m3/day # if self.method == "accuTravelTime": self.accuTravelTime() if self.method == "simplifiedKinematicWave": self.simplifiedKinematicWave() # # # channel discharge (m3/s): for current time step # self.discharge = self.Q / vos.secondsPerDay() self.discharge = pcr.max(0., self.discharge) # reported channel discharge cannot be negative self.discharge = pcr.ifthen(self.landmask, self.discharge) # self.disChanWaterBody = pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0.,\ pcr.areamaximum(self.discharge,self.WaterBodies.waterBodyIds)) self.disChanWaterBody = pcr.cover(self.disChanWaterBody, self.discharge) self.disChanWaterBody = pcr.ifthen(self.landmask, self.disChanWaterBody) # self.disChanWaterBody = pcr.max(0.,self.disChanWaterBody) # reported channel discharge cannot be negative # # ########################################################################################################################## # calculate the statistics of long and short term flow values self.calculate_statistics(groundwater) # return waterBodyStorage to channelStorage self.channelStorage = self.return_water_body_storage_to_channel(self.channelStorage) def calculate_alpha_and_initial_discharge_for_kinematic_wave(self, channelStorage, water_height, innundatedFraction, floodDepth): # calculate alpha (dimensionless), which is the roughness coefficient # - for kinewatic wave (see: http://pcraster.geo.uu.nl/pcraster/4.0.0/doc/manual/op_kinematic.html) # - based on wetted area (m2) and wetted perimeter (m), as well as self.beta (dimensionless) # - assuming rectangular channel # - flood innundated areas with # channel wetted area (m2) # - the minimum wetted area is: water height x channel width (Edwin introduce this) # - channel wetted area is mainly based on channelStorage and channelLength (Rens's approach) channel_wetted_area = water_height * self.channelWidth channel_wetted_area = pcr.max(channel_wetted_area,\ channelStorage / self.channelLength) # wetted perimeter flood_only_wetted_perimeter = floodDepth * (2.0) + \ pcr.max(0.0, innundatedFraction*self.cellArea/self.channelLength - self.channelWidth) channel_only_wetted_perimeter = \ pcr.min(self.channelDepth, vos.getValDivZero(channelStorage, self.channelLength*self.channelWidth, 0.0)) * 2.0 + \ self.channelWidth # total channel wetted perimeter (unit: m) channel_wetted_perimeter = channel_only_wetted_perimeter + \ flood_only_wetted_perimeter # minimum channel wetted perimeter = 10 cm channel_wetted_perimeter = pcr.max(0.1, channel_wetted_perimeter) # corrected Manning's coefficient: if self.floodPlain: usedManningsN = ((channel_only_wetted_perimeter/channel_wetted_perimeter) * self.manningsN**(1.5) + \ ( flood_only_wetted_perimeter/channel_wetted_perimeter) * self.floodplainManN**(1.5))**(2./3.) else: usedManningsN = self.manningsN # alpha (dimensionless) and initial estimate of channel discharge (m3/s) # alpha = (usedManningsN*channel_wetted_perimeter**(2./3.)*self.gradient**(-0.5))**self.beta # dimensionless dischargeInitial = pcr.ifthenelse(alpha > 0.0,\ (channel_wetted_area / alpha)**(1.0/self.beta), 0.0) # unit: m3 return (alpha, dischargeInitial) def calculate_alpha_and_initial_discharge_for_kinematic_wave_OLD(self, channelStorage = None): # calculate alpha (dimensionless), which is the roughness coefficient # - for kinewatic wave (see: http://pcraster.geo.uu.nl/pcraster/4.0.0/doc/manual/op_kinematic.html) # - based on wetted area (m2) and wetted perimeter (m), as well as self.beta (dimensionless) # - assuming rectangular channel # Manning's coefficient: usedManningsN = self.manningsN # channel wetted area (m2) # - alternative 1: based on channelStorage and channelLength (Rens's approach) channel_wetted_area = self.water_height * self.channelWidth # - alternative 2: the minimum wetted are is: water height x channel width (Edwin introduce this) channel_wetted_area = pcr.max(channel_wetted_area,\ channelStorage / self.channelLength) # unit: m2 # channel wetted perimeter (m) channel_wetted_perimeter = 2.0*channel_wetted_area/self.channelWidth + self.channelWidth # unit: m # flood fraction (dimensionless) and flood depth (unit: m) floodFraction = pcr.scalar(0.0) floodDepth = pcr.scalar(0.0) if self.floodPlain: # return flood fraction and flood/innundation depth above the flood plain floodFraction, floodDepth = self.returnInundationFractionAndFloodDepth(channelStorage) # wetted perimeter flood_only_wetted_perimeter = pcr.max(0.0, floodFraction*self.cellArea/self.channelLength - self.channelWidth) + \ floodDepth * (2.0) channel_only_wetted_perimeter = \ self.channelWidth + \ 2.0 * pcr.min(self.channelDepth, channelStorage/(self.channelLength*self.channelWidth)) # channel_wetted_perimeter = channel_only_wetted_perimeter + flood_only_wetted_perimeter # unit: m # corrected Manning's coefficient: usedManningsN = ((channel_only_wetted_perimeter/channel_wetted_perimeter) * self.manningsN**(1.5) + \ ( flood_only_wetted_perimeter/channel_wetted_perimeter) * self.floodplainManN**(1.5))**(2./3.) # alpha (dimensionless) and estimate of channel discharge (m3/s) # alpha = (usedManningsN*channel_wetted_perimeter**(2./3.)*self.gradient**(-0.5))**self.beta # dimensionless dischargeInitial = pcr.ifthenelse(alpha > 0.0,\ (channel_wetted_area / alpha)**(1.0/self.beta), 0.0) # unit: m3 return (alpha, dischargeInitial, floodFraction, floodDepth) def integralLogisticFunction(self,x): # returns a tupple of two values holding the integral of the logistic functions of (x) and (-x) logInt=pcr.ln(pcr.exp(-x)+1) return logInt,x+logInt def returnInundationFractionAndFloodDepth(self, channelStorage): # flood/innundation depth above the flood plain (unit: m) floodDepth = 0.0 # channel and flood innundated fraction (dimensionless, the minimum value is channelFraction) inundatedFraction = self.channelFraction if self.floodPlain: msg = 'Calculate channel inundated fraction and flood inundation depth above the floodplain.' logger.info(msg) # given the flood channel volume: channelStorage # - return the flooded fraction and the associated water height # - using a logistic smoother near intersections (K&K, 2007) # flood/innundation/excess volume (excess above the bankfull capacity, unit: m3) excessVolume = pcr.max(0.0, channelStorage - self.channelStorageCapacity) # find the match on the basis of the shortest distance # to the available intersections or steps # deltaXMin = self.floodVolume[self.nrZLevels-1] y_i = pcr.scalar(1.0) k = [pcr.scalar(0.0)]*2 mInt = pcr.scalar(0.0) for iCnt in range(self.nrZLevels-1,0,-1): # - find x_i for current volume and update match if applicable # also update slope and intercept deltaX = excessVolume - self.floodVolume[iCnt] mask = pcr.abs(deltaX) < pcr.abs(deltaXMin) deltaXMin = pcr.ifthenelse(mask, deltaX, deltaXMin) y_i = pcr.ifthenelse(mask, self.areaFractions[iCnt], y_i) k[0] = pcr.ifthenelse(mask, self.kSlope[iCnt-1], k[0]) k[1] = pcr.ifthenelse(mask, self.kSlope[iCnt], k[1]) mInt = pcr.ifthenelse(mask, self.mInterval[iCnt], mInt) # all values returned, process data: calculate scaled deltaX and smoothed function # on the basis of the integrated logistic functions PHI(x) and 1-PHI(x) # deltaX = deltaXMin deltaXScaled = pcr.ifthenelse(deltaX < 0.,pcr.scalar(-1.),1.)*\ pcr.min(self.criterionKK,pcr.abs(deltaX/pcr.max(1.,mInt))) logInt = self.integralLogisticFunction(deltaXScaled) # compute fractional inundated/flooded area inundatedFraction = pcr.ifthenelse(excessVolume > 0.0,\ pcr.ifthenelse(pcr.abs(deltaXScaled) < self.criterionKK,\ y_i-k[0]*mInt*logInt[0]+k[1]*mInt*logInt[1],\ y_i+pcr.ifthenelse(deltaX < 0.,k[0],k[1])*deltaX), 0.0) # - minimum value is channelFraction inundatedFraction = pcr.max(self.channelFraction, inundatedFraction) # - maximum value is 1.0 inundatedFraction = pcr.max(0.,pcr.min(1.0, inundatedFraction)) # dimensionless # calculate flooded/inundated depth (unit: m) above the floodplain #_- it will be zero if excessVolume == 0 floodDepth = pcr.ifthenelse(inundatedFraction > 0., \ excessVolume/(pcr.max(self.min_fracwat_for_water_height, inundatedFraction)*self.cellArea),0.) # unit: m # - maximum flood depth max_flood_depth = 25.0 floodDepth = pcr.max(0.0, pcr.min(max_flood_depth, floodDepth)) return inundatedFraction, floodDepth def return_water_body_storage_to_channel(self, channelStorage): # return waterBodyStorage to channelStorage # waterBodyStorageTotal = \ pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., pcr.areaaverage(\ pcr.ifthen(self.landmask,self.WaterBodies.waterBodyStorage),\ pcr.ifthen(self.landmask,self.WaterBodies.waterBodyIds)) + \ pcr.areatotal(pcr.cover(\ pcr.ifthen(self.landmask,channelStorage), 0.0),\ pcr.ifthen(self.landmask,self.WaterBodies.waterBodyIds))) waterBodyStoragePerCell = \ waterBodyStorageTotal*\ self.cellArea/\ pcr.areatotal(pcr.cover(\ self.cellArea, 0.0),\ pcr.ifthen(self.landmask,self.WaterBodies.waterBodyIds)) waterBodyStoragePerCell = \ pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., waterBodyStoragePerCell) # unit: m3 # channelStorage = pcr.cover(waterBodyStoragePerCell, channelStorage) # unit: m3 channelStorage = pcr.ifthen(self.landmask, channelStorage) return channelStorage def kinematic_wave_update(self, landSurface,groundwater,currTimeStep,meteo): logger.info("Using the fully kinematic wave method! ") # updating timesteps to calculate long and short term statistics # values of avgDischarge, avgInflow, avgOutflow, etc. self.timestepsToAvgDischarge += 1. # the following variable defines total local change (input) to surface water storage bodies # unit: m3 # - only local processes; therefore not considering any routing processes self.local_input_to_surface_water = pcr.scalar(0.0) # initiate the variable, start from zero # For simplification, surface water abstraction # is done outside the sub daily time steps. # # update channelStorage (unit: m3) after actSurfaceWaterAbstraction self.channelStorage -= landSurface.actSurfaceWaterAbstract * self.cellArea self.local_input_to_surface_water -= landSurface.actSurfaceWaterAbstract * self.cellArea # # reporting channelStorage after surface water abstraction (unit: m3) self.channelStorageAfterAbstraction = pcr.ifthen(self.landmask, self.channelStorage) # return flow from (m) non irrigation water demand # - calculated in the landSurface.py module: landSurface.nonIrrReturnFlow # water consumption for non irrigation water demand (m) - this water is removed from the system/water balance self.nonIrrWaterConsumption = pcr.max(0.0,\ landSurface.nonIrrGrossDemand - \ landSurface.nonIrrReturnFlow) # runoff from landSurface cells (unit: m/day) self.runoff = landSurface.landSurfaceRunoff +\ groundwater.baseflow # values are over the entire cell area # route only non negative channelStorage (otherwise stay): # - note that, the following includes storages in channelStorageThatWillNotMove = pcr.ifthenelse(self.channelStorage < 0.0, self.channelStorage, 0.0) # channelStorage that will be given to the ROUTING operation: channelStorageForRouting = pcr.max(0.0, self.channelStorage) # unit: m3 # estimate of water height (m) # - needed to estimate the length of sub-time step and # also to estimate the channel wetted area (for the calculation of alpha and dischargeInitial) self.water_height = channelStorageForRouting /\ (pcr.max(self.min_fracwat_for_water_height, self.dynamicFracWat) * self.cellArea) # estimate the length of sub-time step (unit: s): length_of_sub_time_step, number_of_loops = self.estimate_length_of_sub_time_step() ####################################################################################################################### for i_loop in range(number_of_loops): msg = "sub-daily time step "+str(i_loop+1)+" from "+str(number_of_loops) logger.info(msg) # initiating accumulated values: if i_loop == 0: acc_local_input_to_surface_water = pcr.scalar(0.0) # unit: m3 acc_water_body_evaporation_volume = pcr.scalar(0.0) # unit: m3 acc_discharge_volume = pcr.scalar(0.0) # unit: m3 if self.debugWaterBalance:\ preStorage = pcr.ifthen(self.landmask,\ channelStorageForRouting) # update channelStorageForRouting after runoff and return flow from non irrigation demand channelStorageForRouting += (self.runoff + landSurface.nonIrrReturnFlow) * \ self.cellArea * length_of_sub_time_step/vos.secondsPerDay() # unit: m3 acc_local_input_to_surface_water += (self.runoff + landSurface.nonIrrReturnFlow) * \ self.cellArea * length_of_sub_time_step/vos.secondsPerDay() # unit: m3 # potential evaporation within the sub-time step ; unit: m, values are over the entire cell area # water_body_potential_evaporation = self.calculate_potential_evaporation(landSurface,currTimeStep,meteo) *\ length_of_sub_time_step/vos.secondsPerDay() # - accumulating potential evaporation if i_loop == 0: self.waterBodyPotEvap = pcr.scalar(0.0) self.waterBodyPotEvap += water_body_potential_evaporation # update channelStorageForRouting after evaporation water_body_evaporation_volume = pcr.min(channelStorageForRouting, \ water_body_potential_evaporation * self.cellArea * length_of_sub_time_step/vos.secondsPerDay()) channelStorageForRouting -= water_body_evaporation_volume acc_local_input_to_surface_water -= water_body_evaporation_volume acc_water_body_evaporation_volume += water_body_evaporation_volume if self.debugWaterBalance:\ vos.waterBalanceCheck([self.runoff * length_of_sub_time_step/vos.secondsPerDay(), \ landSurface.nonIrrReturnFlow * length_of_sub_time_step/vos.secondsPerDay()],\ [water_body_evaporation_volume/self.cellArea],\ [preStorage/self.cellArea],\ [channelStorageForRouting/self.cellArea],\ 'channelStorageForRouting',\ True,\ currTimeStep.fulldate,threshold=5e-5) # the kinematic wave is implemented only for channels (not to lakes and reservoirs) # at cells where lakes and/or reservoirs defined, move channelStorage to waterBodyStorage # storageAtLakeAndReservoirs = \ pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., channelStorageForRouting) storageAtLakeAndReservoirs = pcr.cover(storageAtLakeAndReservoirs,0.0) # # - move only non negative values and use rounddown values storageAtLakeAndReservoirs = pcr.max(0.00, pcr.rounddown(storageAtLakeAndReservoirs)) channelStorageForRouting -= storageAtLakeAndReservoirs # unit: m3 # alpha parameter and initial discharge variable needed for kinematic wave alpha, dischargeInitial = \ self.calculate_alpha_and_initial_discharge_for_kinematic_wave(channelStorageForRouting, \ self.water_height, \ self.innundatedFraction, self.floodDepth) # discharge (m3/s) based on kinematic wave approximation #~ logger.debug('start pcr.kinematic') self.subDischarge = pcr.kinematic(self.lddMap, dischargeInitial, 0.0, alpha, self.beta, \ 1, length_of_sub_time_step, self.channelLength) self.subDischarge = pcr.max(0.0, pcr.cover(self.subDischarge, 0.0)) #~ logger.debug('done') # set discharge to zero for lakes and reservoirs: self.subDischarge = pcr.cover(\ pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0., pcr.scalar(0.0)), self.subDischarge) # make sure that we do not get negative channel storage self.subDischarge = pcr.min(self.subDischarge * length_of_sub_time_step, \ pcr.max(0.0, channelStorageForRouting + pcr.upstream(self.lddMap, self.subDischarge * length_of_sub_time_step)))/length_of_sub_time_step # update channelStorage (m3) storage_change_in_volume = pcr.upstream(self.lddMap, self.subDischarge * length_of_sub_time_step) - self.subDischarge * length_of_sub_time_step channelStorageForRouting += storage_change_in_volume if self.debugWaterBalance:\ vos.waterBalanceCheck([self.runoff * length_of_sub_time_step/vos.secondsPerDay(), \ landSurface.nonIrrReturnFlow * length_of_sub_time_step/vos.secondsPerDay(),\ storage_change_in_volume/self.cellArea],\ [water_body_evaporation_volume/self.cellArea],\ [preStorage/self.cellArea - storageAtLakeAndReservoirs/self.cellArea],\ [channelStorageForRouting/self.cellArea],\ 'channelStorageForRouting (after routing, without lakes/reservoirs)',\ True,\ currTimeStep.fulldate,threshold=5e-4) # lakes and reservoirs: update waterBodyStorage (inflow, storage and outflow) self.WaterBodies.update(storageAtLakeAndReservoirs,\ self.timestepsToAvgDischarge,\ self.maxTimestepsToAvgDischargeShort,\ self.maxTimestepsToAvgDischargeLong,\ currTimeStep,\ self.avgDischarge,\ length_of_sub_time_step,\ self.downstreamDemand) # waterBodyStorage (m3) after outflow: # values given are per water body id (not per cell) self.waterBodyStorage = pcr.ifthen(self.landmask, self.WaterBodies.waterBodyStorage) # transfer outflow from lakes and/or reservoirs to channelStorages waterBodyOutflow = pcr.cover(\ pcr.ifthen(\ self.WaterBodies.waterBodyOut, self.WaterBodies.waterBodyOutflow), 0.0) # unit: m3 # update channelStorage (m3) after waterBodyOutflow (m3) channelStorageForRouting += pcr.upstream(self.lddMap, waterBodyOutflow) # Note that local_input_to_surface_water does not include waterBodyOutflow # at the lake/reservoir outlets, add the discharge of water body outflow waterBodyOutflowInM3PerSec = pcr.ifthen(\ self.WaterBodies.waterBodyOut, self.WaterBodies.waterBodyOutflow) / length_of_sub_time_step self.subDischarge = self.subDischarge + \ pcr.cover(waterBodyOutflowInM3PerSec, 0.0) self.subDischarge = pcr.ifthen(self.landmask, self.subDischarge) # total discharge_volume (m3) until this present i_loop acc_discharge_volume += self.subDischarge * length_of_sub_time_step # return waterBodyStorage to channelStorage channelStorageForRouting = self.return_water_body_storage_to_channel(channelStorageForRouting) # route only non negative channelStorage (otherwise stay): channelStorageThatWillNotMove += pcr.ifthenelse(channelStorageForRouting < 0.0, channelStorageForRouting, 0.0) channelStorageForRouting = pcr.max(0.000, channelStorageForRouting) # update flood fraction and flood depth self.inundatedFraction, self.floodDepth = self.returnInundationFractionAndFloodDepth(channelStorageForRouting) # update dynamicFracWat: fraction of surface water bodies (dimensionless) including lakes and reservoirs # - lake and reservoir surface water fraction self.dynamicFracWat = pcr.cover(\ pcr.min(1.0, self.WaterBodies.fracWat), 0.0) # - fraction of channel (including its excess above bankfull capacity) self.dynamicFracWat += pcr.max(0.0, 1.0 - self.dynamicFracWat) * pcr.max(self.channelFraction, self.innundatedFraction) # - maximum value of dynamicFracWat is 1.0 self.dynamicFracWat = pcr.ifthen(self.landmask, pcr.min(1.0, self.dynamicFracWat)) # estimate water_height for the next loop # - needed to estimate the channel wetted area (for the calculation of alpha and dischargeInitial) self.water_height = channelStorageForRouting / (pcr.max(self.min_fracwat_for_water_height, self.dynamicFracWat) * self.cellArea) # TODO: Check whether the usage of dynamicFracWat provides any problems? ####################################################################################################################### # evaporation (m/day) self.waterBodyEvaporation = water_body_evaporation_volume / self.cellArea # local input to surface water (m3) self.local_input_to_surface_water += acc_local_input_to_surface_water # channel discharge (m3/day) = self.Q self.Q = acc_discharge_volume # updating channelStorage (after routing) self.channelStorage = channelStorageForRouting # return channelStorageThatWillNotMove to channelStorage: self.channelStorage += channelStorageThatWillNotMove # channel discharge (m3/s): for current time step # self.discharge = self.Q / vos.secondsPerDay() self.discharge = pcr.max(0., self.discharge) # reported channel discharge cannot be negative self.discharge = pcr.ifthen(self.landmask, self.discharge) # self.disChanWaterBody = pcr.ifthen(pcr.scalar(self.WaterBodies.waterBodyIds) > 0.,\ pcr.areamaximum(self.discharge,self.WaterBodies.waterBodyIds)) self.disChanWaterBody = pcr.cover(self.disChanWaterBody, self.discharge) self.disChanWaterBody = pcr.ifthen(self.landmask, self.disChanWaterBody) # self.disChanWaterBody = pcr.max(0.,self.disChanWaterBody) # reported channel discharge cannot be negative # calculate the statistics of long and short term flow values self.calculate_statistics(groundwater) def calculate_statistics(self, groundwater): # short term average inflow (m3/s) and long term average outflow (m3/s) from lake and reservoirs self.avgInflow = pcr.ifthen(self.landmask, pcr.cover(self.WaterBodies.avgInflow , 0.0)) self.avgOutflow = pcr.ifthen(self.landmask, pcr.cover(self.WaterBodies.avgOutflow, 0.0)) # short term and long term average discharge (m3/s) # - see: online algorithm on http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance # # - long term average disharge # dishargeUsed = pcr.max(0.0, self.discharge) dishargeUsed = pcr.max(dishargeUsed, self.disChanWaterBody) # deltaAnoDischarge = dishargeUsed - self.avgDischarge self.avgDischarge = self.avgDischarge +\ deltaAnoDischarge/\ pcr.min(self.maxTimestepsToAvgDischargeLong, self.timestepsToAvgDischarge) self.avgDischarge = pcr.max(0.0, self.avgDischarge) self.m2tDischarge = self.m2tDischarge + pcr.abs(deltaAnoDischarge*(dishargeUsed - self.avgDischarge)) # # - short term average disharge # deltaAnoDischargeShort = dishargeUsed - self.avgDischargeShort self.avgDischargeShort = self.avgDischargeShort +\ deltaAnoDischargeShort/\ pcr.min(self.maxTimestepsToAvgDischargeShort, self.timestepsToAvgDischarge) self.avgDischargeShort = pcr.max(0.0, self.avgDischargeShort) # long term average baseflow (m3/s) ; used as proxies for partitioning groundwater and surface water abstractions # baseflowM3PerSec = groundwater.baseflow * self.cellArea / vos.secondsPerDay() deltaAnoBaseflow = baseflowM3PerSec - self.avgBaseflow self.avgBaseflow = self.avgBaseflow +\ deltaAnoBaseflow/\ pcr.min(self.maxTimestepsToAvgDischargeLong, self.timestepsToAvgDischarge) self.avgBaseflow = pcr.max(0.0, self.avgBaseflow) def estimate_discharge_for_environmental_flow(self, channelStorage): # statistical assumptions: # - using z_score from the percentile 90 z_score = 1.2816 #~ # - using z_score from the percentile 95 #~ z_score = 1.645 # long term variance and standard deviation of discharge values varDischarge = self.m2tDischarge / \ pcr.max(1.,\ pcr.min(self.maxTimestepsToAvgDischargeLong, self.timestepsToAvgDischarge)-1.) # see: online algorithm on http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance stdDischarge = pcr.max(varDischarge**0.5, 0.0) # calculate minimum discharge for environmental flow (m3/s) minDischargeForEnvironmentalFlow = pcr.max(0.0, self.avgDischarge - z_score * stdDischarge) factor = 0.10 # to avoid flip flop minDischargeForEnvironmentalFlow = pcr.max(factor*self.avgDischarge, minDischargeForEnvironmentalFlow) # unit: m3/s minDischargeForEnvironmentalFlow = pcr.max(0.0, minDischargeForEnvironmentalFlow) return minDischargeForEnvironmentalFlow def estimate_available_volume_for_abstraction(self, channelStorage, length_of_time_step = vos.secondsPerDay()): # input: channelStorage in m3 # estimate minimum discharge for environmental flow (m3/s) minDischargeForEnvironmentalFlow = self.estimate_discharge_for_environmental_flow(channelStorage) # available channelStorage that can be extracted for surface water abstraction readAvlChannelStorage = pcr.max(0.0,channelStorage) # reduce readAvlChannelStorage if the average discharge < minDischargeForEnvironmentalFlow readAvlChannelStorage *= pcr.min(1.0,\ vos.getValDivZero(pcr.max(0.0, pcr.min(self.avgDischargeShort, self.avgDischarge)), \ minDischargeForEnvironmentalFlow, vos.smallNumber)) # maintaining environmental flow if average discharge > minDischargeForEnvironmentalFlow # TODO: Check why do we need this? readAvlChannelStorage = pcr.ifthenelse(self.avgDischargeShort < minDischargeForEnvironmentalFlow, readAvlChannelStorage, pcr.max(readAvlChannelStorage, \ pcr.max(0.0,\ self.avgDischargeShort - minDischargeForEnvironmentalFlow)*length_of_time_step)) # maximum (precentage) of water can be abstracted from the channel - to avoid flip-flop maximum_percentage = 0.90 readAvlChannelStorage = pcr.min(readAvlChannelStorage, \ maximum_percentage*channelStorage) readAvlChannelStorage = pcr.max(0.0,\ readAvlChannelStorage) # ignore small volume values - less than 0.1 m3 readAvlChannelStorage = pcr.rounddown(readAvlChannelStorage*10.)/10. readAvlChannelStorage = pcr.ifthen(self.landmask, readAvlChannelStorage) return readAvlChannelStorage # unit: m3 def initiate_old_style_routing_reporting(self,iniItems): self.report = True try: self.outDailyTotNC = iniItems.routingOptions['outDailyTotNC'].split(",") self.outMonthTotNC = iniItems.routingOptions['outMonthTotNC'].split(",") self.outMonthAvgNC = iniItems.routingOptions['outMonthAvgNC'].split(",") self.outMonthEndNC = iniItems.routingOptions['outMonthEndNC'].split(",") self.outAnnuaTotNC = iniItems.routingOptions['outAnnuaTotNC'].split(",") self.outAnnuaAvgNC = iniItems.routingOptions['outAnnuaAvgNC'].split(",") self.outAnnuaEndNC = iniItems.routingOptions['outAnnuaEndNC'].split(",") except: self.report = False if self.report == True: # daily output in netCDF files: self.outNCDir = iniItems.outNCDir self.netcdfObj = PCR2netCDF(iniItems) # if self.outDailyTotNC[0] != "None": for var in self.outDailyTotNC: # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_dailyTot.nc",\ var,"undefined") # MONTHly output in netCDF files: # - cummulative if self.outMonthTotNC[0] != "None": for var in self.outMonthTotNC: # initiating monthlyVarTot (accumulator variable): vars(self)[var+'MonthTot'] = None # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_monthTot.nc",\ var,"undefined") # - average if self.outMonthAvgNC[0] != "None": for var in self.outMonthAvgNC: # initiating monthlyTotAvg (accumulator variable) vars(self)[var+'MonthTot'] = None # initiating monthlyVarAvg: vars(self)[var+'MonthAvg'] = None # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_monthAvg.nc",\ var,"undefined") # - last day of the month if self.outMonthEndNC[0] != "None": for var in self.outMonthEndNC: # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_monthEnd.nc",\ var,"undefined") # YEARly output in netCDF files: # - cummulative if self.outAnnuaTotNC[0] != "None": for var in self.outAnnuaTotNC: # initiating yearly accumulator variable: vars(self)[var+'AnnuaTot'] = None # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_annuaTot.nc",\ var,"undefined") # - average if self.outAnnuaAvgNC[0] != "None": for var in self.outAnnuaAvgNC: # initiating annualyVarAvg: vars(self)[var+'AnnuaAvg'] = None # initiating annualyTotAvg (accumulator variable) vars(self)[var+'AnnuaTot'] = None # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_annuaAvg.nc",\ var,"undefined") # - last day of the year if self.outAnnuaEndNC[0] != "None": for var in self.outAnnuaEndNC: # creating the netCDF files: self.netcdfObj.createNetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_annuaEnd.nc",\ var,"undefined") def old_style_routing_reporting(self,currTimeStep): if self.report == True: timeStamp = datetime.datetime(currTimeStep.year,\ currTimeStep.month,\ currTimeStep.day,\ 0) # writing daily output to netcdf files timestepPCR = currTimeStep.timeStepPCR if self.outDailyTotNC[0] != "None": for var in self.outDailyTotNC: self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_dailyTot.nc",\ var,\ pcr2numpy(self.__getattribute__(var),vos.MV),\ timeStamp,timestepPCR-1) # writing monthly output to netcdf files # -cummulative if self.outMonthTotNC[0] != "None": for var in self.outMonthTotNC: # introduce variables at the beginning of simulation or # reset variables at the beginning of the month if currTimeStep.timeStepPCR == 1 or \ currTimeStep.day == 1:\ vars(self)[var+'MonthTot'] = pcr.scalar(0.0) # accumulating vars(self)[var+'MonthTot'] += vars(self)[var] # reporting at the end of the month: if currTimeStep.endMonth == True: self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_monthTot.nc",\ var,\ pcr2numpy(self.__getattribute__(var+'MonthTot'),\ vos.MV),timeStamp,currTimeStep.monthIdx-1) # -average if self.outMonthAvgNC[0] != "None": for var in self.outMonthAvgNC: # only if a accumulator variable has not been defined: if var not in self.outMonthTotNC: # introduce accumulator at the beginning of simulation or # reset accumulator at the beginning of the month if currTimeStep.timeStepPCR == 1 or \ currTimeStep.day == 1:\ vars(self)[var+'MonthTot'] = pcr.scalar(0.0) # accumulating vars(self)[var+'MonthTot'] += vars(self)[var] # calculating average & reporting at the end of the month: if currTimeStep.endMonth == True: vars(self)[var+'MonthAvg'] = vars(self)[var+'MonthTot']/\ currTimeStep.day self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_monthAvg.nc",\ var,\ pcr2numpy(self.__getattribute__(var+'MonthAvg'),\ vos.MV),timeStamp,currTimeStep.monthIdx-1) # # -last day of the month if self.outMonthEndNC[0] != "None": for var in self.outMonthEndNC: # reporting at the end of the month: if currTimeStep.endMonth == True: self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_monthEnd.nc",\ var,\ pcr2numpy(self.__getattribute__(var),vos.MV),\ timeStamp,currTimeStep.monthIdx-1) # writing yearly output to netcdf files # -cummulative if self.outAnnuaTotNC[0] != "None": for var in self.outAnnuaTotNC: # introduce variables at the beginning of simulation or # reset variables at the beginning of the month if currTimeStep.timeStepPCR == 1 or \ currTimeStep.doy == 1:\ vars(self)[var+'AnnuaTot'] = pcr.scalar(0.0) # accumulating vars(self)[var+'AnnuaTot'] += vars(self)[var] # reporting at the end of the year: if currTimeStep.endYear == True: self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_annuaTot.nc",\ var,\ pcr2numpy(self.__getattribute__(var+'AnnuaTot'),\ vos.MV),timeStamp,currTimeStep.annuaIdx-1) # -average if self.outAnnuaAvgNC[0] != "None": for var in self.outAnnuaAvgNC: # only if a accumulator variable has not been defined: if var not in self.outAnnuaTotNC: # introduce accumulator at the beginning of simulation or # reset accumulator at the beginning of the year if currTimeStep.timeStepPCR == 1 or \ currTimeStep.doy == 1:\ vars(self)[var+'AnnuaTot'] = pcr.scalar(0.0) # accumulating vars(self)[var+'AnnuaTot'] += vars(self)[var] # # calculating average & reporting at the end of the year: if currTimeStep.endYear == True: vars(self)[var+'AnnuaAvg'] = vars(self)[var+'AnnuaTot']/\ currTimeStep.doy self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_annuaAvg.nc",\ var,\ pcr2numpy(self.__getattribute__(var+'AnnuaAvg'),\ vos.MV),timeStamp,currTimeStep.annuaIdx-1) # # -last day of the year if self.outAnnuaEndNC[0] != "None": for var in self.outAnnuaEndNC: # reporting at the end of the year: if currTimeStep.endYear == True: self.netcdfObj.data2NetCDF(str(self.outNCDir)+"/"+ \ str(var)+"_annuaEnd.nc",\ var,\ pcr2numpy(self.__getattribute__(var),vos.MV),\ timeStamp,currTimeStep.annuaIdx-1)