Source code for hesseflux.nee2gpp

#!/usr/bin/env python
"""
nee2gpp : Estimates photosynthesis (GPP) and ecosystem respiration (RECO)
          from Eddy covariance CO2 flux data.

This module was written by Matthias Cuntz while at Department of
Computational Hydrosystems, Helmholtz Centre for Environmental
Research - UFZ, Leipzig, Germany, and continued while at Institut
National de Recherche pour l'Agriculture, l'Alimentation et
l'Environnement (INRAE), Nancy, France.

Copyright (c) 2012-2020 Matthias Cuntz - mc (at) macu (dot) de
Released under the MIT License; see LICENSE file for details.

* Written 2012 by Matthias Cuntz - mc (at) macu (dot) de
* Set default undef to NaN, Mar 2012, Arndt Piayda
* Add wrapper nee2gpp for individual routines, Nov 2012, Matthias Cuntz
* Ported to Python 3, Feb 2013, Matthias Cuntz
* Use generel cost function cost_abs from functions module,
  May 2013, Matthias Cuntz
* Use fmin_tnc to allow params < 0, Aug 2014, Arndt Piayda
* Keyword nogppnight, Aug 2014, Arndt Piayda
* Add wrapper nee2gpp for individual routines, Nov 2012, Matthias Cuntz
* Add wrapper nee2gpp for individual routines, Nov 2012, Matthias Cuntz
* Input can be pandas Dataframe or numpy array(s), Apr 2020, Matthias Cuntz
* Using numpy docstring format, May 2020, Matthias Cuntz
* Removed np.float and np.int, Jun 2024, Matthias Cuntz

.. moduleauthor:: Matthias Cuntz, Arndt Piayda

The following functions are provided

.. autosummary::
   nee2gpp
"""
from __future__ import division, absolute_import, print_function
import warnings
import numpy as np
import pandas as pd
import scipy.optimize as opt  # curve_fit, fmin, fmin_tnc
from pyjams import mad
from pyjams.functions import cost_abs, lloyd_only_rref_p
from pyjams.functions import cost_lloyd_fix, lloyd_fix, lloyd_fix_p
from pyjams.functions import cost_lasslop, lasslop


__all__ = ['nee2gpp']


# ----------------------------------------------------------------------
[docs] def nee2gpp(dfin, flag=None, isday=None, date=None, timeformat='%Y-%m-%d %H:%M:%S', colhead=None, undef=-9999, method='reichstein', nogppnight=False, swthr=10.): """ Calculate photosynthesis (GPP) and ecosystem respiration (RECO) from Eddy covariance CO2 flux data. It uses either 1. a fit of Reco vs. temperature to all nighttime data [1]_ (`method='falge'`), or 2. several fits over the season of Reco vs. temperature as in Reichstein et al. (2005) [2]_ (`method='reichstein'`), or 3. the daytime method of Lasslop et al. (2010) [3]_ (`method='lasslop'`), in order to calculate Reco and then GPP = Reco - NEE. Parameters ---------- dfin : pandas.Dataframe or numpy.array time series of CO2 fluxes and air temperature, and possibly incoming shortwave radiation and air vapour pressure deficit. `dfin` can be a pandas.Dataframe with the columns 'FC' or 'NEE' (or starting with 'FC\\_' or 'NEE\\_') for observed CO2 flux [umol(CO2) m-2 s-1] 'TA' (or starting with 'TA\\_') for air temperature [K] `method='lasslop'` or `method='day'` needs also 'SW_IN' (or starting with 'SW_IN') for incoming short-wave radiation [W m-2] 'VPD' (or starting with 'VPD') for air vapour deficit [Pa] The index is taken as date variable. `dfin` can also me a numpy array with the same columns. In this case `colhead`, `date`, and possibly `dateformat` must be given. flag : pandas.Dataframe or numpy.array, optional flag Dataframe or array has the same shape as `dfin`. Non-zero values in `flag` will be treated as missing values in `dfin`. `flag` must follow the same rules as `dfin` if pandas.Dataframe. If `flag` is numpy array, `df.columns.values` will be used as column heads and the index of `dfin` will be copied to `flag`. isday : array_like of bool, optional True when it is day, False when night. Must have the same length as `dfin.shape[0]`. If `isday` is not given, `dfin` must have a column with head 'SW_IN' or starting with 'SW_IN'. `isday` will then be `dfin['SW_IN'] > swthr`. date : array_like of string, optional 1D-array_like of calendar dates in format given in `timeformat`. `date` must be given if `dfin` is numpy array. timeformat : str, optional Format of dates in `date`, if given (default: '%Y-%m-%d %H:%M:%S'). See strftime documentation of Python's datetime module: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior colhed : array_like of str, optional column names if `dfin` is numpy array. See `dfin` for mandatory column names. undef : float, optional values having `undef` value are treated as missing values in `dfin` (default: -9999) method : str, optional method to use for partitioning. Possible values are: 'global' or 'falge': fit of Reco vs. temperature to all nighttime data 'local' of 'reichstein': several fits over the season of Reco vs. temperature as in Reichstein et al. (2005) (default) 'day' or 'lasslop': method of Lasslop et al. (2010) fitting a light-response curve nogppnight : float, optional GPP will be set to zero at night. RECO will then equal NEE at night (default: False) swthr : float, optional Threshold to determine daytime from incoming shortwave radiation if `isday` not given (default: 10). Returns ------- pandas.Dataframe or numpy arrays pandas.Dataframe with two columns 'GPP' and 'RECO' with estimated photosynthesis and ecosystem respiration, or two numpy arrays [GPP, RECO]. Notes ----- Negative respiration possible at night if GPP is forced to 0 with `nogppnight=True`. References ---------- .. [1] Falge et al. (2001) Gap filling strategies for defensible annual sums of net ecosystem exchange, Acricultural and Forest Meteorology 107, 43-69 .. [2] Reichstein et al. (2005) On the separation of net ecosystem exchange into assimilation and ecosystem respiration: review and improved algorithm, Global Change Biology 11, 1424-1439 .. [3] Lasslop et al. (2010) Separation of net ecosystem exchange into assimilation and respiration using a light response curve approach: critical issues and global evaluation, Global Change Biology 16, 187-208 Examples -------- >>> from fread import fread >>> from date2dec import date2dec >>> from dec2date import dec2date >>> ifile = 'test_nee2gpp.csv' >>> undef = -9999. >>> dat = fread(ifile, skip=2, transpose=True) >>> ndat = dat.shape[1] >>> head = fread(ifile, skip=2, header=True) >>> head1 = head[0] >>> # date >>> jdate = date2dec(dy=dat[0,:], mo=dat[1,:], yr=dat[2,:], hr=dat[3,:], mi=dat[4,:]) >>> adate = dec2date(jdate, eng=True) >>> # colhead >>> idx = [] >>> for i in head1: ... if i in ['NEE', 'rg', 'Tair', 'VPD']: idx.append(head1.index(i)) >>> colhead = ['FC', 'SW_IN', 'TA', 'VPD'] >>> # data >>> dfin = dat[idx,:] >>> dfin[2,:] = np.where(dfin[2,:] == undef, undef, dfin[2,:]+273.15) >>> dfin[3,:] = np.where(dfin[3,:] == undef, undef, dfin[3,:]*100.) >>> # flag >>> flag = np.where(dfin == undef, 2, 0) >>> # partition >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='local') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> print(Reco[1120:1128]) [1.68311981 1.81012431 1.9874173 2.17108871 2.38759152 2.64372415 2.90076664 3.18592735] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='local') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='global') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.33166157e+00 8.18228013e+00 1.04092252e+01 8.19395317e+00 1.08427448e+01] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='Reichstein') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='reichstein') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='day') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 2.78457540e+00 6.63212545e+00 8.88902165e+00 6.74243873e+00 9.51364527e+00] >>> print(Reco[1120:1128]) [0.28786696 0.34594516 0.43893276 0.5495954 0.70029545 0.90849165 1.15074873 1.46137527] History ------- Written Matthias Cuntz, Mar 2012 Modified Arndt Piayda, Mar 2012 - undef=np.nan Matthias Cuntz, Nov 2012 - wrapper for individual routines nee2gpp_reichstein etc. Matthias Cuntz, Feb 2013 - ported to Python 3 Matthias Cuntz, May 2013 - replaced cost functions by generel cost function cost_abs if possible Arndt Piayda, Aug 2014 - replaced fmin with fmin_tnc to permit params<0, permit gpp<0 at any time if nogppnight=True Matthias Cuntz, Jun 2024 - removed np.int and np.float """ # Check input # numpy or panda if isinstance(dfin, (np.ndarray, np.ma.MaskedArray)): isnumpy = True assert colhead is not None, ( 'colhead must be given if input is numpy.ndarray.') if dfin.shape[0] == len(colhead): df = pd.DataFrame(dfin.T, columns=colhead) elif dfin.shape[1] == len(colhead): df = pd.DataFrame(dfin, columns=colhead) else: raise ValueError('Length of colhead must be number of columns' ' in input array. len(colhead)=' + str(len(colhead)) + ' shape(input)=(' + str(dfin.shape[0]) + ',' + str(dfin.shape[1]) + ').') assert date is not None, 'Date must be given if input is numpy arrary.' df['Datetime'] = pd.to_datetime(date, format=timeformat) df.set_index('Datetime', drop=True, inplace=True) else: isnumpy = False assert isinstance(dfin, pd.core.frame.DataFrame), ( 'Input must be either numpy.ndarray or pandas.DataFrame.') df = dfin.copy(deep=True) # Incoming flags if flag is not None: if isinstance(flag, (np.ndarray, np.ma.MaskedArray)): if flag.shape[0] == len(df): ff = pd.DataFrame(flag, columns=df.columns.values) elif flag.shape[1] == len(df): ff = pd.DataFrame(flag.T, columns=df.columns.values) else: raise ValueError( 'flag must have same shape as data array.' ' data: ({:d},{:d}); flag: ({:d},{:d})'.format( dfin.shape[0], dfin.shape[1], flag.shape[0], flag.shape[1])) ff = ff.set_index(df.index) else: assert isinstance(flag, pd.core.frame.DataFrame), ( 'Flag must be either numpy.ndarray or pandas.DataFrame.') ff = flag.copy(deep=True) else: # flags: 0: good; 1: input flagged; 2: output flagged ff = df.copy(deep=True).astype(int) ff[:] = 0 ff[df == undef] = 1 ff[df.isna()] = 1 # day or night if isday is None: sw_id = '' for cc in df.columns: if cc.startswith('SW_IN'): sw_id = cc break assert sw_id, ('Global radiation with name SW or starting with' ' SW_ must be in input if isday not given.') # Papale et al. (Biogeosciences, 2006): 20; REddyProc: 10 isday = df[sw_id] > swthr if isinstance(isday, (pd.core.series.Series, pd.core.frame.DataFrame)): isday = isday.to_numpy() isday[isday == undef] = np.nan ff[np.isnan(isday)] = 1 # Global relationship in Reichstein et al. (2005) if ((method.lower() == 'global') | (method.lower() == 'falge')): dfout = _nee2gpp_falge(df, ff, isday, undef=undef) # Local relationship = Reichstein et al. (2005) elif ((method.lower() == 'local') | (method.lower() == 'reichstein')): dfout = _nee2gpp_reichstein(df, ff, isday, undef=undef, nogppnight=nogppnight) # Lasslop et al. (2010) daytime method elif ((method.lower() == 'day') | (method.lower() == 'lasslop')): dfout = _nee2gpp_lasslop(df, ff, isday, undef=undef, nogppnight=nogppnight) # Include new methods here else: raise ValueError('Error nee2gpp: method not implemented yet.') if isnumpy: return dfout['GPP'].to_numpy(), dfout['RECO'].to_numpy() else: return dfout
# ---------------------------------------------------------------------- def _nee2gpp_falge(df, ff, isday, undef=-9999): """ Calculate photosynthesis (GPP) and ecosystem respiration (RECO) from original Eddy flux data, using a fit of Reco vs. temperature to all nighttime data [1]_, in order to calculate Reco and then GPP = Reco - NEE. Parameters ---------- df : pandas.Dataframe time series of CO2 fluxes and air temperature. pandas.Dataframe with the columns 'FC' or 'NEE' (or starting with 'FC\\_' or 'NEE\\_') for observed CO2 flux [umol(CO2) m-2 s-1] 'TA' (or starting with 'TA\\_') for air temperature [K] The index is taken as date variable. ff : pandas.Dataframe flag Dataframe or array has the same shape as `df`. Non-zero values in `ff` will be treated as missing values in `df`. `ff` must follow the same rules as `df`. isday : array_like of bool True when it is day, False when night. Must have the same length as `df.shape[0].` undef : float, optional values having `undef` value are treated as missing values in `df` (default: -9999) Returns ------- pandas.Dataframe or numpy arrays pandas.Dataframe with two columns 'GPP' and 'RECO' with estimated photosynthesis and ecosystem respiration. References ---------- .. [1] Falge et al. (2001) Gap filling strategies for defensible annual sums of net ecosystem exchange, Acricultural and Forest Meteorology 107, 43-69 Examples -------- >>> from fread import fread >>> from date2dec import date2dec >>> from dec2date import dec2date >>> ifile = 'test_nee2gpp.csv' >>> undef = -9999. >>> dat = fread(ifile, skip=2, transpose=True) >>> ndat = dat.shape[1] >>> head = fread(ifile, skip=2, header=True) >>> head1 = head[0] >>> # date >>> jdate = date2dec(dy=dat[0,:], mo=dat[1,:], yr=dat[2,:], ... hr=dat[3,:], mi=dat[4,:]) >>> adate = dec2date(jdate, eng=True) >>> # colhead >>> idx = [] >>> for i in head1: ... if i in ['NEE', 'rg', 'Tair', 'VPD']: idx.append(head1.index(i)) >>> colhead = ['FC', 'SW_IN', 'TA', 'VPD'] >>> # data >>> dfin = dat[idx,:] >>> dfin[2,:] = np.where(dfin[2,:] == undef, undef, dfin[2,:]+273.15) >>> dfin[3,:] = np.where(dfin[3,:] == undef, undef, dfin[3,:]*100.) >>> # flag >>> flag = np.where(dfin == undef, 2, 0) >>> # partition >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='global') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.33166157e+00 8.18228013e+00 1.04092252e+01 8.19395317e+00 1.08427448e+01] History ------- Written Matthias Cuntz, Mar 2012 Modified Arndt Piayda, Mar 2012 - undef=np.nan Matthias Cuntz, Nov 2012 - individual routine Matthias Cuntz, Feb 2013 - ported to Python 3 Matthias Cuntz, Jun 2024 - removed np.int and np.float """ # Variables fc_id = '' for cc in df.columns: if ( cc.startswith('FC_') or (cc == 'FC') or cc.startswith('NEE_') or (cc == 'NEE') ): fc_id = cc break ta_id = '' for cc in df.columns: if cc.startswith('TA_') or (cc == 'TA'): ta_id = cc break assert fc_id, ('Carbon net flux with name FC or NEE or starting with FC_' ' or NEE_ must be in input.') assert ta_id, ('Air temperature with name TA or starting with TA_ must' ' be in input.') nee = np.ma.array(df[fc_id], mask=(ff[fc_id] > 0)) t = np.ma.array(df[ta_id], mask=(ff[ta_id] > 0)) misday = np.ma.array(isday, mask=((~np.isfinite(isday)) | (isday == undef))) # Partition - Global relationship as in Falge et al. (2001) # Select valid nighttime ndata = nee.size mask = misday | nee.mask | t.mask | misday.mask ii = np.where(~mask)[0] tt = np.ma.compressed(t[ii]) net = np.ma.compressed(nee[ii]) p = opt.fmin(cost_abs, [2., 200.], args=(lloyd_fix_p, tt, net), disp=False) Reco = np.ones(ndata) * undef ii = np.where(~t.mask)[0] Reco[ii] = lloyd_fix(t[ii], p[0], p[1]) # GPP GPP = np.ones(ndata) * undef ii = np.where(~(t.mask | nee.mask))[0] GPP[ii] = Reco[ii] - nee[ii] dfout = pd.DataFrame({'GPP': GPP, 'RECO': Reco}, index=df.index) return dfout # ---------------------------------------------------------------------- def _nee2gpp_reichstein(df, ff, isday, undef=-9999, nogppnight=False): """ Calculate photosynthesis (GPP) and ecosystem respiration (RECO) from original Eddy flux data, using several fits of Reco vs. temperature of nighttime data over the season, as in Reichstein et al. (2005) [2]_, in order to calculate Reco and then GPP = Reco - NEE. Parameters ---------- df : pandas.Dataframe time series of CO2 fluxes and air temperature. pandas.Dataframe with the columns 'FC' or 'NEE' (or starting with 'FC\\_' or 'NEE\\_') for observed CO2 flux [umol(CO2) m-2 s-1] 'TA' (or starting with 'TA\\_') for air temperature [K] The index is taken as date variable. ff : pandas.Dataframe flag Dataframe or array has the same shape as `df`. Non-zero values in `ff` will be treated as missing values in `df`. `ff` must follow the same rules as `df`. isday : array_like of bool True when it is day, False when night. Must have the same length as `df.shape[0].` undef : float, optional values having `undef` value are treated as missing values in `df` (default: -9999) nogppnight : float, optional GPP will be set to zero at night. RECO will then equal NEE at night (default: False) Returns ------- pandas.Dataframe pandas.Dataframe with two columns 'GPP' and 'RECO' with estimated photosynthesis and ecosystem respiration. References ---------- .. [2] Reichstein et al. (2005) On the separation of net ecosystem exchange into assimilation and ecosystem respiration: review and improved algorithm, Global Change Biology 11, 1424-1439 Examples -------- >>> from fread import fread >>> from date2dec import date2dec >>> from dec2date import dec2date >>> ifile = 'test_nee2gpp.csv' >>> undef = -9999. >>> dat = fread(ifile, skip=2, transpose=True) >>> ndat = dat.shape[1] >>> head = fread(ifile, skip=2, header=True) >>> head1 = head[0] >>> # date >>> jdate = date2dec(dy=dat[0,:], mo=dat[1,:], yr=dat[2,:], hr=dat[3,:], ... mi=dat[4,:]) >>> adate = dec2date(jdate, eng=True) >>> # colhead >>> idx = [] >>> for i in head1: ... if i in ['NEE', 'rg', 'Tair', 'VPD']: idx.append(head1.index(i)) >>> colhead = ['FC', 'SW_IN', 'TA', 'VPD'] >>> # data >>> dfin = dat[idx,:] >>> dfin[2,:] = np.where(dfin[2,:] == undef, undef, dfin[2,:]+273.15) >>> dfin[3,:] = np.where(dfin[3,:] == undef, undef, dfin[3,:]*100.) >>> # flag >>> flag = np.where(dfin == undef, 2, 0) >>> # partition >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='local') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> print(Reco[1120:1128]) [1.68311981 1.81012431 1.9874173 2.17108871 2.38759152 2.64372415 2.90076664 3.18592735] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='local') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='Reichstein') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, ... undef=undef, method='reichstein') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] History ------- Written Matthias Cuntz, Mar 2012 Modified Arndt Piayda, Mar 2012 - undef=np.nan Matthias Cuntz, Nov 2012 - individual routine Matthias Cuntz, Feb 2013 - ported to Python 3 Matthias Cuntz, Jun 2024 - removed np.int and np.float """ # Variables fc_id = '' for cc in df.columns: if ( cc.startswith('FC_') or (cc == 'FC') or cc.startswith('NEE_') or (cc == 'NEE') ): fc_id = cc break ta_id = '' for cc in df.columns: if cc.startswith('TA_') or (cc == 'TA'): ta_id = cc break assert fc_id, ('Carbon net flux with name FC or NEE or starting with FC_' ' or NEE_ must be in input.') assert ta_id, ('Air temperature with name TA or starting with TA_ must' ' be in input.') nee = np.ma.array(df[fc_id], mask=(ff[fc_id] > 0)) t = np.ma.array(df[ta_id], mask=(ff[ta_id] > 0)) misday = np.ma.array(isday, mask=((~np.isfinite(isday)) | (isday == undef))) dates = df.index.to_julian_date() # Partition - Local relationship = Reichstein et al. (2005) ndata = nee.size GPP = np.ones(ndata) * undef Reco = np.ones(ndata) * undef dfout = pd.DataFrame({'GPP': GPP, 'RECO': Reco}, index=df.index) # Select valid nighttime mask = misday | nee.mask | t.mask | misday.mask ii = np.where(~mask)[0] if (ii.size == 0): # raise ValueError('Error _nee2gpp_reichstein:' # ' no valid nighttime data.') print('Warning _nee2gpp_reichstein: no valid nighttime data.') return dfout jul = dates[ii] tt = np.ma.compressed(t[ii]) net = np.ma.compressed(nee[ii]) # 1. each 5 days, in 15 day period, fit if range of T > 5 locp = [] # local param locs = [] # local err # be aware that julian days starts at noon, i.e. 1.0 is 12h # so the search will be from noon to noon and thus includes all nights dmin = np.floor(np.amin(jul)).astype(int) dmax = np.ceil(np.amax(jul)).astype(int) for i in range(dmin, dmax, 5): iii = np.where((jul >= i) & (jul < (i + 14)))[0] niii = iii.size if niii > 6: tt1 = tt[iii] net1 = net[iii] # make fit more robust by removing outliers mm = ~mad(net1, z=4.5) if (np.ptp(tt[iii]) >= 5.) & (np.sum(mm) > 6): p, temp1, temp2 = opt.fmin_tnc(cost_lloyd_fix, [2., 200.], bounds=[[0., None], [0., None]], args=(tt1[mm], net1[mm]), approx_grad=True, disp=False) try: # params, covariance p1, c = opt.curve_fit(lloyd_fix, tt1[mm], net1[mm], p0=p, maxfev=10000) # possible return of curvefit: c=inf if np.all(np.isfinite(c)): s = np.sqrt(np.diag(c)) else: s = 10. * np.abs(p) except: s = 10. * np.abs(p) locp += [p] locs += [s] # if ((s[1]/p[1])<0.5) & (p[1] > 0.): pdb.set_trace() if len(locp) == 0: # raise ValueError('Error _nee2gpp_reichstein:' # ' No local relationship found.') print('Warning _nee2gpp_reichstein: No local relationship found.') return dfout locp = np.squeeze(np.array(locp).astype(float)) locs = np.squeeze(np.array(locs).astype(float)) # 2. E0 = avg of best 3 # Reichstein et al. (2005), p. 1430, 1st paragraph. with warnings.catch_warnings(): warnings.simplefilter("ignore") iii = np.where((locp[:, 1] > 0.) & (locp[:, 1] < 450.) & (np.abs(locs[:, 1] / locp[:, 1]) < 0.5))[0] niii = iii.size if niii == 0: # raise ValueError('Error _nee2gpp_reichstein:' # ' No good local relationship found.') # loosen the criteria: take the best three estimates anyway iii = np.where((locp[:, 1] > 0.))[0] niii = iii.size if niii < 1: # raise ValueError('Error _nee2gpp_reichstein: No E0>0 found.') print('Warning _nee2gpp_reichstein: No E0>0 found.') return dfout lp = locp[iii, :] ls = locs[iii, :] iis = np.argsort(ls[:, 1]) bestp = np.mean(lp[iis[0:np.minimum(3, niii)], :], axis=0) bests = np.mean(ls[iis[0:np.minimum(3, niii)], :], axis=0) elif niii == 1: bestp = np.squeeze(locp[iii, :]) bests = np.squeeze(locs[iii, :]) elif niii == 2: bestp = np.mean(locp[iii, :], axis=0) bests = np.mean(locs[iii, :], axis=0) # ls = locs[iii,:] # iis = np.argsort(ls[:,1]) else: lp = locp[iii, :] ls = locs[iii, :] iis = np.argsort(ls[:, 1]) bestp = np.mean(lp[iis[0:3], :], axis=0) bests = np.mean(ls[iis[0:3], :], axis=0) # 3. Refit Rref with fixed E0, each 4 days refp = [] # Rref param refii = [] # mean index of data points E0 = bestp[1] et = lloyd_fix(tt, 1., E0) for i in range(dmin, dmax, 4): iii = np.where((jul >= i) & (jul < (i + 4)))[0] niii = iii.size if niii > 3: # Calc directly minisation of (nee-p*et)**2 p, temp1, temp2 = opt.fmin_tnc(cost_abs, [2.], bounds=[[0., None]], args=(lloyd_only_rref_p, et[iii], net[iii]), approx_grad=True, disp=False) refp += [p] refii += [int((iii[0] + iii[-1]) // 2)] if len(refp) == 0: # raise ValueError('Error _nee2gpp_reichstein:' # ' No ref relationship found.') print('Warning _nee2gpp_reichstein: No ref relationship found.') return dfout refp = np.squeeze(np.array(refp)) refii = np.squeeze(np.array(refii)) # 4. Interpol Rref Rref = np.interp(dates, jul[refii], refp) # 5. Calc Reco Reco = np.ones(ndata) * undef ii = np.where(~t.mask)[0] Reco[ii] = lloyd_fix(t[ii], Rref[ii], E0) # 6. Calc GPP GPP = np.ones(ndata) * undef ii = np.where(~(t.mask | nee.mask))[0] GPP[ii] = Reco[ii] - nee[ii] # 7. Set GPP=0 at night, if wanted if nogppnight: mask = misday | nee.mask | t.mask | misday.mask # night ii = np.where(~mask)[0] Reco[ii] = nee[ii] GPP[ii] = 0. # and prohibit negative gpp at any time mask = nee.mask | t.mask | (GPP > 0.) ii = np.where(~mask)[0] Reco[ii] -= GPP[ii] GPP[ii] = 0. dfout = pd.DataFrame({'GPP': GPP, 'RECO': Reco}, index=df.index) return dfout # ---------------------------------------------------------------------- def _nee2gpp_lasslop(df, ff, isday, undef=-9999, nogppnight=False): """ Calculate photosynthesis (GPP) and ecosystem respiration (RECO) from original Eddy flux data, using the daytime method of Lasslop et al. (2010) [3]_, in order to calculate Reco and then GPP = Reco - NEE. Parameters ---------- df : pandas.Dataframe or numpy.array time series of CO2 fluxes, air temperature, incoming shortwave radiation, and air vapour pressure deficit. `df` can be a pandas.Dataframe with the columns 'FC' or 'NEE' (or starting with 'FC\\_' or 'NEE\\_') for observed CO2 flux [umol(CO2) m-2 s-1] 'TA' (or starting with 'TA\\_') for air temperature [K] 'SW_IN' (or starting with 'SW_IN') for incoming short-wave radiation [W m-2] 'VPD' (or starting with 'VPD') for air vapour deficit [Pa] The index is taken as date variable. ff : pandas.Dataframe or numpy.array, optional flag Dataframe or array has the same shape as `df`. Non-zero values in `ff` will be treated as missing values in `df`. `ff` must follow the same rules as `df` if pandas.Dataframe. isday : array_like of bool, optional True when it is day, False when night. Must have the same length as `df.shape[0]`. undef : float, optional values having `undef` value are treated as missing values in `df` (default: -9999) nogppnight : float, optional GPP will be set to zero at night. RECO will then equal NEE at night (default: False) Returns ------- pandas.Dataframe pandas.Dataframe with two columns 'GPP' and 'RECO' with estimated photosynthesis and ecosystem respiration. References ---------- .. [3] Lasslop et al. (2010) Separation of net ecosystem exchange into assimilation and respiration using a light response curve approach: critical issues and global evaluation, Global Change Biology 16, 187-208 Examples -------- >>> from fread import fread >>> from date2dec import date2dec >>> from dec2date import dec2date >>> ifile = 'test_nee2gpp.csv' >>> undef = -9999. >>> dat = fread(ifile, skip=2, transpose=True) >>> ndat = dat.shape[1] >>> head = fread(ifile, skip=2, header=True) >>> head1 = head[0] >>> # date >>> jdate = date2dec(dy=dat[0,:], mo=dat[1,:], yr=dat[2,:], hr=dat[3,:], mi=dat[4,:]) >>> adate = dec2date(jdate, eng=True) >>> # colhead >>> idx = [] >>> for i in head1: ... if i in ['NEE', 'rg', 'Tair', 'VPD']: idx.append(head1.index(i)) >>> colhead = ['FC', 'SW_IN', 'TA', 'VPD'] >>> # data >>> dfin = dat[idx,:] >>> dfin[2,:] = np.where(dfin[2,:] == undef, undef, dfin[2,:]+273.15) >>> dfin[3,:] = np.where(dfin[3,:] == undef, undef, dfin[3,:]*100.) >>> # flag >>> flag = np.where(dfin == undef, 2, 0) >>> # partition >>> GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='day') >>> print(GPP[1120:1128]) [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 2.78457540e+00 6.63212545e+00 8.88902165e+00 6.74243873e+00 9.51364527e+00] >>> print(Reco[1120:1128]) [0.28786696 0.34594516 0.43893276 0.5495954 0.70029545 0.90849165 1.15074873 1.46137527] History ------- Written Matthias Cuntz, Mar 2012 Modified Arndt Piayda, Mar 2012 - undef=np.nan Matthias Cuntz, Nov 2012 - individual routine Matthias Cuntz, Feb 2013 - ported to Python 3 Matthias Cuntz, Jun 2024 - removed np.int and np.float """ # Variables fc_id = '' for cc in df.columns: if ( cc.startswith('FC_') or (cc == 'FC') or cc.startswith('NEE_') or (cc == 'NEE') ): fc_id = cc break ta_id = '' for cc in df.columns: if cc.startswith('TA_') or (cc == 'TA'): ta_id = cc break sw_id = '' for cc in df.columns: if cc.startswith('SW_IN_') or (cc == 'SW_IN'): sw_id = cc break vpd_id = '' for cc in df.columns: if cc.startswith('VPD_') or (cc == 'VPD'): vpd_id = cc break assert fc_id, ('Carbon net flux with name FC or NEE or starting with FC_' ' or NEE_ must be in input.') assert ta_id, ('Air temperature with name TA or starting with TA_ must' ' be in input.') assert sw_id, ('Global radiation with name SW or starting with SW_ must' ' be in input.') assert vpd_id, ('Vapour pressure deficit with name VPD or starting with' ' VPD_ must be in input.') nee = np.ma.array(df[fc_id], mask=(ff[fc_id] > 0)) t = np.ma.array(df[ta_id], mask=(ff[ta_id] > 0)) sw = np.ma.array(df[sw_id], mask=(ff[sw_id] > 0)) vpd = np.ma.array(df[vpd_id], mask=(ff[vpd_id] > 0)) misday = np.ma.array(isday, mask=((~np.isfinite(isday)) | (isday == undef))) dates = df.index.to_julian_date() # Partition - Lasslop et al. (2010) method ndata = nee.size GPP = np.ones(ndata) * undef Reco = np.ones(ndata) * undef dfout = pd.DataFrame({'GPP': GPP, 'RECO': Reco}, index=df.index) do_lgpp = False mask = nee.mask | t.mask | misday.mask | sw.mask | vpd.mask # night nmask = misday | mask nii = np.ma.where(~nmask)[0] njul = dates[nii] ntt = np.ma.compressed(t[nii]) nnet = np.ma.compressed(nee[nii]) aRref = np.mean(nnet) # day dmask = (~misday) | mask dii = np.ma.where(~dmask)[0] djul = dates[dii] dtt = np.ma.compressed(t[dii]) dnet = np.ma.compressed(nee[dii]) dsw = np.ma.compressed(sw[dii]) dvpd = np.ma.compressed(vpd[dii]) # starting values for optim aalpha = 0.01 qnet = np.sort(dnet) nqnet = qnet.size abeta0 = np.abs(qnet[np.floor(0.97 * nqnet).astype(int)] - qnet[np.ceil(0.03 * nqnet).astype(int)]) ak = 0. # out lE0 = [] lalpha = [] if do_lgpp: lbeta0 = [] lk = [] lRref = [] lii = [] dmin = np.floor(np.amin(dates)).astype(int) dmax = np.ceil(np.amax(dates)).astype(int) zaehl = -1 for i in range(dmin, dmax, 2): good = True # 1. Estimate E0 from nighttime data iii = np.squeeze(np.where((njul >= i) & (njul < (i + 12)))) niii = iii.size if niii > 3: p, temp1, temp2 = opt.fmin_tnc(cost_abs, [aRref, 100.], bounds=[[0., None], [0., None]], args=(lloyd_fix_p, ntt[iii], nnet[iii]), approx_grad=True, disp=False) E0 = np.maximum(p[1], 50.) else: if zaehl >= 0: E0 = lE0[zaehl] else: # large gap at beginning of data set, i.e. skip the period good = False continue # 2. Estimate alpha, k, beta0, Rref from daytime data iii = np.squeeze(np.where((djul >= i) & (djul < (i + 4)))) niii = iii.size if niii > 3: et = lloyd_fix(dtt[iii], 1., E0) again = True ialpha = aalpha ibeta0 = abeta0 ik = ak iRref = aRref bounds = [[None, None], [None, None], [None, None], [None, None]] while again: again = False p, nfeval, rc = opt.fmin_tnc(cost_lasslop, [ialpha, ibeta0, ik, iRref], bounds=bounds, args=(dsw[iii], et, dvpd[iii], dnet[iii]), approx_grad=True, disp=False) # if parameters beyond some bounds, set params and redo # the optim or skip if ((p[0] < 0.) | (p[0] > 0.22)): # alpha again = True if zaehl >= 0: bounds[0] = [lalpha[zaehl], lalpha[zaehl]] ialpha = lalpha[zaehl] else: bounds[0] = [0., 0.] ialpha = 0. if p[1] < 0.: # beta0 bounds[1] = [0., 0.] ibeta0 = 0. again = True if p[1] > 250.: good = False continue if p[2] < 0.: # k bounds[2] = [0., 0.] ik = 0. again = True if p[3] < 0: # Rref good = False continue if good: lalpha = lalpha + [p[0]] if do_lgpp: lbeta0 = lbeta0 + [p[1]] lk = lk + [p[2]] lRref = lRref + [p[3]] lii = lii + [int((iii[0] + iii[-1]) / 2)] else: continue else: continue lE0 = lE0 + [E0] zaehl += 1 if len(lE0) == 0: # raise ValueError('Error _nee2gpp_lasslop:' # ' No day relationship found.') print('Warning _nee2gpp_lasslop: No day relationship found.') return dfout lE0 = np.squeeze(np.array(lE0)) if do_lgpp: lalpha = np.squeeze(np.array(lalpha)) lbeta0 = np.squeeze(np.array(lbeta0)) lk = np.squeeze(np.array(lk)) lRref = np.squeeze(np.array(lRref)) lii = np.squeeze(np.array(lii)) # 3. Interpol E0 and Rref E0 = np.interp(dates, djul[lii], lE0) Rref = np.interp(dates, djul[lii], lRref) # 4. Calc Reco Reco = np.ones(ndata) * undef ii = np.squeeze(np.where(~t.mask)) Reco[ii] = lloyd_fix(t[ii], Rref[ii], E0[ii]) # 5. Calc GPP from light response for check if do_lgpp: alpha = np.interp(dates, djul[lii], lE0) beta0 = np.interp(dates, djul[lii], lbeta0) k = np.interp(dates, djul[lii], lk) et = lloyd_fix(t, 1., E0) lmask = t.mask | misday.mask | sw.mask | vpd.mask ii = np.squeeze(np.where(~lmask)) lgpp = np.zeros(ndata) lgpp[ii] = lasslop(sw[ii], et[ii], vpd[ii], alpha[ii], beta0[ii], k[ii], Rref[ii]) - Reco[ii] # 6. GPP GPP = np.ones(ndata) * undef ii = np.squeeze(np.where(~(t.mask | nee.mask))) GPP[ii] = Reco[ii] - nee[ii] # 7. Set GPP=0 at night, if wanted if nogppnight: mask = misday | nee.mask | t.mask | misday.mask # night ii = np.where(~mask)[0] Reco[ii] = nee[ii] GPP[ii] = 0. # and prohibit negative gpp at any time mask = nee.mask | t.mask | (GPP > 0.) ii = np.where(~mask)[0] Reco[ii] -= GPP[ii] GPP[ii] = 0. dfout = pd.DataFrame({'GPP': GPP, 'RECO': Reco}, index=df.index) return dfout # ------------------------------------------------------------- if __name__ == '__main__': import doctest doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE) # from fread import fread # from date2dec import date2dec # from dec2date import dec2date # ifile = 'test_nee2gpp.csv' # undef = -9999. # # Day,Month,Year,Hour,Minute,NEE,rg,Tair,VPD,GPP_f,Reco # # -,-,-,-,-,umolm-2s-1,Wm-2,degC,hPa,umol_m-2_s-1,umol_m-2_s-1 # # 1,1,2006,0,15,-9999,0,5.235,0.14192,-0.42485,2.89716 # dat = fread(ifile, skip=2, transpose=True) # ndat = dat.shape[1] # head = fread(ifile, skip=2, header=True) # head1 = head[0] # # date # jdate = date2dec(dy=dat[0,:], mo=dat[1,:], yr=dat[2,:], hr=dat[3,:], mi=dat[4,:]) # adate = dec2date(jdate, eng=True) # # colhead # idx = [] # for i in head1: # if i in ['NEE', 'rg', 'Tair', 'VPD']: idx.append(head1.index(i)) # colhead = ['FC', 'SW_IN', 'TA', 'VPD'] # # data # dfin = dat[idx,:] # dfin[2,:] = np.where(dfin[2,:] == undef, undef, dfin[2,:]+273.15) # dfin[3,:] = np.where(dfin[3,:] == undef, undef, dfin[3,:]*100.) # # flag # flag = np.where(dfin == undef, 2, 0) # # isday = np.where(dfin[1,:] > 10., True, False) # GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='local') # print(GPP[1120:1128]) # # [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 # # 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] # print(Reco[1120:1128]) # # [1.68311981 1.81012431 1.9874173 2.17108871 2.38759152 2.64372415 # # 2.90076664 3.18592735] # GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='local') # print(GPP[1120:1128]) # # [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 # # 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] # GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='global') # print(GPP[1120:1128]) # # [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.33166157e+00 # # 8.18228013e+00 1.04092252e+01 8.19395317e+00 1.08427448e+01] # GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='Reichstein') # print(GPP[1120:1128]) # # [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.406068706013192 # # 8.319421516040766 10.624254150217764 8.492456637225963 11.238197347837367] # GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='reichstein') # print(GPP[1120:1128]) # # [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 4.40606871e+00 # # 8.31942152e+00 1.06242542e+01 8.49245664e+00 1.12381973e+01] # GPP, Reco = nee2gpp(dfin, flag=flag, date=adate, colhead=colhead, undef=undef, method='day') # print(GPP[1120:1128]) # # [-9.99900000e+03 -9.99900000e+03 -9.99900000e+03 2.78457540e+00 # # 6.63212545e+00 8.88902165e+00 6.74243873e+00 9.51364527e+00] # print(Reco[1120:1128]) # # [0.28786696 0.34594516 0.43893276 0.5495954 0.70029545 0.90849165 # # 1.15074873 1.46137527]