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 | |
---|