HydroGrid.py 10.8 KB
Newer Older
1 2 3 4 5 6 7 8
#
# Reads ans manages the model grid
#
import numpy as np
from netCDF4 import Dataset
# Doc : https://spacetelescope.github.io/spherical_geometry/api/spherical_geometry.polygon.SphericalPolygon.html
from spherical_geometry import polygon
import RPPtools as RPP
9
import os 
10
import sys
11
#
12 13
import getargs
config = getargs.SetupConfig()
14
EarthRadius=config.getfloat("OverAll", "EarthRadius", fallback=6370000.0)
15 16 17 18 19 20 21
#
import getargs
log_master, log_world = getargs.getLogger(__name__)
INFO, DEBUG, ERROR = log_master.info, log_master.debug, log_world.error
INFO_ALL, DEBUG_ALL = log_world.info, log_world.debug
#
def getbox(ncdf, corners) :
22 23 24 25 26 27 28 29 30 31 32 33
    #
    # Check that the domain fits in the data available.
    lonrange=[getattrcontaining(ncdf, "nav_lon", "min"), getattrcontaining(ncdf, "nav_lon", "max")]
    latrange=[getattrcontaining(ncdf, "nav_lat", "min"), getattrcontaining(ncdf, "nav_lat", "max")]
    if len(lonrange[0]) <= 0 :
        lonrange=[np.min(ncdf.variables["nav_lon"][:,:]),np.max(ncdf.variables["nav_lon"][:,:])]
    if len(latrange[0]) <= 0 :
        latrange=[np.min(ncdf.variables["nav_lat"][:,:]),np.max(ncdf.variables["nav_lat"][:,:])]
    if np.min(corners[0][:]) < np.min(lonrange) or \
       np.max(corners[0][:]) > np.max(lonrange) or \
       np.min(corners[1][:]) < np.min(latrange) or \
       np.max(corners[1][:]) > np.max(latrange) :
POLCHER Jan's avatar
POLCHER Jan committed
34
        ERROR("The atmospheric domain does not fit inside the area covered by HydroFile. Please check the file.")
35 36
        sys.exit()
    #
37 38 39 40 41 42
    # Add a few points to tbe box to make sure to cover everything
    halo_pts=2
    ii=[np.argmin(abs(ncdf.variables["nav_lon"][0,:]-np.min(corners[0][:]))),
        np.argmin(abs(ncdf.variables["nav_lon"][0,:]-np.max(corners[0][:])))]
    jj=[np.argmin(abs(ncdf.variables["nav_lat"][:,0]-np.min(corners[1][:]))),
        np.argmin(abs(ncdf.variables["nav_lat"][:,0]-np.max(corners[1][:])))]
43

44
    return min(ii)-halo_pts, max(ii)+halo_pts, min(jj)-halo_pts, max(jj)+halo_pts
45 46 47
#
# Get the corners for a regular lat/lon grid
#
48
def centers(lon, lat) :
49
    jjm,iim = lon.shape
50 51
    #
    index = [[j,i] for i in range(iim) for j in range(jjm)]
52 53
    centersll = [[lon[j,i], lat[j,i]] for j,i in index]
    return index, centersll
54

55 56 57
def corners(subcentersll, hdlon, hdlat) :
    cornersll = [RPP.boxit(np.array([lo, la]), hdlon, hdlat, 2) for lo,la in subcentersll]
    Llats = [[0.0000001 if p[1] == 0 else np.sign(p[1])*89.99 if np.abs(p[1]) == 90 else p[1] for p in boxll] for boxll in cornersll]
58
    Llons = [[0.0000001 if p[0] == 0 else np.sign(p[0])*89.99 if np.abs(p[0]) == 90 else p[0] for p in boxll] for boxll in cornersll]
59 60
    cornerspoly = [ polygon.SphericalPolygon.from_lonlat(lons, lats, center=cent) for lons, lats, cent in zip(Llons, Llats, subcentersll)]
    #radiusll = [RPP.maxradius(np.array(cent), np.array(lons), np.array(lats)) for cent, lons, lats in zip(subcentersll,Llons,Llats)]
61
    #
62
    return cornersll, cornerspoly
63
#
64
def gather(x, index, default = 0) :
65 66
    y=[]
    for ia in index :
67
      try:
68 69 70 71
        if (ia[:,0] == [-1, -1]).all():
            y.append([default])
        else:
            y.append(list(x[ia[0,i],ia[1,i]] for i in range(ia.shape[1]) ))
72 73 74
      except:
        print("Error - the hydrological file may not recover all the domain", ia)
        sys.exit()
75 76 77 78 79 80 81 82 83
    return y
#
def getattrcontaining(nc, varname, substr) :
    att=[]
    for s in nc.variables[varname].ncattrs() :
        if s.lower().find(substr) >= 0 :
            att.append(nc.variables[varname].getncattr(s))
    return att
#
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
def calctopoindex(nf, istr, iend, jstr, jend, index, hydrogrid) :
    # The routing code in Fortran (i=1, j=2)
    # 8 --- 1 --- 2
    # |     |     |
    # 7 --- X --- 3
    # |     |     |
    # 6 --- 5 --- 4
    #
    inc = np.array([[0,1,1,1,0,-1,-1,-1],[-1,-1,0,1,1,1,0,-1]])
    elevation_precision=0.1
    #
    jlen,ilen = nf.variables["orog"].shape
    missing = nf.variables["orog"].missing_value
    land = np.where(nf.variables["orog"][jstr:jend,istr:iend] < missing)
    topoind=np.zeros((jend-jstr,iend-istr))
    topoind[:,:] = np.nan
    #
    for j,i in zip(land[:][0],land[:][1]) :
        trip = int(nf.variables["trip"][jstr+j,istr+i]-1)
        if trip < 8 :
            inf=(istr+i+inc[0,trip])%ilen
            jnf=(jstr+j+inc[1,trip])%jlen
            dz=max(elevation_precision,nf.variables["orog"][jstr+j,istr+i]-nf.variables["orog"][jnf,inf])
            dl=nf.variables["disto"][jstr+j,istr+i]-nf.variables["disto"][jnf,inf]
        else :
POLCHER Jan's avatar
POLCHER Jan committed
109 110 111 112 113 114
            if trip > 97 :
                # Going into the ocean which is at elevation 0
                dz = max(elevation_precision,nf.variables["orog"][jstr+j,istr+i])
            else :
                # Endorehic basin so we use thte lowest dz possible.
                dz = elevation_precision
115
            #
POLCHER Jan's avatar
POLCHER Jan committed
116 117 118 119 120 121
            if nf.variables["disto"][jstr+j,istr+i] > 0 :
                dl = nf.variables["disto"][jstr+j,istr+i]
            else :
                # Some other reasonable assumption
                rx,ry = hydrogrid.resolution(i,j)
                dl = (rx+ry)*0.5*0.5
122 123 124 125 126 127 128
        topoind[j,i]=np.sqrt(dl**3./(dz*1.0e6))
        #
    minitopo = np.nanmin(topoind)
    if minitopo <= np.finfo(float).eps :
        ERROR("Topoind close to zero encoutered.")
        sys.exit()
    #
129
    return gather(topoind, index, default = 10), "Topographic index which serves for the residence time", minitopo
130
#
131
class HydroGrid :
132
    def __init__(self, lolacorners, wfile) :
133
        #
134 135 136
        self.source=config.get("OverAll", "HydroFile")
        INFO("Opening in HydroGrid : "+self.source)
        self.ncfile=Dataset(self.source,'r')
137
        istr, iend, jstr, jend = getbox(self.ncfile, lolacorners)
138 139
        self.hdlon = np.mean(np.abs(np.diff(self.ncfile.variables["nav_lon"][0,:])))
        self.hdlat = np.mean(np.abs(np.diff(self.ncfile.variables["nav_lat"][:,0])))
140
        self.hd = max(self.hdlon,self.hdlat)
141 142 143 144 145
        # In case the box is on the edge of the routing.nc file
        if istr<0:
            istr = 0  
        if iend >=self.ncfile.variables["nav_lon"][0,:].shape[0]: 
            iend = self.ncfile.variables["nav_lon"][0,:].shape[0]
146 147 148 149
        if jstr<0:
            jstr = 0
        if iend >=self.ncfile.variables["nav_lat"][0,:].shape[0]:
            iend = self.ncfile.variables["nav_lat"][0,:].shape[0]
150
        self.box=[istr, iend, jstr, jend]   
151 152 153
        self.lon=np.copy(self.ncfile.variables["nav_lon"][jstr:jend,istr:iend])
        self.lat=np.copy(self.ncfile.variables["nav_lat"][jstr:jend,istr:iend])
        self.jjm,self.iim = self.lon.shape
154 155 156 157
        DEBUG("# Dimensions :"+str(self.iim)+" -- "+str(self.jjm))
        if self.iim > 0 and self.jjm > 0 :
            DEBUG("# Range Lon :"+str(np.min(self.lon))+" -- "+str(np.max(self.lon)))
            DEBUG("# Range Lat :"+str(np.min(self.lat))+" -- "+str(np.max(self.lat)))
158
        #
159 160
        if wfile is None or not os.path.exists(wfile):
            self.index, self.centers = centers(self.lon, self.lat) 
161 162

    def select(self, c, r) :
163
        indices = [i for i in range(len(self.centers)) if (RPP.loladist(np.array(c),np.array(self.centers[i])) <= r+1.1*self.hd)]
164 165 166
        subcenters = [self.centers[i] for i in indices]
        polyll, polylist = corners(subcenters, self.hdlon, self.hdlat)
        return indices, polylist
167

168 169 170 171 172 173 174 175 176 177 178 179 180
    def resolution(self,i,j) :
        if i+1 >= self.iim or j+1 >= self.jjm :
            inx=i-1; jnx=j-1
        else :
            inx=i+1; jnx=j+1
        dlon = np.radians(np.abs(self.lon[j,i]-self.lon[j,inx]))
        alon = np.cos(np.radians(self.lat[j,i]))**2*(np.sin(dlon/2))**2
        clon = 2*np.arctan2(np.sqrt(alon), np.sqrt(1-alon))
        dlat = np.radians(np.abs(self.lat[j,i]-self.lat[jnx,i]))
        alat = (np.sin(dlat/2))**2
        clat = 2*np.arctan2(np.sqrt(alat), np.sqrt(1-alat))
        return EarthRadius*clon, EarthRadius*clat
    
181
class HydroData :
182
    def __init__(self, nf, box, index, part, hydrogrid) :
183
        istr, iend, jstr, jend = box[:]
184 185 186
        #
        # Flow direction
        #
187
        self.trip=gather(nf.variables["trip"][jstr:jend,istr:iend].astype(np.float32), index, default = 98)
188 189
        self.tripdesc=nf.variables["trip"].long_name
        #
190 191
        # ID of basin
        #
192
        self.basins=gather(nf.variables["basins"][jstr:jend,istr:iend], index, 999)
193 194 195 196 197
        self.basinsdesc=nf.variables["basins"].long_name
        att = getattrcontaining(nf, "basins", "max")
        if len(att) > 0 :
            self.basinsmax=att[0]
        else :
198
            INFO("We need to scan full file to find the largest basin ID")
199
            # This variable seems not to be used further
200 201 202
            ##self.basinsmax = part.domainmax(np.ma.max(np.ma.masked_where(self.basins < 1.e10, self.basins)))
            ##self.basinsmax = part.domainmax(np.max(np.array(self.basins)[np.array(self.basins) < missing]))
            self.basinsmax = 999
203
        #
204
        # Distance to ocean
205
        #
206
        self.disto=gather(nf.variables["disto"][jstr:jend,istr:iend].astype(np.float32), index, default = 500)
207 208
        self.distodesc=nf.variables["disto"].long_name
        #
209 210
        # Flow accumulation
        #
211
        self.fac=gather(nf.variables["fac"][jstr:jend,istr:iend].astype(np.float32), index, default = 0)
212
        self.facdesc=nf.variables["fac"].long_name
213
        #
214 215 216
        # If Orography is present in the file then we can compute the topographic index.
        # Else the topographic index needs to be present in the file.
        #
POLCHER Jan's avatar
POLCHER Jan committed
217
        if "orog" in nf.variables.keys() and "disto" in nf.variables.keys() :
218
            self.orog = gather(nf.variables["orog"][jstr:jend,istr:iend].astype(np.float32), index, default = 0)
219
            self.orogdesc=nf.variables["orog"].long_name
220 221
            self.topoind, self.topoinddesc, mintopo = calctopoindex(nf, istr, iend, jstr, jend, index, hydrogrid)
            self.topoindmin = part.domainmin(mintopo)
222
        elif "topoind" in nf.variables.keys() and "orog" in nf.variables.keys() :
223
            self.orog = gather(np.zeros((jend-jstr,iend-istr)).astype(np.float32), index)
224
            self.topoind=gather(nf.variables["topoind"][jstr:jend,istr:iend].astype(np.float32), index, default = 10)
225 226 227 228 229 230 231
            self.topoinddesc=nf.variables["topoind"].long_name
            att = getattrcontaining(nf, "topoind", "min")
            if len(att) > 0 :
                self.topoindmin=att[0]
            else :
                INFO("We need to scan full file to find minimum topoind over domain")
                self.topoindmin=np.min(np.where(nf.variables["topoind"][:,:] <  1.e15))
POLCHER Jan's avatar
POLCHER Jan committed
232 233 234
        else :
            ERROR("Not enough information in the HydroFile in order to compute topographic residence time.")
            sys.exit()
235
        #
236 237
        #
        if "floodplains" in nf.variables.keys():
238
            self.floodplains = gather(nf.variables["floodplains"][jstr:jend,istr:iend].astype(np.float32), index, 0)
239 240 241
            self.floodplainsdesc=nf.variables["floodplains"].long_name
        else:
            self.floodplains = gather(np.zeros((jend-jstr,iend-istr)).astype(np.float32), index)