Module pyl4c.apps.l4c.core
Classes
class TCF (params: dict,
land_cover_map: Sequence,
state: Sequence = None,
litterfall: Sequence = None)-
Expand source code
class TCF(object): ''' The Terrestrial Carbon Flux (TCF) model, which is the basis for the NASA Soil Moisture Active Passive (SMAP) Level 4 Carbon (L4C) model. TCF incorporates these basic assumptions: 1. Carbon assimilation (gross primary production) is linearly related to the amount of solar radiation and green photosynthetic land cover. 2. There is a maximum rate of carbon assimilation. Low water availability, cold or freezing temperatures, and high atmospheric demand for water vapor each reduce the efficiency of carbon assimilation. 3. Soil organic carbon (SOC) decomposes at different rates based on the type of material. The optimal rate of decomposition is fixed and depends only on the land-cover type. 4. The efficiency of soil organic carbon decomposition is reduced under low soil temperatures or low soil moisture. A complete description is available in Kimball et al. (2009) and in Jones et al. (2017). Soil organic carbon (SOC) and soil decomposition are discussed in Endsley et al. (2020) and Endsley et al. (2022). If initial model "state" is provided it should be a 2D array with the first axis containing, in order: - Initial state of "metabolic" SOC pool - Initial state of "structural" SOC pool - Initial state of "recalcitrant" SOC pool NOTE: For developers, `self.params` refers to model parameters that have been vectorized to improve performance. If a function performs vectorized calculations on longitudinal (N x T) arrays, the vectorized parameter(s) can be used with their current shape. For cross-sectional calculations, e.g., on (N,) or (1 x N) arrays, make sure to use the transpose, `self.params.name.T`, where `name` is the parameter name. Example use for a single pixel: tcf = TCF(params, land_cover_map = [7]) tcf.nee(state = [500, 500, 1000], drivers = [...]) Parameters ---------- params : dict Dictionary of model parameters; keys should be among `TCF.required_parameters` and values should be sequences ordered by PFT code, either 9 values (value at index 0 being `np.nan`) or as many values as there are keys in `TCF.valid_pft` land_cover_map : Sequence or numpy.ndarray 1-dimensional sequence of one or more land-cover types, one for each model resolution cell (pixel). These types should be represented as integers (unsigned 16-bit type or lower). state : Sequence or numpy.ndarray or None A sequence of 3 values or a 2D (S x N) array representing the state variables for each of N pixels. The first three (3) state variables should be the initial states of each SOC pool. litterfall : Sequence or numpy.ndarray or None A sequence of values or 1D array representing average annual litterfall (units: g C m-2 year-1) for each model resolution cell (pixel). If not provided, it will be based on the climatological NPP (when it is calculated). ''' required_drivers = [ 'fPAR', 'PAR', 'Tmin', 'VPD', 'SMRZ', 'FT', 'Tsoil', 'SMSF' ] required_parameters = [ 'LUE', 'tmin0', 'tmin1', 'vpd0', 'vpd1', 'smrz0', 'smrz1', 'ft0', 'CUE', 'tsoil', 'smsf0', 'smsf1', 'decay_rates', 'f_structural', 'f_metabolic' ] valid_pft = { 1: 'Evergreen Needleleaf', 2: 'Evergreen Broadleaf', 3: 'Deciduous Needleleaf', 4: 'Deciduous Broadleaf', 5: 'Shrubland', 6: 'Grassland', 7: 'Cereal Croplands', 8: 'Broadleaf Croplands' } num_soc_pools = 3 # Number of soil organic carbon (SOC) pools version = '7.4.1' # Pegged to the SMAP L4C version def __init__( self, params: dict, land_cover_map: Sequence, state: Sequence = None, litterfall: Sequence = None ): self.constants = Namespace() self.history = Namespace() self.state = Namespace() self.params = Namespace() # Parameters accessed, e.g., tcf.params.LUE self.lc_map = np.array(land_cover_map, dtype = np.uint16) # Load available annual litterfall (per resolution cell/ pixel) if litterfall is not None: litterfall = np.array(litterfall, dtype = np.float32) self.constants.add('litterfall', litterfall) self.constants.add('litterfall_rate', None) # Load soil organic carbon (SOC) state if state is not None: if hasattr(state, 'ndim'): assert state.ndim <= 2, '"state" should have at most 2 dimensions' # "state" either begins as or is converted to a numpy.ndarray state = np.array(state, dtype = np.float32) if state.ndim == 1: state = state[:,np.newaxis] assert len(state) == 3, 'Expected one "state" value for each SOC pool' self.state.add('soc', np.array(state)) # Create a parameters vector; e.g., for a land-cover map: # array([ 1, 2, 1, 3]) # We create an array of parameters: # array([1.5, 3.0, 1.5, 1.0]) # Where the unique parameter for each land-cover class is copied as # many times as that class appears; result is an array that is the # same shape and size as the land-cover array # Possible items of the parameters dictionary include, e.g.: # {"key": 3.14} # {"key": [3.14, 1.10]} # {"key": np.array([3.14, 1.10])} # {"key": np.array([[3.14, 1.10], [...], ...])} for key, value in params.items(): if key not in self.required_parameters: continue p_vector = np.array(value) # Copy parameter values based on PFT map if key == 'decay_rates': if p_vector.shape == (self.num_soc_pools,): p_vector = p_vector[:,np.newaxis]\ .repeat(self.lc_map.size, axis = -1) elif hasattr(value, 'count') and p_vector.ndim == 2: # i.e., "value" was nested lists and result of converting # to a NumPy array was a (N x self.num_soc_pools) array p_vector = p_vector.swapaxes(0, 1)[:,self.lc_map] elif p_vector.ndim == 2: p_vector = p_vector[:,self.lc_map] assert p_vector.shape == (self.num_soc_pools, self.lc_map.size) else: if p_vector.ndim == 0: p_vector = p_vector[np.newaxis][np.newaxis]\ .repeat(self.lc_map.size, axis = 0) elif p_vector.ndim == 1: p_vector = p_vector.ravel()[self.lc_map]\ .reshape((self.lc_map.size, 1)) elif p_vector.ndim == 2: p_vector = p_vector[:,self.lc_map].swapaxes(0, 1) # Result should be a 2D vectorized parameter array, either: # (N,) or (S x N), where S is, e.g., each SOC pool assert p_vector.ndim in (1, 2) assert p_vector.shape[0] == self.lc_map.size or p_vector.shape[0] == 3 self.params.add(key, p_vector) def _rescale_smrz(self, smrz0, smrz_min, smrz_max = 100): r''' Rescales root-zone soil-moisture (SMRZ) to increase plant sensitivity to very low water availability. $$ \hat{\theta} &= 100 \times\left( \frac{\theta - \theta_{WP}}{\text{max}(\theta) - \theta_{WP}} \right) + 1\\ \theta_{RZ} &= 95 \times \frac{\text{ln}(\hat{\theta})}{\text{ln(101)}} + 5 $$ Parameters ---------- smrz0 : numpy.ndarray (N x T) array of original SMRZ data, in percent wetness [%] units for N sites and T time steps smrz_min : numpy.ndarray or float Site-level long-term minimum SMRZ (proportion saturation) smrz_max : numpy.ndarray or float Site-level long-term maximum SMRZ (proportion saturation); can optionally provide a fixed upper-limit on SMRZ Returns ------- tuple A tuple of `(gpp, npp, tmult, wmult)`, each an numpy.ndarray ''' smrz_min = np.array(smrz_min) if smrz_min.ndim == 1: smrz_min = smrz_min[:,np.newaxis] # Clip input SMRZ to the lower, upper bounds smrz0 = np.where(smrz0 < smrz_min, smrz_min, smrz0) smrz0 = np.where(smrz0 > smrz_max, smrz_max, smrz0) smrz_norm = np.add(np.multiply(100, np.divide( np.subtract(smrz0, smrz_min), np.subtract(smrz_max, smrz_min))), 1) # Log-transform normalized data and rescale to range between # 5.0 and 100% saturation) return np.add( np.multiply(95, np.divide(np.log(smrz_norm), np.log(101))), 5) def _setup_forward( self, drivers: Sequence, state: Sequence = None, dates: Sequence = None ) -> np.ndarray: 'Pre-computes some vectorized quantities prior to forward run' self.check_drivers(drivers) litter = self.constants.litterfall if litter is None: assert dates is not None,\ 'Either: "litterfall" must be provided to TCF() or "dates" must be provided at runtime' assert len(dates) >= 365 and drivers[0].shape[-1] >= 365,\ 'At least 365 daily time steps must be provided to allow computation of annual NPP sum' assert hasattr(dates[0], 'year') and hasattr(dates[0], 'strftime'),\ 'The values of "dates" must be datetime.date or datetime.datetime instances' # GPP can be computed matrix-wise, in a single time step gpp = self.gpp(drivers[0:6]) npp = self.params.CUE * gpp if litter is None: # Compute litterfall from the mean annual NPP sum npp_sum = climatology365(npp, dates).sum(axis = 0) self.constants.add('litterfall', npp_sum) # Pre-compute environmental constraints for soil RH tsoil, smsf = drivers[-2:] f_smsf = linear_constraint(self.params.smsf0, self.params.smsf1) # Swap axes here only to make time the major (first) axis tmult = arrhenius(tsoil, self.params.tsoil).swapaxes(0, 1) wmult = f_smsf(smsf).swapaxes(0, 1) return (gpp, npp, tmult, wmult) def check_drivers(self, drivers: Sequence): ''' Checks that driver datasets have correct shape and units. Issues warnings otherwise. Parameters ---------- drivers : Sequence Flat sequence of drivers or (P x ...) array for P drivers ''' n = len(drivers) _drivers = dict(zip(self.required_drivers[0:n], drivers)) if 'fPAR' in _drivers.keys(): if np.nanmax(_drivers['fPAR']) > 1: warnings.warn('WARNING: fPAR might not have correct units; maximum value exceeds 1.0') if 'SMRZ' in _drivers.keys(): if np.nanmax(_drivers['SMRZ']) <= 1: warnings.warn('WARNING: Root-zone soil moisture might not have correct units; maximum value is less than 1.0') if 'SMSF' in _drivers.keys(): if np.nanmax(_drivers['SMSF']) <= 1: warnings.warn('WARNING: Surface soil moisture might not have correct units; maximum value is less than 1.0') def diagnose_emult(self, drivers): ''' Returns the environmental constraint multiplier on GPP (Emult). This dimensionless quantity indicates the aggregate impact of meteorological conditions on GPP. Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) Returns ------- tuple 4-tuple of (FT, Tmin, VPD, SMRZ) environmental constraints ''' if len(drivers) == 5: fpar, par, tmin, vpd, smrz0 = drivers ft0 = np.where(tmin < 273.15, 0, 1) else: fpar, par, tmin, vpd, smrz0, ft0 = drivers # Rescale root-zone soil moisture smrz = self._rescale_smrz(smrz0, np.nanmin(smrz0, axis = -1)) # Convert freeze-thaw flag to a multiplier (always 1 when thawed but # potentially non-zero and less than 1 when thawed) ft = np.where(ft0 == 0, self.params.ft0, 1) # Get a function that constrains each met. driver to [0, 1] f_tmin = linear_constraint(self.params.tmin0, self.params.tmin1)(tmin) f_vpd = linear_constraint( self.params.vpd0, self.params.vpd1, 'reversed')(vpd) f_smrz = linear_constraint(self.params.smrz0, self.params.smrz1)(smrz) # Compute the environmental constraint return (ft, f_tmin, f_vpd, f_smrz) def diagnose_kmult(self, drivers): ''' Returns the environmental constraint multiplier on RH (Kmult). This dimensionless quantity indicates the aggregate impact of meteorological conditions on heterotrophic respiration (RH). Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %] Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) Returns ------- tuple 2-tuple of (Tmult, Wmult), or (Tsoil, SMSF), environmental constraints ''' tsoil, smsf = drivers[-2:] f_smsf = linear_constraint(self.params.smsf0, self.params.smsf1) # Swap axes here only to make time the major (first) axis tmult = arrhenius(tsoil, self.params.tsoil) wmult = f_smsf(smsf) return (tmult, wmult) def forward_run( self, drivers: Sequence, state: Sequence = None, dates: Sequence = None, track_state: bool = False, verbose: bool = True ) -> np.ndarray: ''' Runs the TCF model forward in time for daily time steps. This is the recommended interface for most users. If `litterfall` was not provided to `TCF` at initialization, it will be necessary to provide at least 365 daily steps and the `years` of each time step. Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %] GPP calculation is vectorized but RH and NEE calculation proceed step-wise because they depend on the model state (SOC). Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) state : Sequence or numpy.ndarray or None A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool dates : Sequence or numpy.ndarray or None If `litterfall` was not provided to `TCF` during initialization, you must provide a sequence of `datetime.date` instances, of length T for T time steps, indicating the current year of each time step. track_state : bool True to track (soil organic carbon) state at each time step, rather than only tracking the current state (Default: False) verbose : bool True to show a progress bar and other messages (Default: True) Returns ------- tuple A 3-element tuple of (NEE, GPP, RH) ''' if drivers[0].shape[-2] > drivers[0].shape[-1]: warnings.warn('WARNING: Axes of input arrays might not be in the required (..., N, T) order') # NOTE: Allowing for state variables other than SOC to be included in # a later version soc = state if soc is None: soc = self.state.soc # NOTE: self.constants.litterfall is also initialized in this function gpp, npp, tmult, wmult = self._setup_forward(drivers, state, dates) # Pre-allocate output arrays rh = np.ones((3, *gpp.shape), dtype = np.float32) # (3 x N x T) nee = np.ones((*gpp.shape,), dtype = np.float32) # (N x T) # Determine the number of forward time steps if dates is not None: steps = np.arange(0, len(dates)) else: try: steps = np.arange(0, drivers[0].shape[-1]) except: raise ValueError('Could not determine number of time steps; provide a "dates" argument') # Create a way to track SOC state over time if track_state: if not hasattr(self.history, 'soc'): self.history.add('soc', soc[...,np.newaxis].repeat( steps.size, axis = -1).astype(np.float32)) litter0 = self.constants.litterfall # Available annual litterfall litter = litter0 / 365 # Default: Allocate equal fraction every day for t in tqdm(steps, disable = not verbose): # If litterfall is scheduled: is a varying fraction of the annual if self.constants.litterfall_rate is not None: litter = litter0 * self.constants.litterfall_rate[:,t] if track_state: self.history.soc[...,t] = soc rh_t = np.empty((3, litter.shape[0])) # Allocate RH(t) array for pool in range(0, soc.shape[0]): rh_t[pool] = self.params.decay_rates[pool] *\ wmult[t] * tmult[t] * soc[pool] # Compute SOC change dc1 = (litter * self.params.f_metabolic.T) - rh_t[0,...] dc2 = (litter * (1 - self.params.f_metabolic.T)) - rh_t[1,...] dc3 = (self.params.f_structural.T * rh_t[1,...]) - rh_t[2,...] for i, delta in enumerate([dc1, dc2, dc3]): delta[np.isnan(delta)] = 0 # Protect against NaN contamination soc[i] += delta[0] # "the adjustment...to account for material transferred into the slow # pool during humification" (Jones et al. 2017, TGARS, p.5); note # that this is a loss FROM the "medium" (structural) pool rh_t[1,...] = rh_t[1,...] * (1 - self.params.f_structural.T) # Record RH and NEE at this time step rh[...,t] = rh_t nee[...,t] = rh_t.sum(axis = 0) - npp[...,t] return (nee, gpp, rh) def gpp(self, drivers: Sequence) -> np.ndarray: ''' Calculates gross primary production (GPP) under prevailing climatic climatic conditions. Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] The FT state is optional; if that axis of the data cube is not provided, FT state will be calculated from Tmin using a threshold of 32 degrees F (273.15 deg K). Unit of time should be consistent with the units of PAR. For example, if PAR is given as [MJ m-2 day-1], then GPP will be in units of [g C m-2 day-1]. It's assumed the other driver data are representative of that time step (e.g., daily averages). GPP doesn't depend on model state, so it can be estimated for an arbitrary number of time steps. Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) Returns ------- numpy.ndarray Gross primary production (GPP) in [g C m-2 time-1] where time is the time step of the PAR data, e.g., [g C m-2 day-1] ''' self.check_drivers(drivers) if len(drivers) == 5: fpar, par, _, _, _ = drivers else: fpar, par, _, _, _, _ = drivers ft, f_tmin, f_vpd, f_smrz = self.diagnose_emult(drivers) emult = ft * f_tmin * f_vpd * f_smrz return par * fpar * emult * self.params.LUE def rh( self, drivers: Sequence, state: Sequence = None ) -> np.ndarray: ''' Calculates heterotrophic respiration (RH) based on the available soil organic carbon (SOC) state and prevailing climatic conditions. This calculation is NOT vectorized, so can only be applied to a single time point. Order of driver variables should be: Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %] Unit of time should be consistent with the units of PAR and the turnover time (`decay_rate`). It's assumed that PAR is denominated by daily time steps, so RH would be given in [g C m-2 day-1]. Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) state : Sequence or numpy.ndarray or None A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool Returns ------- numpy.ndarray A (3 x N) array representing the RH flux from each SOC pool, in units of [g C m-2] per unit time, most likely [g C m-2 day-1] ''' if hasattr(drivers, 'ndim'): assert drivers.ndim <= 2,\ 'TCF.rh() computes a single time step; only (P x N) driver arrays should be provided' # NOTE: Allowing for state variables other than SOC to be included in # a later version soc = state if soc is None: soc = self.state.soc tsoil, smsf = drivers # Unpack met. drivers # Take transpose of parameter vectors here because the driver datasets # are cross-sectional; i.e., smsf and tsoil are 1D vectors f_smsf = linear_constraint(self.params.smsf0.T, self.params.smsf1.T) tmult = arrhenius(tsoil, self.params.tsoil.T) wmult = f_smsf(smsf) rh = wmult * tmult * self.params.decay_rates * soc # "the adjustment...to account for material transferred into the slow # pool during humification" (Jones et al. 2017, TGARS, p.5); note # that this is a loss FROM the "medium" (structural) pool rh[1,...] = rh[1,...] * (1 - self.params.f_structural.T) return rh def setup_litterfall( self, dates: Sequence, litterfall_rate: Sequence, period_days: int = 8): ''' Create a schedule for daily litterfall allocation. This step is optional and, without it, the default behavior is to allocate an equal daily fraction (i.e., 1/365) of total annual litterfall. The resulting litterfall schedule is stored in the `constants` namespace as `constants.litterfall_rate`. Parameters ---------- dates : Sequence or numpy.ndarray A sequence of `datetime.date` instances, of length T for T time steps, indicating the current year of each time step. litterfall_rate : numpy.ndarray An (P x N) array specifying the litterfall rate in each period (P periods per year) for N pixels. There are 46 8-day periods in a year. period_days : int The number of days; defaults to 8 (8-day periods) to match the 8-day composite cycle of MODIS/VIIRS datasets ''' n_periods = 1 + (365 // period_days) # e.g., 46 8-day periods in 365 days _preamble = f'Expected "litterfall_rate" to be shape ({n_periods} x N)' assert litterfall_rate.shape[0] == n_periods,\ f'{_preamble} but first axis does not have {n_periods} elements' # Get a sequence of (e.g., 8-day) periods periods = np.arange(0, n_periods)\ .reshape((n_periods, 1)).repeat(period_days, axis = 1) periods = periods.ravel()[:365] jdates = np.array(ordinals365(dates)) - 1 assert jdates.max() == 364 # Python starts counting at zero # Index into periods [0,45], which can then index into litter_rate, # a (46 x N) array, to produce an (T x N) array, then swap axes litterfall_rate = litterfall_rate[periods[jdates],:].swapaxes(0, 1) # Finally, convert litterfall rate from (1/period_days) to (1/day); # i.e., for an 8-day period, instead of "this amount per 8 days" # we want a daily amount litterfall_rate /= period_days # Check that, for a given year, 100% of litterfall is allocated; this # is like a checksum for the schedule: The fractions in a year # should add up to (close to) 1.0 n_years = len(dates) // 365 # Expected number of years assert (np.abs(litterfall_rate.sum(axis = -1) - n_years) < 1).all(),\ f'Summing daily "litterfall_rate" over time did not result in 100% of litterfall allocated each year; check data values or the value of "period_days"' self.constants.add('litterfall_rate', litterfall_rate) def spin_up( self, dates: Sequence, drivers: Sequence, state: Sequence = None, max_cycles: int = 1000, threshold: float = 1, verbose: bool = True, verbose_type = 'tqdm' ) -> np.ndarray: ''' Repeatedly cycle climatology until SOC state reaches equilibrium. See `TCF.forward_run()` for details on `drivers` and `state` arguments. Parameters ---------- dates : Sequence or numpy.ndarray A sequence of `datetime.date` instances, of length T for T time steps drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) state : Sequence or numpy.ndarray or None A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool max_cycles : int Maximum number of climatology cycles (365-day years) to apply (Default: 100) threshold : float Threshold for inter-annual change in NEE [g C m-2 year-1]; when the difference in the annual NEE sum is less than this number, spin-up is complete (Default: 1 g C m-2 year-1) verbose : bool True to show a progress bar and other messages (Default: True) Returns ------- numpy.ndarray The history of change in annual NEE ''' # NOTE: Allowing for state variables other than SOC to be included in # a later version if state is None: assert hasattr(self.state, 'soc'), 'No prior soil organic carbon ("soc") state defined' else: self.state.soc = state soc = self.state.soc clim = [] for each in drivers: # Reshape the (365 x ...) arrays to (... x 365) arrays after # calculating a climatology clim.append(climatology365(each, dates).swapaxes(0, -1)) tolerance = np.nan * np.ones((soc.shape[-1], max_cycles), np.float32) disable = (not verbose or not verbose_type == 'tqdm') for cycle in tqdm(range(0, max_cycles), disable = disable): nee, gpp, rh = self.forward_run(drivers, soc, dates, verbose = False) # Diagnostics # rh_sum = rh.sum(axis = 0).sum(axis = -1) # npp_sum = (gpp * self.params.CUE).sum(axis = -1) nee_sum = np.nansum(nee, axis = -1) if cycle == 0: nee_last = nee_sum else: tolerance[:,cycle] = (nee_last - nee_sum) nee_last = nee_sum if (np.abs(tolerance[:,cycle]) < threshold).all(): break # Diagnostics # rh_track[:,step] = rh_sum # npp_track[:,step] = npp_sum # soc_track[:,step] = self.state.soc.sum(axis = 0) if cycle > 0 and verbose and verbose_type != 'tqdm': print( 'Change in annual NEE sum [SOC state]: %.2f, [%.0f]' % (np.nanmean(tolerance[:,cycle]), self.state.soc.sum())) return tolerance
The Terrestrial Carbon Flux (TCF) model, which is the basis for the NASA Soil Moisture Active Passive (SMAP) Level 4 Carbon (L4C) model. TCF incorporates these basic assumptions:
- Carbon assimilation (gross primary production) is linearly related to the amount of solar radiation and green photosynthetic land cover.
- There is a maximum rate of carbon assimilation. Low water availability, cold or freezing temperatures, and high atmospheric demand for water vapor each reduce the efficiency of carbon assimilation.
- Soil organic carbon (SOC) decomposes at different rates based on the type of material. The optimal rate of decomposition is fixed and depends only on the land-cover type.
- The efficiency of soil organic carbon decomposition is reduced under low soil temperatures or low soil moisture.
A complete description is available in Kimball et al. (2009) and in Jones et al. (2017). Soil organic carbon (SOC) and soil decomposition are discussed in Endsley et al. (2020) and Endsley et al. (2022).
If initial model "state" is provided it should be a 2D array with the first axis containing, in order:
- Initial state of "metabolic" SOC pool
- Initial state of "structural" SOC pool
- Initial state of "recalcitrant" SOC pool
NOTE: For developers,
self.params
refers to model parameters that have been vectorized to improve performance. If a function performs vectorized calculations on longitudinal (N x T) arrays, the vectorized parameter(s) can be used with their current shape. For cross-sectional calculations, e.g., on (N,) or (1 x N) arrays, make sure to use the transpose,self.params.name.T
, wherename
is the parameter name.Example use for a single pixel:
tcf = TCF(params, land_cover_map = [7]) tcf.nee(state = [500, 500, 1000], drivers = [...])
Parameters
params
:dict
- Dictionary of model parameters; keys should be among
TCF.required_parameters
and values should be sequences ordered by PFT code, either 9 values (value at index 0 beingnp.nan
) or as many values as there are keys inTCF.valid_pft
land_cover_map
:Sequence
ornumpy.ndarray
- 1-dimensional sequence of one or more land-cover types, one for each model resolution cell (pixel). These types should be represented as integers (unsigned 16-bit type or lower).
state
:Sequence
ornumpy.ndarray
orNone
- A sequence of 3 values or a 2D (S x N) array representing the state variables for each of N pixels. The first three (3) state variables should be the initial states of each SOC pool.
litterfall
:Sequence
ornumpy.ndarray
orNone
- A sequence of values or 1D array representing average annual litterfall (units: g C m-2 year-1) for each model resolution cell (pixel). If not provided, it will be based on the climatological NPP (when it is calculated).
Class variables
var num_soc_pools
var required_drivers
var required_parameters
var valid_pft
var version
Methods
def check_drivers(self, drivers: Sequence)
-
Expand source code
def check_drivers(self, drivers: Sequence): ''' Checks that driver datasets have correct shape and units. Issues warnings otherwise. Parameters ---------- drivers : Sequence Flat sequence of drivers or (P x ...) array for P drivers ''' n = len(drivers) _drivers = dict(zip(self.required_drivers[0:n], drivers)) if 'fPAR' in _drivers.keys(): if np.nanmax(_drivers['fPAR']) > 1: warnings.warn('WARNING: fPAR might not have correct units; maximum value exceeds 1.0') if 'SMRZ' in _drivers.keys(): if np.nanmax(_drivers['SMRZ']) <= 1: warnings.warn('WARNING: Root-zone soil moisture might not have correct units; maximum value is less than 1.0') if 'SMSF' in _drivers.keys(): if np.nanmax(_drivers['SMSF']) <= 1: warnings.warn('WARNING: Surface soil moisture might not have correct units; maximum value is less than 1.0')
Checks that driver datasets have correct shape and units. Issues warnings otherwise.
Parameters
drivers
:Sequence
- Flat sequence of drivers or (P x …) array for P drivers
def diagnose_emult(self, drivers)
-
Expand source code
def diagnose_emult(self, drivers): ''' Returns the environmental constraint multiplier on GPP (Emult). This dimensionless quantity indicates the aggregate impact of meteorological conditions on GPP. Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) Returns ------- tuple 4-tuple of (FT, Tmin, VPD, SMRZ) environmental constraints ''' if len(drivers) == 5: fpar, par, tmin, vpd, smrz0 = drivers ft0 = np.where(tmin < 273.15, 0, 1) else: fpar, par, tmin, vpd, smrz0, ft0 = drivers # Rescale root-zone soil moisture smrz = self._rescale_smrz(smrz0, np.nanmin(smrz0, axis = -1)) # Convert freeze-thaw flag to a multiplier (always 1 when thawed but # potentially non-zero and less than 1 when thawed) ft = np.where(ft0 == 0, self.params.ft0, 1) # Get a function that constrains each met. driver to [0, 1] f_tmin = linear_constraint(self.params.tmin0, self.params.tmin1)(tmin) f_vpd = linear_constraint( self.params.vpd0, self.params.vpd1, 'reversed')(vpd) f_smrz = linear_constraint(self.params.smrz0, self.params.smrz1)(smrz) # Compute the environmental constraint return (ft, f_tmin, f_vpd, f_smrz)
Returns the environmental constraint multiplier on GPP (Emult). This dimensionless quantity indicates the aggregate impact of meteorological conditions on GPP. Order of driver variables should be:
Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed]
Parameters
drivers
:Sequence
ornumpy.ndarray
- Either a flat sequence of P driver variables, each an array with (N x T) or (… x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T)
Returns
tuple
- 4-tuple of (FT, Tmin, VPD, SMRZ) environmental constraints
def diagnose_kmult(self, drivers)
-
Expand source code
def diagnose_kmult(self, drivers): ''' Returns the environmental constraint multiplier on RH (Kmult). This dimensionless quantity indicates the aggregate impact of meteorological conditions on heterotrophic respiration (RH). Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %] Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) Returns ------- tuple 2-tuple of (Tmult, Wmult), or (Tsoil, SMSF), environmental constraints ''' tsoil, smsf = drivers[-2:] f_smsf = linear_constraint(self.params.smsf0, self.params.smsf1) # Swap axes here only to make time the major (first) axis tmult = arrhenius(tsoil, self.params.tsoil) wmult = f_smsf(smsf) return (tmult, wmult)
Returns the environmental constraint multiplier on RH (Kmult). This dimensionless quantity indicates the aggregate impact of meteorological conditions on heterotrophic respiration (RH). Order of driver variables should be:
Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %]
Parameters
drivers
:Sequence
ornumpy.ndarray
- Either a flat sequence of P driver variables, each an array with (N x T) or (… x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T)
Returns
tuple
- 2-tuple of (Tmult, Wmult), or (Tsoil, SMSF), environmental constraints
def forward_run(self,
drivers: Sequence,
state: Sequence = None,
dates: Sequence = None,
track_state: bool = False,
verbose: bool = True) ‑> numpy.ndarray-
Expand source code
def forward_run( self, drivers: Sequence, state: Sequence = None, dates: Sequence = None, track_state: bool = False, verbose: bool = True ) -> np.ndarray: ''' Runs the TCF model forward in time for daily time steps. This is the recommended interface for most users. If `litterfall` was not provided to `TCF` at initialization, it will be necessary to provide at least 365 daily steps and the `years` of each time step. Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %] GPP calculation is vectorized but RH and NEE calculation proceed step-wise because they depend on the model state (SOC). Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) state : Sequence or numpy.ndarray or None A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool dates : Sequence or numpy.ndarray or None If `litterfall` was not provided to `TCF` during initialization, you must provide a sequence of `datetime.date` instances, of length T for T time steps, indicating the current year of each time step. track_state : bool True to track (soil organic carbon) state at each time step, rather than only tracking the current state (Default: False) verbose : bool True to show a progress bar and other messages (Default: True) Returns ------- tuple A 3-element tuple of (NEE, GPP, RH) ''' if drivers[0].shape[-2] > drivers[0].shape[-1]: warnings.warn('WARNING: Axes of input arrays might not be in the required (..., N, T) order') # NOTE: Allowing for state variables other than SOC to be included in # a later version soc = state if soc is None: soc = self.state.soc # NOTE: self.constants.litterfall is also initialized in this function gpp, npp, tmult, wmult = self._setup_forward(drivers, state, dates) # Pre-allocate output arrays rh = np.ones((3, *gpp.shape), dtype = np.float32) # (3 x N x T) nee = np.ones((*gpp.shape,), dtype = np.float32) # (N x T) # Determine the number of forward time steps if dates is not None: steps = np.arange(0, len(dates)) else: try: steps = np.arange(0, drivers[0].shape[-1]) except: raise ValueError('Could not determine number of time steps; provide a "dates" argument') # Create a way to track SOC state over time if track_state: if not hasattr(self.history, 'soc'): self.history.add('soc', soc[...,np.newaxis].repeat( steps.size, axis = -1).astype(np.float32)) litter0 = self.constants.litterfall # Available annual litterfall litter = litter0 / 365 # Default: Allocate equal fraction every day for t in tqdm(steps, disable = not verbose): # If litterfall is scheduled: is a varying fraction of the annual if self.constants.litterfall_rate is not None: litter = litter0 * self.constants.litterfall_rate[:,t] if track_state: self.history.soc[...,t] = soc rh_t = np.empty((3, litter.shape[0])) # Allocate RH(t) array for pool in range(0, soc.shape[0]): rh_t[pool] = self.params.decay_rates[pool] *\ wmult[t] * tmult[t] * soc[pool] # Compute SOC change dc1 = (litter * self.params.f_metabolic.T) - rh_t[0,...] dc2 = (litter * (1 - self.params.f_metabolic.T)) - rh_t[1,...] dc3 = (self.params.f_structural.T * rh_t[1,...]) - rh_t[2,...] for i, delta in enumerate([dc1, dc2, dc3]): delta[np.isnan(delta)] = 0 # Protect against NaN contamination soc[i] += delta[0] # "the adjustment...to account for material transferred into the slow # pool during humification" (Jones et al. 2017, TGARS, p.5); note # that this is a loss FROM the "medium" (structural) pool rh_t[1,...] = rh_t[1,...] * (1 - self.params.f_structural.T) # Record RH and NEE at this time step rh[...,t] = rh_t nee[...,t] = rh_t.sum(axis = 0) - npp[...,t] return (nee, gpp, rh)
Runs the TCF model forward in time for daily time steps. This is the recommended interface for most users. If
litterfall
was not provided toTCF
at initialization, it will be necessary to provide at least 365 daily steps and theyears
of each time step. Order of driver variables should be:Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %]
GPP calculation is vectorized but RH and NEE calculation proceed step-wise because they depend on the model state (SOC).
Parameters
drivers
:Sequence
ornumpy.ndarray
- Either a flat sequence of P driver variables, each an array with (N x T) or (… x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T)
state
:Sequence
ornumpy.ndarray
orNone
- A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool
dates
:Sequence
ornumpy.ndarray
orNone
- If
litterfall
was not provided toTCF
during initialization, you must provide a sequence ofdatetime.date
instances, of length T for T time steps, indicating the current year of each time step. track_state
:bool
- True to track (soil organic carbon) state at each time step, rather than only tracking the current state (Default: False)
verbose
:bool
- True to show a progress bar and other messages (Default: True)
Returns
tuple
- A 3-element tuple of (NEE, GPP, RH)
def gpp(self, drivers: Sequence) ‑> numpy.ndarray
-
Expand source code
def gpp(self, drivers: Sequence) -> np.ndarray: ''' Calculates gross primary production (GPP) under prevailing climatic climatic conditions. Order of driver variables should be: Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed] The FT state is optional; if that axis of the data cube is not provided, FT state will be calculated from Tmin using a threshold of 32 degrees F (273.15 deg K). Unit of time should be consistent with the units of PAR. For example, if PAR is given as [MJ m-2 day-1], then GPP will be in units of [g C m-2 day-1]. It's assumed the other driver data are representative of that time step (e.g., daily averages). GPP doesn't depend on model state, so it can be estimated for an arbitrary number of time steps. Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) Returns ------- numpy.ndarray Gross primary production (GPP) in [g C m-2 time-1] where time is the time step of the PAR data, e.g., [g C m-2 day-1] ''' self.check_drivers(drivers) if len(drivers) == 5: fpar, par, _, _, _ = drivers else: fpar, par, _, _, _, _ = drivers ft, f_tmin, f_vpd, f_smrz = self.diagnose_emult(drivers) emult = ft * f_tmin * f_vpd * f_smrz return par * fpar * emult * self.params.LUE
Calculates gross primary production (GPP) under prevailing climatic climatic conditions. Order of driver variables should be:
Fraction of PAR intercepted (fPAR) [0-1] Photosynthetically active radation (PAR) [MJ m-2 day-1] Minimum temperature (Tmin) [deg K] Vapor pressure deficit (VPD) [Pa] UNSCALED Root-zone soil moisture wetness [percent, %] Freeze-thaw (FT) state of soil [0 = Frozen, 1 = Thawed]
The FT state is optional; if that axis of the data cube is not provided, FT state will be calculated from Tmin using a threshold of 32 degrees F (273.15 deg K).
Unit of time should be consistent with the units of PAR. For example, if PAR is given as [MJ m-2 day-1], then GPP will be in units of [g C m-2 day-1]. It's assumed the other driver data are representative of that time step (e.g., daily averages). GPP doesn't depend on model state, so it can be estimated for an arbitrary number of time steps.
Parameters
drivers
:Sequence
ornumpy.ndarray
- Either a flat sequence of P driver variables, each an array with (N x T) or (… x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T)
Returns
numpy.ndarray
- Gross primary production (GPP) in [g C m-2 time-1] where time is the time step of the PAR data, e.g., [g C m-2 day-1]
def rh(self, drivers: Sequence, state: Sequence = None) ‑> numpy.ndarray
-
Expand source code
def rh( self, drivers: Sequence, state: Sequence = None ) -> np.ndarray: ''' Calculates heterotrophic respiration (RH) based on the available soil organic carbon (SOC) state and prevailing climatic conditions. This calculation is NOT vectorized, so can only be applied to a single time point. Order of driver variables should be: Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %] Unit of time should be consistent with the units of PAR and the turnover time (`decay_rate`). It's assumed that PAR is denominated by daily time steps, so RH would be given in [g C m-2 day-1]. Parameters ---------- drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) state : Sequence or numpy.ndarray or None A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool Returns ------- numpy.ndarray A (3 x N) array representing the RH flux from each SOC pool, in units of [g C m-2] per unit time, most likely [g C m-2 day-1] ''' if hasattr(drivers, 'ndim'): assert drivers.ndim <= 2,\ 'TCF.rh() computes a single time step; only (P x N) driver arrays should be provided' # NOTE: Allowing for state variables other than SOC to be included in # a later version soc = state if soc is None: soc = self.state.soc tsoil, smsf = drivers # Unpack met. drivers # Take transpose of parameter vectors here because the driver datasets # are cross-sectional; i.e., smsf and tsoil are 1D vectors f_smsf = linear_constraint(self.params.smsf0.T, self.params.smsf1.T) tmult = arrhenius(tsoil, self.params.tsoil.T) wmult = f_smsf(smsf) rh = wmult * tmult * self.params.decay_rates * soc # "the adjustment...to account for material transferred into the slow # pool during humification" (Jones et al. 2017, TGARS, p.5); note # that this is a loss FROM the "medium" (structural) pool rh[1,...] = rh[1,...] * (1 - self.params.f_structural.T) return rh
Calculates heterotrophic respiration (RH) based on the available soil organic carbon (SOC) state and prevailing climatic conditions. This calculation is NOT vectorized, so can only be applied to a single time point. Order of driver variables should be:
Soil temperature in the top (0-5 cm) layer [deg K] Surface soil moisture wetness [percent, %]
Unit of time should be consistent with the units of PAR and the turnover time (
decay_rate
). It's assumed that PAR is denominated by daily time steps, so RH would be given in [g C m-2 day-1].Parameters
drivers
:Sequence
ornumpy.ndarray
- Either a flat sequence of P driver variables, each an array with (N x T) or (… x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T)
state
:Sequence
ornumpy.ndarray
orNone
- A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool
Returns
numpy.ndarray
- A (3 x N) array representing the RH flux from each SOC pool, in units of [g C m-2] per unit time, most likely [g C m-2 day-1]
def setup_litterfall(self, dates: Sequence, litterfall_rate: Sequence, period_days: int = 8)
-
Expand source code
def setup_litterfall( self, dates: Sequence, litterfall_rate: Sequence, period_days: int = 8): ''' Create a schedule for daily litterfall allocation. This step is optional and, without it, the default behavior is to allocate an equal daily fraction (i.e., 1/365) of total annual litterfall. The resulting litterfall schedule is stored in the `constants` namespace as `constants.litterfall_rate`. Parameters ---------- dates : Sequence or numpy.ndarray A sequence of `datetime.date` instances, of length T for T time steps, indicating the current year of each time step. litterfall_rate : numpy.ndarray An (P x N) array specifying the litterfall rate in each period (P periods per year) for N pixels. There are 46 8-day periods in a year. period_days : int The number of days; defaults to 8 (8-day periods) to match the 8-day composite cycle of MODIS/VIIRS datasets ''' n_periods = 1 + (365 // period_days) # e.g., 46 8-day periods in 365 days _preamble = f'Expected "litterfall_rate" to be shape ({n_periods} x N)' assert litterfall_rate.shape[0] == n_periods,\ f'{_preamble} but first axis does not have {n_periods} elements' # Get a sequence of (e.g., 8-day) periods periods = np.arange(0, n_periods)\ .reshape((n_periods, 1)).repeat(period_days, axis = 1) periods = periods.ravel()[:365] jdates = np.array(ordinals365(dates)) - 1 assert jdates.max() == 364 # Python starts counting at zero # Index into periods [0,45], which can then index into litter_rate, # a (46 x N) array, to produce an (T x N) array, then swap axes litterfall_rate = litterfall_rate[periods[jdates],:].swapaxes(0, 1) # Finally, convert litterfall rate from (1/period_days) to (1/day); # i.e., for an 8-day period, instead of "this amount per 8 days" # we want a daily amount litterfall_rate /= period_days # Check that, for a given year, 100% of litterfall is allocated; this # is like a checksum for the schedule: The fractions in a year # should add up to (close to) 1.0 n_years = len(dates) // 365 # Expected number of years assert (np.abs(litterfall_rate.sum(axis = -1) - n_years) < 1).all(),\ f'Summing daily "litterfall_rate" over time did not result in 100% of litterfall allocated each year; check data values or the value of "period_days"' self.constants.add('litterfall_rate', litterfall_rate)
Create a schedule for daily litterfall allocation. This step is optional and, without it, the default behavior is to allocate an equal daily fraction (i.e., 1/365) of total annual litterfall. The resulting litterfall schedule is stored in the
constants
namespace asconstants.litterfall_rate
.Parameters
dates
:Sequence
ornumpy.ndarray
- A sequence of
datetime.date
instances, of length T for T time steps, indicating the current year of each time step. litterfall_rate
:numpy.ndarray
- An (P x N) array specifying the litterfall rate in each period (P periods per year) for N pixels. There are 46 8-day periods in a year.
period_days
:int
- The number of days; defaults to 8 (8-day periods) to match the 8-day composite cycle of MODIS/VIIRS datasets
def spin_up(self,
dates: Sequence,
drivers: Sequence,
state: Sequence = None,
max_cycles: int = 1000,
threshold: float = 1,
verbose: bool = True,
verbose_type='tqdm') ‑> numpy.ndarray-
Expand source code
def spin_up( self, dates: Sequence, drivers: Sequence, state: Sequence = None, max_cycles: int = 1000, threshold: float = 1, verbose: bool = True, verbose_type = 'tqdm' ) -> np.ndarray: ''' Repeatedly cycle climatology until SOC state reaches equilibrium. See `TCF.forward_run()` for details on `drivers` and `state` arguments. Parameters ---------- dates : Sequence or numpy.ndarray A sequence of `datetime.date` instances, of length T for T time steps drivers : Sequence or numpy.ndarray Either a flat sequence of P driver variables, each an array with (N x T) or (... x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T) state : Sequence or numpy.ndarray or None A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool max_cycles : int Maximum number of climatology cycles (365-day years) to apply (Default: 100) threshold : float Threshold for inter-annual change in NEE [g C m-2 year-1]; when the difference in the annual NEE sum is less than this number, spin-up is complete (Default: 1 g C m-2 year-1) verbose : bool True to show a progress bar and other messages (Default: True) Returns ------- numpy.ndarray The history of change in annual NEE ''' # NOTE: Allowing for state variables other than SOC to be included in # a later version if state is None: assert hasattr(self.state, 'soc'), 'No prior soil organic carbon ("soc") state defined' else: self.state.soc = state soc = self.state.soc clim = [] for each in drivers: # Reshape the (365 x ...) arrays to (... x 365) arrays after # calculating a climatology clim.append(climatology365(each, dates).swapaxes(0, -1)) tolerance = np.nan * np.ones((soc.shape[-1], max_cycles), np.float32) disable = (not verbose or not verbose_type == 'tqdm') for cycle in tqdm(range(0, max_cycles), disable = disable): nee, gpp, rh = self.forward_run(drivers, soc, dates, verbose = False) # Diagnostics # rh_sum = rh.sum(axis = 0).sum(axis = -1) # npp_sum = (gpp * self.params.CUE).sum(axis = -1) nee_sum = np.nansum(nee, axis = -1) if cycle == 0: nee_last = nee_sum else: tolerance[:,cycle] = (nee_last - nee_sum) nee_last = nee_sum if (np.abs(tolerance[:,cycle]) < threshold).all(): break # Diagnostics # rh_track[:,step] = rh_sum # npp_track[:,step] = npp_sum # soc_track[:,step] = self.state.soc.sum(axis = 0) if cycle > 0 and verbose and verbose_type != 'tqdm': print( 'Change in annual NEE sum [SOC state]: %.2f, [%.0f]' % (np.nanmean(tolerance[:,cycle]), self.state.soc.sum())) return tolerance
Repeatedly cycle climatology until SOC state reaches equilibrium. See
TCF.forward_run()
for details ondrivers
andstate
arguments.Parameters
dates
:Sequence
ornumpy.ndarray
- A sequence of
datetime.date
instances, of length T for T time steps drivers
:Sequence
ornumpy.ndarray
- Either a flat sequence of P driver variables, each an array with (N x T) or (… x N x T) shape for N sites and T time steps; or a single array, with shape (P x N) or (P x N x T)
state
:Sequence
ornumpy.ndarray
orNone
- A sequence of 3 values or an (3 x N) array representing the SOC state in each SOC pool
max_cycles
:int
- Maximum number of climatology cycles (365-day years) to apply (Default: 100)
threshold
:float
- Threshold for inter-annual change in NEE [g C m-2 year-1]; when the difference in the annual NEE sum is less than this number, spin-up is complete (Default: 1 g C m-2 year-1)
verbose
:bool
- True to show a progress bar and other messages (Default: True)
Returns
numpy.ndarray
- The history of change in annual NEE