[7881] | 1 | import datetime, numpy as np, netCDF4, sys |
---|
| 2 | from grid import Grid |
---|
| 3 | from cwt import cwt_kg31to3 |
---|
| 4 | from basics import printStat, writePFTMAP |
---|
| 5 | debug = False |
---|
| 6 | |
---|
| 7 | path_orc = "/home/surface5/vbastri/BDD/ESACCI-LC/ORCHIDEE_NEW/0.25/PFTmap_2010.nc" |
---|
| 8 | path_luh = "/home/surface5/vbastri/BDD/LUH2/GCB2022/states.nc" |
---|
| 9 | path_kg = "/home/surface5/vbastri/BDD/KG2017/KG_720x1440.nc" |
---|
| 10 | path_c4 = "/home/surface5/vbastri/BDD/C4FRAC/C4frac_720x1440.nc" |
---|
| 11 | path_wb = "/home/surface5/vbastri/BDD/ESACCI-LC/WATER/WB_720x1440.nc" |
---|
| 12 | path_out = "netcdf/PFTmap_%(year)s.nc" |
---|
| 13 | luh_year0 = 850 |
---|
| 14 | merge_year = 2010 |
---|
| 15 | build_years = np.arange(850, 2022+1) |
---|
| 16 | |
---|
| 17 | atts = dict(description = "ORCHIDEE PFT map derived from ESACCI Land Cover map v2.0.7b for 2010, aggregated using user-tool v4.3, cross-walking table v2.4, C3/C4 for grasses from C.Still (May 2018), C3/C4 for crops from LUH2-GCB2022, followed by historical reconstrution using G. Hurtt data LUH2-GCB2022 with merging rules v2.0", |
---|
| 18 | contact = "V.Bastrikov (vladislav.bastrikov@lsce.ipsl.fr), P.Peylin (peylin@lsce.ipsl.fr)", |
---|
| 19 | web = "https://orchidas.lsce.ipsl.fr/dev/lccci/", |
---|
| 20 | date = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")) |
---|
| 21 | |
---|
| 22 | # ORCHIDEE PFT indices |
---|
| 23 | BS = 0 |
---|
| 24 | TREBF = 1 |
---|
| 25 | TRDBF = 2 |
---|
| 26 | TEENF = 3 |
---|
| 27 | TEEBF = 4 |
---|
| 28 | TEDBF = 5 |
---|
| 29 | BOENF = 6 |
---|
| 30 | BODBF = 7 |
---|
| 31 | BODNF = 8 |
---|
| 32 | TEGR = 9 |
---|
| 33 | C4GR = 10 |
---|
| 34 | C3CR = 11 |
---|
| 35 | C4CR = 12 |
---|
| 36 | TRGR = 13 |
---|
| 37 | BOGR = 14 |
---|
| 38 | |
---|
| 39 | # LUH class indices |
---|
| 40 | PRIMF = 0 |
---|
| 41 | PRIMN = 1 |
---|
| 42 | SECDF = 2 |
---|
| 43 | SECDN = 3 |
---|
| 44 | URBAN = 4 |
---|
| 45 | C3ANN = 5 |
---|
| 46 | C4ANN = 6 |
---|
| 47 | C3PER = 7 |
---|
| 48 | C4PER = 8 |
---|
| 49 | C3NFX = 9 |
---|
| 50 | PASTR = 10 |
---|
| 51 | RANGE = 11 |
---|
| 52 | ICWTR = 12 |
---|
| 53 | LUH_LIST = ["primf", "primn", "secdf", "secdn", "urban", "c3ann", "c4ann", "c3per", "c4per", "c3nfx", "pastr", "range"] |
---|
| 54 | |
---|
| 55 | # Function to safely divide two streams: if denominator equals zero, division result is set to zero |
---|
| 56 | # During division denominator zeros are changed to units, so that python does not warn on errors |
---|
| 57 | def divide(num, den): return np.where(den == 0, 0, num / np.where(den == 0, 1, den)) |
---|
| 58 | |
---|
| 59 | # Function to make plots for data streams |
---|
| 60 | def plot(**kwargs): |
---|
| 61 | for name, data in kwargs.items(): |
---|
| 62 | for idx, item in enumerate(data if len(data.shape)>2 else [data]): |
---|
| 63 | grid.plotmap(np.ma.masked_where(item==0, item), dpi=150, vmin=0, vmax=1, |
---|
| 64 | filename="%s%s.png" % (name.lower(), idx if len(data.shape)>2 else ""), title="%s%s" % (name.upper(), idx if len(data.shape)>2 else ""), |
---|
| 65 | info="min=%.3g\nmax=%.3g\nmean=%.3g\nnzpts=%s" % (item.min(), item.max(), grid.meanmask(np.ma.masked_where(item==0, item)), (item>0).sum())) |
---|
| 66 | |
---|
| 67 | # Function to extend land by 1 pixel outside land-sea mask |
---|
| 68 | def extend(veg): |
---|
| 69 | newveg = np.ma.masked_all((len(veg), len(lat), len(lon))) |
---|
| 70 | loc_area = np.where(veg[0].mask == False, grid.area, 0) |
---|
| 71 | wveg = veg * loc_area |
---|
| 72 | for ilat in range(len(lat)): |
---|
| 73 | for ilon in range(len(lon)): |
---|
| 74 | if veg[0,ilat,ilon] is not np.ma.masked: |
---|
| 75 | newveg[:,ilat,ilon] = veg[:,ilat,ilon] |
---|
| 76 | else: |
---|
| 77 | if wveg[:, max(0, ilat-1) : ilat+2, max(0, ilon-1) : ilon+2].count() == 0: continue |
---|
| 78 | newveg[:,ilat,ilon] = wveg[:, max(0, ilat-1) : ilat+2, max(0, ilon-1) : ilon+2].sum(axis=-1).sum(axis=-1) / loc_area[max(0, ilat-1) : ilat+2, max(0, ilon-1) : ilon+2].sum() |
---|
| 79 | return newveg |
---|
| 80 | |
---|
| 81 | # Read present-day ORCHIDEE map to be merged with LUH |
---|
| 82 | print("\nREAD ORCHIDEE") |
---|
| 83 | nc = netCDF4.Dataset(path_orc) |
---|
| 84 | lat = nc.variables["lat"][:] |
---|
| 85 | lon = nc.variables["lon"][:] |
---|
| 86 | orc = nc.variables["maxvegetfrac"][0].astype("f8").filled(0.) |
---|
| 87 | nc.close() |
---|
| 88 | printStat(orc=orc) |
---|
| 89 | orc = divide(orc, orc.sum(axis=0)) |
---|
| 90 | printStat(orcn=orc) |
---|
| 91 | grid = Grid(lat=lat, lon=lon) |
---|
| 92 | if debug: plot(orc=orc, orcsum=orc.sum(axis=0)) |
---|
| 93 | |
---|
| 94 | # Read Koeppen-Geiger map with 31 classes |
---|
| 95 | print("\nREAD KG") |
---|
| 96 | nc = netCDF4.Dataset(path_kg) |
---|
| 97 | if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit() |
---|
| 98 | kg31 = nc.variables["kg"][:].astype("f8").filled(0) |
---|
| 99 | nc.close() |
---|
| 100 | printStat(kg31=kg31) |
---|
| 101 | kg31 = divide(kg31, kg31.sum(axis=0)) |
---|
| 102 | printStat(kg31n=kg31) |
---|
| 103 | |
---|
| 104 | # Aggregate KG into 3 groups |
---|
| 105 | kg = np.zeros((len(cwt_kg31to3[0])-1, len(lat), len(lon))) |
---|
| 106 | for idx, rule in enumerate(cwt_kg31to3[1:]): kg = kg + kg31[idx] * np.array(rule[1:])[:,None,None] |
---|
| 107 | printStat(kg3=kg) |
---|
| 108 | if debug: plot(kg=kg) |
---|
| 109 | # Fill KG where undefined with the dominant class across each latitude (or last class if no data in a latitude) |
---|
| 110 | domclass = np.where(kg.sum(axis=(0,2)) == 0, len(kg)-1, kg.sum(axis=2).argmax(axis=0)) |
---|
| 111 | for idx in range(len(lat)): kg[domclass[idx], idx, kg[:,idx,:].sum(axis=0) == 0] = 1 |
---|
| 112 | printStat(kg3f=kg) |
---|
| 113 | if debug: plot(kgf=kg) |
---|
| 114 | |
---|
| 115 | print("\nREAD C4") |
---|
| 116 | nc = netCDF4.Dataset(path_c4) |
---|
| 117 | if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit() |
---|
| 118 | c4 = nc.variables["C4frac"][:].astype("f8").filled(0) |
---|
| 119 | nc.close() |
---|
| 120 | printStat(c4=c4) |
---|
| 121 | if debug: plot(c4=c4) |
---|
| 122 | |
---|
| 123 | print("\nREAD WB") |
---|
| 124 | nc = netCDF4.Dataset(path_wb) |
---|
| 125 | if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit() |
---|
| 126 | wb = nc.variables["wb"][:].astype("f8").filled(0) |
---|
| 127 | nc.close() |
---|
| 128 | printStat(wb=wb) |
---|
| 129 | lsm = np.where(wb[1] > 0.001, 1, 0) |
---|
| 130 | print("land pts=%s" % lsm.sum()) |
---|
| 131 | if debug: plot(wb=wb, lsm=lsm) |
---|
| 132 | |
---|
| 133 | # Read LUH for the merging year |
---|
| 134 | print("\nREAD LUH") |
---|
| 135 | nc = netCDF4.Dataset(path_luh) |
---|
| 136 | if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit() |
---|
| 137 | luh = np.zeros((len(LUH_LIST)+1, len(lat), len(lon))) # array for all LUH maps plus ICWTR |
---|
| 138 | for idx, item in enumerate(LUH_LIST): luh[idx] = nc.variables[item][merge_year-luh_year0].astype("f8") # do not fill with 0 to calculate icwtr over land (ice and water) |
---|
| 139 | luh[ICWTR] = np.clip(1 - luh.sum(axis=0), 0, 1) |
---|
| 140 | luh = np.ma.masked_invalid(luh).filled(0) |
---|
| 141 | luh = divide(luh, luh.sum(axis=0)) |
---|
| 142 | nc.close() |
---|
| 143 | printStat(luh=luh) |
---|
| 144 | if debug: plot(luh=luh, luhsum=luh.sum(axis=0)) |
---|
| 145 | |
---|
| 146 | onlyorc = np.where((orc.sum(axis=0) > 0) & (luh.sum(axis=0) == 0), 1, 0) |
---|
| 147 | onlyluh = np.where((orc.sum(axis=0) == 0) & (luh.sum(axis=0) > 0), 1, 0) |
---|
| 148 | print("ORC>0 LUH=0", onlyorc.sum()) |
---|
| 149 | print("ORC=0 LUH>0", onlyluh.sum()) |
---|
| 150 | if debug: plot(onlyorc=onlyorc, onlyluh=onlyluh) |
---|
| 151 | |
---|
| 152 | print("\nBUILD FRACTIONS") |
---|
| 153 | luh_C3 = luh[C3ANN] + luh[C3PER] + luh[C3NFX] |
---|
| 154 | luh_C4 = luh[C4ANN] + luh[C4PER] |
---|
| 155 | luh_ant = luh_C3 + luh_C4 + luh[PASTR] |
---|
| 156 | |
---|
| 157 | orc_gr = orc[TEGR] + orc[TRGR] + orc[BOGR] + orc[C4GR] |
---|
| 158 | orc_cr = orc[C3CR] + orc[C4CR] |
---|
| 159 | gr_ant = np.clip(luh_ant - orc_cr, 0, np.inf) |
---|
| 160 | gr_nat = np.clip(orc_gr - gr_ant, 0, np.inf) |
---|
| 161 | gr_tr = gr_nat * divide(orc[TRGR], orc_gr) |
---|
| 162 | gr_te = gr_nat * divide(orc[TEGR], orc_gr) |
---|
| 163 | gr_bo = gr_nat * divide(orc[BOGR], orc_gr) |
---|
| 164 | gr_C4 = gr_nat * divide(orc[C4GR], orc_gr) |
---|
| 165 | total_nat = orc[BS] + orc[TREBF] + orc[TRDBF] + orc[TEENF] + orc[TEEBF] + orc[TEDBF] + orc[BOENF] + orc[BODBF] + orc[BODNF] + gr_tr + gr_te + gr_bo + gr_C4 |
---|
| 166 | |
---|
| 167 | frac = divide(orc, total_nat) |
---|
| 168 | frac[TRGR] = divide(gr_tr, total_nat) |
---|
| 169 | frac[TEGR] = divide(gr_te, total_nat) |
---|
| 170 | frac[BOGR] = divide(gr_bo, total_nat) |
---|
| 171 | frac[C4GR] = divide(gr_C4, total_nat) |
---|
| 172 | frac[C3CR] = 0 |
---|
| 173 | frac[C4CR] = 0 |
---|
| 174 | printStat(frac=frac) |
---|
| 175 | print("ORC>0 FRAC=0", ((orc.sum(axis=0) > 0) & (frac.sum(axis=0) == 0)).sum()) # these are pixels with crops in ORCHIDEE (161) |
---|
| 176 | print("LUH>0 FRAC=0", ((luh.sum(axis=0) > 0) & (frac.sum(axis=0) == 0)).sum()) # these are mostly ICWTR pixels in LUH, few as PRIMN (361) |
---|
| 177 | print("ORC>0 LUH>0 FRAC=0", ((orc.sum(axis=0) > 0) & (luh.sum(axis=0) > 0) & (frac.sum(axis=0) == 0)).sum()) # 40 |
---|
| 178 | if debug: plot(frac=frac, fracsum=frac.sum(axis=0)) |
---|
| 179 | |
---|
| 180 | nc = netCDF4.Dataset(path_luh) |
---|
| 181 | for year in build_years: |
---|
| 182 | print("\nREAD LUH FOR YEAR %s" % year) |
---|
| 183 | yluh = np.zeros((len(LUH_LIST)+1, len(lat), len(lon))) |
---|
| 184 | for idx, item in enumerate(LUH_LIST): yluh[idx] = nc.variables[item][year-luh_year0].astype("f8") |
---|
| 185 | yluh[ICWTR] = np.clip(1 - yluh.sum(axis=0), 0, 1) |
---|
| 186 | yluh = np.ma.masked_invalid(yluh).filled(0) |
---|
| 187 | yluh = divide(yluh, yluh.sum(axis=0)) |
---|
| 188 | printStat(yluh=yluh) |
---|
| 189 | |
---|
| 190 | yluh_nat = yluh[PRIMF] + yluh[PRIMN] + yluh[SECDF] + yluh[SECDN] + yluh[RANGE] + yluh[URBAN] + yluh[ICWTR] |
---|
| 191 | yluh_C3 = yluh[C3ANN] + yluh[C3PER] + yluh[C3NFX] |
---|
| 192 | yluh_C4 = yluh[C4ANN] + yluh[C4PER] |
---|
| 193 | |
---|
| 194 | print("\nBUILD ORC FOR YEAR %s" % year) |
---|
| 195 | yorc = frac * yluh_nat |
---|
| 196 | yorc[TRGR] = yorc[TRGR] + kg[0] * (1 - c4) * yluh[PASTR] |
---|
| 197 | yorc[TEGR] = yorc[TEGR] + kg[1] * (1 - c4) * yluh[PASTR] |
---|
| 198 | yorc[BOGR] = yorc[BOGR] + kg[2] * (1 - c4) * yluh[PASTR] |
---|
| 199 | yorc[C4GR] = yorc[C4GR] + c4 * yluh[PASTR] |
---|
| 200 | yorc[C3CR] = yluh_C3 |
---|
| 201 | yorc[C4CR] = yluh_C4 |
---|
| 202 | printStat(yorc=yorc) |
---|
| 203 | print("0<YORC<0.999", ((yorc.sum(axis=0)>0) & (yorc.sum(axis=0)<0.999)).sum()) |
---|
| 204 | print("ORC>0 LUH>0 YORC<0.999", ((orc.sum(axis=0)>0) & (luh.sum(axis=0)>0) & (yorc.sum(axis=0)<0.999)).sum()) |
---|
| 205 | print("ORC>0 LUH=0 YORC=0", ((orc.sum(axis=0)>0) & (luh.sum(axis=0)==0) & (yorc.sum(axis=0)==0)).sum()) |
---|
| 206 | print("ORC>0 YORC<0.999", ((orc.sum(axis=0)>0) & (yorc.sum(axis=0)<0.999)).sum()) |
---|
| 207 | if debug: plot(yorc=yorc) |
---|
| 208 | |
---|
| 209 | # POST PROCESS |
---|
| 210 | # - drop pixels where ORC has no info |
---|
| 211 | # - use data from ORC where LUH has no info or problematic balance |
---|
| 212 | # - drop fractions below 0.001 |
---|
| 213 | # - normalize to 1 |
---|
| 214 | # - if OCEAN>0.999 apply mask |
---|
| 215 | # - filter out Antarctica (lat < S60) |
---|
| 216 | yorc = np.where((orc.sum(axis=0)==0), 0, yorc) |
---|
| 217 | yorc = np.where((orc.sum(axis=0)>0) & (yorc.sum(axis=0)<0.999), orc, yorc) |
---|
| 218 | yorc = np.where(yorc < 0.001, 0, yorc) |
---|
| 219 | yorc = divide(yorc, yorc.sum(axis=0)) |
---|
| 220 | yorc = np.ma.where(wb[0] < 0.999, yorc, np.ma.masked) |
---|
| 221 | yorc[:, lat < -60, :] = np.ma.masked |
---|
| 222 | printStat(orcn=yorc) |
---|
| 223 | if debug: plot(yorcn=yorc, yorcnsum=yorc.sum(axis=0)) |
---|
| 224 | |
---|
| 225 | # Check that resulting LSM is identical to ORC original |
---|
| 226 | m1 = np.where(orc.sum(axis=0)>0, 1, 0) |
---|
| 227 | m2 = np.where(yorc.sum(axis=0)>0, 1, 0) |
---|
| 228 | print(np.all(m1==m2)) |
---|
| 229 | |
---|
| 230 | # Extend by 1 pixel |
---|
| 231 | yorc = extend(yorc) |
---|
| 232 | printStat(orcext=yorc) |
---|
| 233 | |
---|
| 234 | writePFTMAP(yorc, lat=lat, lon=lon, year=year, filename=path_out % dict(year=year), atts=atts) |
---|
| 235 | |
---|
| 236 | nc.close() |
---|
| 237 | |
---|