source: trunk/TOOLS/GENERATE_VEGET_MAP/ESA-LUH2/mergeLUH.py

Last change on this file was 7881, checked in by vladislav.bastrikov, 2 years ago

New scripts used to produce ORCHIDEE PFT maps v2023

File size: 9.7 KB
Line 
1import datetime, numpy as np, netCDF4, sys
2from grid import Grid
3from cwt import cwt_kg31to3
4from basics import printStat, writePFTMAP
5debug = False
6
7path_orc = "/home/surface5/vbastri/BDD/ESACCI-LC/ORCHIDEE_NEW/0.25/PFTmap_2010.nc"
8path_luh = "/home/surface5/vbastri/BDD/LUH2/GCB2022/states.nc"
9path_kg  = "/home/surface5/vbastri/BDD/KG2017/KG_720x1440.nc"
10path_c4  = "/home/surface5/vbastri/BDD/C4FRAC/C4frac_720x1440.nc"
11path_wb  = "/home/surface5/vbastri/BDD/ESACCI-LC/WATER/WB_720x1440.nc"
12path_out = "netcdf/PFTmap_%(year)s.nc"
13luh_year0 = 850
14merge_year = 2010
15build_years = np.arange(850, 2022+1)
16
17atts = 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
23BS    = 0
24TREBF = 1
25TRDBF = 2
26TEENF = 3
27TEEBF = 4
28TEDBF = 5
29BOENF = 6
30BODBF = 7
31BODNF = 8
32TEGR  = 9
33C4GR  = 10
34C3CR  = 11
35C4CR  = 12
36TRGR  = 13
37BOGR  = 14
38
39# LUH class indices
40PRIMF = 0
41PRIMN = 1
42SECDF = 2
43SECDN = 3
44URBAN = 4
45C3ANN = 5
46C4ANN = 6
47C3PER = 7
48C4PER = 8
49C3NFX = 9
50PASTR = 10
51RANGE = 11
52ICWTR = 12
53LUH_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
57def 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
60def 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
68def 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
82print("\nREAD ORCHIDEE")
83nc = netCDF4.Dataset(path_orc)
84lat = nc.variables["lat"][:]
85lon = nc.variables["lon"][:]
86orc = nc.variables["maxvegetfrac"][0].astype("f8").filled(0.)
87nc.close()
88printStat(orc=orc)
89orc = divide(orc, orc.sum(axis=0))
90printStat(orcn=orc)
91grid = Grid(lat=lat, lon=lon)
92if debug: plot(orc=orc, orcsum=orc.sum(axis=0))
93
94# Read Koeppen-Geiger map with 31 classes
95print("\nREAD KG")
96nc = netCDF4.Dataset(path_kg)
97if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit()
98kg31 = nc.variables["kg"][:].astype("f8").filled(0)
99nc.close()
100printStat(kg31=kg31)
101kg31 = divide(kg31, kg31.sum(axis=0))
102printStat(kg31n=kg31)
103
104# Aggregate KG into 3 groups
105kg = np.zeros((len(cwt_kg31to3[0])-1, len(lat), len(lon)))
106for idx, rule in enumerate(cwt_kg31to3[1:]): kg = kg + kg31[idx] * np.array(rule[1:])[:,None,None]
107printStat(kg3=kg)
108if 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)
110domclass = np.where(kg.sum(axis=(0,2)) == 0, len(kg)-1, kg.sum(axis=2).argmax(axis=0))
111for idx in range(len(lat)): kg[domclass[idx], idx, kg[:,idx,:].sum(axis=0) == 0] = 1
112printStat(kg3f=kg)
113if debug: plot(kgf=kg)
114
115print("\nREAD C4")
116nc = netCDF4.Dataset(path_c4)
117if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit()
118c4 = nc.variables["C4frac"][:].astype("f8").filled(0)
119nc.close()
120printStat(c4=c4)
121if debug: plot(c4=c4)
122
123print("\nREAD WB")
124nc = netCDF4.Dataset(path_wb)
125if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit()
126wb = nc.variables["wb"][:].astype("f8").filled(0)
127nc.close()
128printStat(wb=wb)
129lsm = np.where(wb[1] > 0.001, 1, 0)
130print("land pts=%s" % lsm.sum())
131if debug: plot(wb=wb, lsm=lsm)
132
133# Read LUH for the merging year
134print("\nREAD LUH")
135nc = netCDF4.Dataset(path_luh)
136if not np.all(lat == nc.variables["lat"][:]) or not np.all(lon == nc.variables["lon"][:]): print("Different grid"); sys.exit()
137luh = np.zeros((len(LUH_LIST)+1, len(lat), len(lon))) # array for all LUH maps plus ICWTR
138for 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)
139luh[ICWTR] = np.clip(1 - luh.sum(axis=0), 0, 1)
140luh = np.ma.masked_invalid(luh).filled(0)
141luh = divide(luh, luh.sum(axis=0))
142nc.close()
143printStat(luh=luh)
144if debug: plot(luh=luh, luhsum=luh.sum(axis=0))
145
146onlyorc = np.where((orc.sum(axis=0) > 0) & (luh.sum(axis=0) == 0), 1, 0)
147onlyluh = np.where((orc.sum(axis=0) == 0) & (luh.sum(axis=0) > 0), 1, 0)
148print("ORC>0 LUH=0", onlyorc.sum())
149print("ORC=0 LUH>0", onlyluh.sum())
150if debug: plot(onlyorc=onlyorc, onlyluh=onlyluh)
151
152print("\nBUILD FRACTIONS")
153luh_C3  = luh[C3ANN] + luh[C3PER] + luh[C3NFX]
154luh_C4  = luh[C4ANN] + luh[C4PER]
155luh_ant = luh_C3 + luh_C4 + luh[PASTR]
156
157orc_gr = orc[TEGR] + orc[TRGR] + orc[BOGR] + orc[C4GR]
158orc_cr = orc[C3CR] + orc[C4CR]
159gr_ant = np.clip(luh_ant - orc_cr, 0, np.inf)
160gr_nat = np.clip(orc_gr  - gr_ant, 0, np.inf)
161gr_tr = gr_nat * divide(orc[TRGR], orc_gr)
162gr_te = gr_nat * divide(orc[TEGR], orc_gr)
163gr_bo = gr_nat * divide(orc[BOGR], orc_gr)
164gr_C4 = gr_nat * divide(orc[C4GR], orc_gr)
165total_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
167frac = divide(orc, total_nat)
168frac[TRGR] = divide(gr_tr, total_nat)
169frac[TEGR] = divide(gr_te, total_nat)
170frac[BOGR] = divide(gr_bo, total_nat)
171frac[C4GR] = divide(gr_C4, total_nat)
172frac[C3CR] = 0
173frac[C4CR] = 0
174printStat(frac=frac)
175print("ORC>0 FRAC=0", ((orc.sum(axis=0) > 0) & (frac.sum(axis=0) == 0)).sum()) # these are pixels with crops in ORCHIDEE (161)
176print("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)
177print("ORC>0 LUH>0 FRAC=0", ((orc.sum(axis=0) > 0) & (luh.sum(axis=0) > 0) & (frac.sum(axis=0) == 0)).sum()) # 40
178if debug: plot(frac=frac, fracsum=frac.sum(axis=0))
179
180nc = netCDF4.Dataset(path_luh)
181for 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
236nc.close()
237
Note: See TracBrowser for help on using the repository browser.