fluxdataqaqc 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {fluxdataqaqc-0.3.2/fluxdataqaqc.egg-info → fluxdataqaqc-0.3.3}/PKG-INFO +1 -1
  2. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Basic_usage/AMF_US-Tw3_BASE_HH_5-5.csv +1 -1
  3. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Config_options/data_for_multiple_soil_vars.csv +1 -1
  4. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc/__init__.py +1 -1
  5. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc/qaqc.py +131 -219
  6. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3/fluxdataqaqc.egg-info}/PKG-INFO +1 -1
  7. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/tests/test_fluxdataqaqc.py +0 -93
  8. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/LICENSE.txt +0 -0
  9. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/MANIFEST.in +0 -0
  10. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/README.rst +0 -0
  11. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/environment.yml +0 -0
  12. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Basic_usage/FLX_US-AR1_FLUXNET2015_SUBSET_DD_2009-2012_1-3.xlsx +0 -0
  13. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Basic_usage/US-Tw3_config.ini +0 -0
  14. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Basic_usage/USGS_config.ini +0 -0
  15. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Basic_usage/fluxnet_config.ini +0 -0
  16. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Basic_usage/raw_subhour_DVDV_10.xlsx +0 -0
  17. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Config_options/config_for_QC_flag_filtering.ini +0 -0
  18. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Config_options/config_for_multiple_soil_vars.ini +0 -0
  19. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/examples/Config_options/data_for_QC_flag_filtering.csv +0 -0
  20. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc/data.py +0 -0
  21. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc/plot.py +0 -0
  22. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc/util.py +0 -0
  23. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc.egg-info/SOURCES.txt +0 -0
  24. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc.egg-info/dependency_links.txt +0 -0
  25. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc.egg-info/requires.txt +0 -0
  26. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/fluxdataqaqc.egg-info/top_level.txt +0 -0
  27. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/requirements.txt +0 -0
  28. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/setup.cfg +0 -0
  29. {fluxdataqaqc-0.3.2 → fluxdataqaqc-0.3.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxdataqaqc
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Tools for QA/QC of eddy covariance station data
5
5
  Home-page: https://github.com/Open-ET/flux-data-qaqc
6
6
  Author: John Volk
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: fluxdataqaqc-0.3.2/examples/Basic_usage/AMF_US-Tw3_BASE_HH_5-5.csv
2
+ name: fluxdataqaqc-0.3.3/examples/Basic_usage/AMF_US-Tw3_BASE_HH_5-5.csv
3
3
  size: 38180927 bytes
4
4
  sha256: 0febe82fef616bd6f4b7664a971d2e6316a4f2f42090a80e2c43279102bfd383
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: fluxdataqaqc-0.3.2/examples/Config_options/data_for_multiple_soil_vars.csv
2
+ name: fluxdataqaqc-0.3.3/examples/Config_options/data_for_multiple_soil_vars.csv
3
3
  size: 44901633 bytes
4
4
  sha256: 4c372d81a79b2b0f474ef6184fc2d84e3b8d40520c798196b1633e640ece4861
@@ -4,7 +4,7 @@ Tools for performing energy balance correction and QA/QC of eddy covariance flux
4
4
 
5
5
  __name__ = 'fluxdataqaqc'
6
6
  __author__ = 'John Volk'
7
- __version__ = '0.3.2'
7
+ __version__ = '0.3.3'
8
8
 
9
9
 
10
10
  from fluxdataqaqc.data import Data
@@ -275,7 +275,6 @@ class QaQc(Plot, Convert):
275
275
  'ebr_corr',
276
276
  'ebr_user_corr',
277
277
  'ebc_cf',
278
- 'ebc_cf_method',
279
278
  'ebr_5day_clim',
280
279
  'flux',
281
280
  'flux_corr',
@@ -941,11 +940,7 @@ class QaQc(Plot, Convert):
941
940
  # rename columns to internal names
942
941
  df = self._df.rename(columns=self.inv_map).copy()
943
942
  # avoid including string QC flags because of float forcing on resample
944
- # also skip the daily energy balance correction method flag
945
- numeric_cols = [
946
- c for c in df.columns
947
- if not '_qc_flag' in c and not c == 'ebc_cf_method'
948
- ]
943
+ numeric_cols = [c for c in df.columns if not '_qc_flag' in c]
949
944
  sum_cols = [k for k,v in self.agg_dict.items() if v == 'sum']
950
945
  # to avoid warning/error of missing columns
951
946
  sum_cols = list(set(sum_cols).intersection(df.columns))
@@ -1143,13 +1138,10 @@ class QaQc(Plot, Convert):
1143
1138
 
1144
1139
  Note:
1145
1140
  The *ebr_corr* variable or energy balance closure ratio is
1146
- calculated from the corrected versions of LE and H for the 'br'
1147
- and 'lin_regress' methods and for monthly data. For daily data
1148
- with the 'ebr' method it is the filtered and smoothed energy
1149
- balance ratio, i.e. the inverse of the correction factor that is
1150
- applied to the raw H and LE, which is saved as *ebc_cf*. The
1151
- method used to get each day's correction factor (1-4, see
1152
- :ref:`Closure Methodologies`) is saved as *ebc_cf_method*.
1141
+ calculated from the corrected versions of LE and H independent
1142
+ of the method. When using the 'ebr' method the energy balance
1143
+ correction factor (what is applied to the raw H and LE) is left as
1144
+ calculated (inverse of ebr) and saved as *ebc_cf*.
1153
1145
 
1154
1146
  See Also:
1155
1147
  For explanation of the linear regression method see the
@@ -1703,25 +1695,8 @@ a_site Rn 6.99350781229883 1.552 1.054 0.943
1703
1695
  """
1704
1696
  Energy balance ratio correction method for daily LE, H, EBR, and ET.
1705
1697
 
1706
- Correct turbulent fluxes to close the energy balance following the
1707
- daily energy balance closure correction of FLUXNET2015 (ONEFlux)
1708
- described by Pastorello et al. (2020), with the additional limits on
1709
- the correction factor and corrected LE used by ``flux-data-qaqc``:
1710
-
1711
- 1. calculate the daily correction factor, CF = (Rn - G) / (H + LE),
1712
- and remove values outside of 1.5 times its interquartile range
1713
- 2. method 1, median CF in a +/- 7 day window with at least 5 values
1714
- 3. method 2, mean CF in a +/- 5 day window
1715
- 4. method 3, mean CF in +/- 5 days around the same day in the
1716
- previous and next years
1717
- 5. method 4 (not in FLUXNET), +/- 5 day moving mean of the day of
1718
- year mean CF from methods 1 and 2 over all years
1719
- 6. keep CF between 0.5 and 2 and multiply LE and H by CF, remove
1720
- days where corrected LE is <= -100 or >= 850 w/m2
1721
-
1722
- A CF outside of the limits from any method is not used and the next
1723
- method may fill that day. The method used for each day is saved as
1724
- "ebc_cf_method".
1698
+ Correct turblent fluxes to close energy balance using methods
1699
+ described by FLUXNET for daily H and LE.
1725
1700
 
1726
1701
  Updates :attr:`QaQc.df` and :attr:`QaQc.variables` attributes with new
1727
1702
  variables related to the corrections, e.g. LE_corr, ebr_corr, etc.
@@ -1733,68 +1708,129 @@ a_site Rn 6.99350781229883 1.552 1.054 0.943
1733
1708
  :obj:`None`
1734
1709
 
1735
1710
  """
1711
+ # moving windows FLUXNET methods 1, 2 and 3
1712
+ window_1 = 15
1713
+ window_2 = 11
1714
+ half_win_1 = window_1 // 2
1715
+ half_win_2 = window_2 // 2
1716
+
1736
1717
  # drop relavant calculated variables if they exist
1737
1718
  self._df = _drop_cols(self.df, self._eb_calc_vars)
1738
1719
  df = self._df.rename(columns=self.inv_map)
1739
- LE = df.LE.astype(float)
1740
- H = df.H.astype(float)
1741
- Rn = df.Rn.astype(float)
1742
- G = df.G.astype(float)
1743
-
1744
- # daily energy balance ratio and correction factor (its inverse)
1745
- df['ebr'] = (H + LE) / (Rn - G)
1746
- cf = (Rn - G) / (H + LE)
1747
- cf = cf.replace([np.inf, -np.inf], np.nan)
1748
-
1749
- # remove correction factors outside of 1.5 times the IQR
1750
- Q1 = cf.quantile(0.25)
1751
- Q3 = cf.quantile(0.75)
1720
+ # check if mean G, or heat storage measurements exist
1721
+ vars_to_use = ['LE','H','Rn','G']
1722
+ for el in ['SH', 'SLE', 'SG']:
1723
+ if el in df.columns:
1724
+ vars_to_use.append(el)
1725
+ df = df[vars_to_use].astype(float).copy()
1726
+
1727
+ # make copy of original data for later
1728
+ orig_df = df[['LE','H','Rn','G']].astype(float).copy()
1729
+ orig_df['ebr'] = (orig_df.H + orig_df.LE) / (orig_df.Rn - orig_df.G)
1730
+
1731
+ # compute IQR to filter out extreme ebrs,
1732
+ df['ebr'] = (df.H + df.LE) / (df.Rn - df.G)
1733
+ Q1 = df['ebr'].quantile(0.25)
1734
+ Q3 = df['ebr'].quantile(0.75)
1752
1735
  IQR = Q3 - Q1
1753
- cf = cf.where((cf >= Q1 - 1.5 * IQR) & (cf <= Q3 + 1.5 * IQR))
1754
-
1755
- # methods 1 and 2, moving windows around each day
1756
- cf_corr, cf_method = self._ebc_cf_moving_windows(cf)
1757
- # out of limit values are left as gaps for methods 3 and 4
1758
- out_of_limits = (cf_corr <= 0.5) | (cf_corr >= 2)
1759
- cf_corr[out_of_limits] = np.nan
1760
- cf_method[out_of_limits] = np.nan
1761
-
1762
- # method 3, same days in the previous and next years
1763
- cf_adjacent = self._ebc_cf_adjacent_years(cf)
1764
- # method 4, climatology of method 1 and 2 results for all years
1765
- cf_clim = self._ebc_cf_climatology(cf_corr)
1766
- cf_clim = pd.Series(
1767
- cf_clim.reindex(df.index.dayofyear).values, index=df.index
1736
+ # filter values between Q1-1.5IQR and Q3+1.5IQR
1737
+ mask = (df['ebr'] >= (Q1 - 1.5 * IQR)) & (df['ebr'] <= (Q3 + 1.5 * IQR))
1738
+ filtered = df[mask]
1739
+ # apply filter
1740
+ filtered_mask = filtered.index
1741
+ removed_mask = set(df.index) - set(filtered_mask)
1742
+ removed_mask = pd.to_datetime(list(removed_mask))
1743
+ df.loc[removed_mask] = np.nan
1744
+
1745
+ # FLUXNET methods 1 and 2 for filtering/smoothing ebr
1746
+ ebr = df.ebr.values
1747
+ df['ebr_corr'] = np.nan
1748
+ for i in range(len(ebr)):
1749
+ win_arr1 = ebr[i-half_win_1:i+half_win_1+1]
1750
+ win_arr2 = ebr[i-half_win_2:i+half_win_2+1]
1751
+ count = np.count_nonzero(~np.isnan(win_arr1))
1752
+ # get median of daily window1 if half window2 or more days exist
1753
+ if count >= half_win_2:
1754
+ val = np.nanpercentile(win_arr1, 50, axis=None)
1755
+ if abs(1/val) >= 2 or abs(1/val) <= 0.5:
1756
+ val = np.nan
1757
+ # if at least one day exists in window2 take mean
1758
+ elif np.count_nonzero(~np.isnan(win_arr2)) > 0:
1759
+ val = np.nanmedian(win_arr2)
1760
+ if abs(1/val) >= 2 or abs(1/val) <= 0.5:
1761
+ val = np.nan
1762
+ else:
1763
+ # assign nan for now, update with 5 day climatology
1764
+ val = np.nan
1765
+ # assign values if they were found in methods 1 or 2
1766
+ df.iloc[i, df.columns.get_loc('ebr_corr')] = val
1767
+ # make 5 day climatology of ebr for method 3
1768
+ # the cenetered window skips first and last 5 DOYs
1769
+ # so prepend and append first and last 5 days and loop...
1770
+ doy_ebr_mean=df['ebr_corr'].groupby(df.index.dayofyear).mean().copy()
1771
+ l5days = pd.Series(
1772
+ index=np.arange(-4,1), data=doy_ebr_mean.iloc[-5:].values)
1773
+ f5days = pd.Series(
1774
+ index=np.arange(367,372), data=doy_ebr_mean.iloc[:5].values)
1775
+ #doy_ebr_mean = doy_ebr_mean.append(f5days)
1776
+ doy_ebr_mean = pd.concat([doy_ebr_mean, f5days])
1777
+ doy_ebr_mean = pd.concat([l5days, doy_ebr_mean])
1778
+ ebr_5day_clim = pd.DataFrame(
1779
+ index=np.arange(1,367), columns=['ebr_5day_clim'])
1780
+ doy_ebr_mean = doy_ebr_mean.values
1781
+ for i in range(len(doy_ebr_mean)):
1782
+ # i = 0 which starts at prepended 5 days, shift window up
1783
+ win = doy_ebr_mean[i:i+2*half_win_2+1]
1784
+ count = np.count_nonzero(~np.isnan(win))
1785
+ # get 11 day moving window mean
1786
+ if i in ebr_5day_clim.index and count > 0:
1787
+ ebr_5day_clim.iloc[
1788
+ i-1, ebr_5day_clim.columns.get_loc('ebr_5day_clim')
1789
+ ] = np.nanmean(win)
1790
+ ebr_5day_clim['DOY'] = ebr_5day_clim.index
1791
+ ebr_5day_clim.index.name = 'date'
1792
+
1793
+ # fill gaps of 11 or more days in filtered EBR with 5 day clim
1794
+ df['DOY'] = df.index.dayofyear
1795
+ # datetime indices of all remaining null elements
1796
+ null_dates = df.loc[df.ebr_corr.isnull(), 'ebr_corr'].index
1797
+
1798
+ merged = pd.merge(
1799
+ df, ebr_5day_clim, left_on='DOY', right_index=True
1768
1800
  )
1769
-
1770
- # fill remaining gaps with method 3 then method 4, only correction
1771
- # factors between 0.5 and 2 are used
1772
- for method, cf_fill in [(3, cf_adjacent), (4, cf_clim)]:
1773
- cf_fill = cf_fill.where((cf_fill > 0.5) & (cf_fill < 2))
1774
- gaps = cf_corr.isna() & cf_fill.notna()
1775
- cf_corr[gaps] = cf_fill[gaps]
1776
- cf_method[gaps] = method
1777
-
1778
- # apply corrections to LE and H
1779
- df['LE_corr'] = LE * cf_corr
1780
- df['H_corr'] = H * cf_corr
1801
+ # assign 5 day climatology of EBR
1802
+ merged.loc[null_dates,'ebr_corr'] =\
1803
+ merged.loc[null_dates,'ebr_5day_clim'].astype(float)
1804
+ # replace raw variables with unfiltered dataframe copy
1805
+ merged.LE = orig_df.LE
1806
+ merged.H = orig_df.H
1807
+ merged.Rn = orig_df.Rn
1808
+ merged.G = orig_df.G
1809
+ merged.ebr = orig_df.ebr
1810
+ # calculated corrected EBR to assign to ebr_corr (not EBC_CF),
1811
+ # save CFs as defined by fluxnet method, i.e. inverse of EBR
1812
+ merged['ebc_cf'] = 1/merged.ebr_corr
1813
+ # filter out CF that are >=2 or <=0.5 (absolute)
1814
+ merged.loc[
1815
+ (abs(merged.ebc_cf) >= 2) | (abs(merged.ebc_cf <= 0.5)), 'ebc_cf'
1816
+ ] = np.nan
1817
+ # apply corrections to LE and H multiply by 1/EBR
1818
+ merged['LE_corr'] = merged.LE * merged.ebc_cf
1819
+ merged['H_corr'] = merged.H * merged.ebc_cf
1781
1820
  # filter out any corrected LE that <= -100 or >= 850 w/m2
1782
- # also removing corrected H and correction factor
1783
- bad_LE = (df.LE_corr >= 850) | (df.LE_corr <= -100)
1784
- df.loc[bad_LE, ['LE_corr', 'H_corr']] = np.nan
1785
- cf_corr[bad_LE] = np.nan
1786
- cf_method[bad_LE] = np.nan
1787
-
1788
- df['ebc_cf'] = cf_corr
1789
- df['ebc_cf_method'] = cf_method
1790
- # filtered and smoothed energy balance ratio, inverse of ebc_cf
1791
- df['ebr_corr'] = 1 / cf_corr
1792
- # 5 day climatology of the energy balance ratio (method 4)
1793
- df['ebr_5day_clim'] = 1 / cf_clim
1794
- df['flux_corr'] = df.LE_corr + df.H_corr
1821
+ # also removing corrected H, EBR, and EBC_CF
1822
+ merged.loc[
1823
+ (merged.LE_corr >= 850) | (merged.LE_corr <= -100), (
1824
+ 'LE_corr', 'H_corr', 'ebr_corr', 'ebc_cf'
1825
+ )
1826
+ ] = np.nan
1827
+ # compute EBR total turb flux
1828
+ merged['flux_corr'] = merged['LE_corr'] + merged['H_corr']
1829
+
1830
+ df = self._df.rename(columns=self.inv_map)
1795
1831
  # other variables needed for plots using raw data
1796
- df['flux'] = LE + H
1797
- df['energy'] = Rn - G
1832
+ df['flux'] = merged.LE + merged.H
1833
+ df['energy'] = merged.Rn - merged.G
1798
1834
 
1799
1835
  # corrected turbulent flux if given from input data
1800
1836
  if set(['LE_user_corr','H_user_corr']).issubset(df.columns):
@@ -1805,6 +1841,12 @@ a_site Rn 6.99350781229883 1.552 1.054 0.943
1805
1841
  flux_user_corr = 'flux_user_corr',
1806
1842
  ebr_user_corr = 'ebr_user_corr'
1807
1843
  )
1844
+ # grab select columns to merge into main dataframe
1845
+ cols = list(set(merged.columns).difference(df.columns))
1846
+ # join calculated data in
1847
+ merged = df.join(merged[cols], how='outer')
1848
+ # remove merge columns with suffix
1849
+ merged = merged.drop(columns=['DOY_x','DOY_y'])
1808
1850
 
1809
1851
  self.variables.update(
1810
1852
  energy = 'energy',
@@ -1815,144 +1857,14 @@ a_site Rn 6.99350781229883 1.552 1.054 0.943
1815
1857
  ebr = 'ebr',
1816
1858
  ebr_corr = 'ebr_corr',
1817
1859
  ebc_cf = 'ebc_cf',
1818
- ebc_cf_method = 'ebc_cf_method',
1819
1860
  ebr_5day_clim = 'ebr_5day_clim'
1820
1861
  )
1821
1862
 
1822
1863
  # revert column names to user's
1823
- self._df = df.rename(columns=self.variables)
1864
+ self._df = merged.rename(columns=self.variables)
1824
1865
  # update flag for other methods
1825
1866
  self.corrected = True
1826
-
1827
- @staticmethod
1828
- def _ebc_cf_moving_windows(cf, half_win_1=7, half_win_2=5,
1829
- min_count_1=5):
1830
- """
1831
- Energy balance closure correction factors from moving windows,
1832
- methods 1 and 2 of the FLUXNET2015 daily correction.
1833
-
1834
- Method 1 is the median of a +/- 7 day window when it has at least 5
1835
- values, otherwise method 2 is the mean of a +/- 5 day window.
1836
- Windows are cut off at the start and end of the record. Assumes a
1837
- continuous daily time series.
1838
-
1839
- Arguments:
1840
- cf (:obj:`pandas.Series`): daily correction factors with
1841
- outliers removed (null).
1842
-
1843
- Keyword Arguments:
1844
- half_win_1 (int): default 7. Half width of the method 1 window
1845
- [days].
1846
- half_win_2 (int): default 5. Half width of the method 2 window
1847
- [days].
1848
- min_count_1 (int): default 5. Minimum number of values in the
1849
- method 1 window.
1850
-
1851
- Returns:
1852
- cf_corr, cf_method (tuple): :obj:`pandas.Series` of correction
1853
- factors and the method used (1 or 2), null where the windows
1854
- have no values.
1855
- """
1856
- cf_vals = cf.values
1857
- cf_corr = np.full(len(cf_vals), np.nan)
1858
- cf_method = np.full(len(cf_vals), np.nan)
1859
-
1860
- for i in range(len(cf_vals)):
1861
- win_1 = cf_vals[max(0, i - half_win_1):i + half_win_1 + 1]
1862
- win_2 = cf_vals[max(0, i - half_win_2):i + half_win_2 + 1]
1863
- count_1 = np.count_nonzero(~np.isnan(win_1))
1864
- count_2 = np.count_nonzero(~np.isnan(win_2))
1865
- # method 1, median of +/- 7 days
1866
- if count_1 >= min_count_1:
1867
- cf_corr[i] = np.nanmedian(win_1)
1868
- cf_method[i] = 1
1869
- # method 2, mean of +/- 5 days
1870
- elif count_2 > 0:
1871
- cf_corr[i] = np.nanmean(win_2)
1872
- cf_method[i] = 2
1873
-
1874
- return (
1875
- pd.Series(cf_corr, index=cf.index),
1876
- pd.Series(cf_method, index=cf.index)
1877
- )
1878
-
1879
- @staticmethod
1880
- def _ebc_cf_adjacent_years(cf, half_win=5):
1881
- """
1882
- Energy balance closure correction factors from the previous and next
1883
- years, method 3 of the FLUXNET2015 daily correction.
1884
-
1885
- For each day, the mean of the correction factors within +/- 5 days
1886
- of the same day in the previous and next years. Assumes a
1887
- continuous daily time series.
1888
-
1889
- Arguments:
1890
- cf (:obj:`pandas.Series`): daily correction factors with
1891
- outliers removed (null).
1892
-
1893
- Keyword Arguments:
1894
- half_win (int): default 5. Half width of the window [days].
1895
-
1896
- Returns:
1897
- cf_adjacent (:obj:`pandas.Series`): correction factors, null
1898
- where neither year has values.
1899
- """
1900
- # sum and count of values in a +/- 5 day window around each day
1901
- window = cf.rolling(2 * half_win + 1, center=True, min_periods=1)
1902
- win_sum = window.sum()
1903
- win_count = window.count()
1904
-
1905
- # add windows from the same days one year before and after
1906
- total = np.zeros(len(cf))
1907
- count = np.zeros(len(cf))
1908
- for years in (-1, 1):
1909
- dates = cf.index + pd.DateOffset(years=years)
1910
- total += win_sum.reindex(dates).fillna(0).values
1911
- count += win_count.reindex(dates).fillna(0).values
1912
-
1913
- cf_adjacent = np.full(len(cf), np.nan)
1914
- has_values = count > 0
1915
- cf_adjacent[has_values] = total[has_values] / count[has_values]
1916
-
1917
- return pd.Series(cf_adjacent, index=cf.index)
1918
-
1919
- @staticmethod
1920
- def _ebc_cf_climatology(cf_corr, half_win=5):
1921
- """
1922
- Day of year climatology of energy balance closure correction
1923
- factors, used to fill gaps that remain after FLUXNET method 3.
1924
-
1925
- The mean correction factor for each day of year over all years is
1926
- smoothed with a +/- 5 day moving mean, windows near the start and
1927
- end of the year wrap around to the other end.
1928
-
1929
- Arguments:
1930
- cf_corr (:obj:`pandas.Series`): daily correction factors from
1931
- methods 1 and 2.
1932
-
1933
- Keyword Arguments:
1934
- half_win (int): default 5. Half width of the window [days].
1935
-
1936
- Returns:
1937
- cf_clim (:obj:`pandas.Series`): correction factor for each day of
1938
- year, index is day of year from 1 to 366.
1939
- """
1940
- doy_mean = cf_corr.groupby(cf_corr.index.dayofyear).mean()
1941
- # include all days of year so windows line up with day of year
1942
- doy_mean = doy_mean.reindex(np.arange(1, 367))
1943
-
1944
- # wrap around the end of the year before the moving mean
1945
- wrapped = pd.concat([
1946
- doy_mean.iloc[-half_win:], doy_mean, doy_mean.iloc[:half_win]
1947
- ])
1948
- cf_clim = wrapped.rolling(
1949
- 2 * half_win + 1, center=True, min_periods=1
1950
- ).mean()
1951
- cf_clim = cf_clim.iloc[half_win:-half_win]
1952
- cf_clim.index = np.arange(1, 367)
1953
-
1954
- return cf_clim
1955
-
1867
+
1956
1868
  def _bowen_ratio_correction(self):
1957
1869
  """
1958
1870
  Create corrected/adjusted latent energy and sensible heat flux to
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxdataqaqc
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Tools for QA/QC of eddy covariance station data
5
5
  Home-page: https://github.com/Open-ET/flux-data-qaqc
6
6
  Author: John Volk
@@ -253,18 +253,6 @@ class TestData(object):
253
253
  with pytest.raises(ValueError, match='latent_heat_flux_units'):
254
254
  Data(config)
255
255
 
256
- def test_soil_heat_flux_column_named_G(self, data, tmp_path):
257
- # g_1 uses the same column as G, its units are in the config but
258
- # not stored in Data.units, this was rejected in version 0.3.1
259
- config = data['package_root_dir']/'examples'\
260
- /'Config_options'/'config_for_QC_flag_filtering.ini'
261
- config = _tmp_config(
262
- config, tmp_path,
263
- set_opts=[('DATA', 'g_1', 'G'), ('DATA', 'g_1_units', 'w/m2')]
264
- )
265
- q = QaQc(Data(config))
266
- assert 'G' in q.df.rename(columns=q.inv_map).columns
267
-
268
256
  def test_missing_units_for_plot_only_variable(self, data, tmp_path):
269
257
  # potential shortwave units are only used for plot labels
270
258
  config = data['package_root_dir']/'examples'\
@@ -621,87 +609,6 @@ class TestQaQc(object):
621
609
  assert sw_pot.loc['2020-06-21 12:00'] > 0
622
610
  assert sw_pot.max() > 0
623
611
 
624
- def _daily_cf(self, values, start='2010-03-01'):
625
- """Daily correction factor series for energy balance tests"""
626
- index = pd.date_range(start, periods=len(values), freq='D')
627
- return pd.Series(values, index=index, dtype=float)
628
-
629
- def test_ebc_cf_method_1_at_record_start(self):
630
- # first days used to get no moving window value
631
- cf = self._daily_cf([1.2] * 30)
632
- cf.iloc[0] = 1.0
633
- cf_corr, method = QaQc._ebc_cf_moving_windows(cf)
634
- assert (method == 1).all()
635
- # day 0 window is days 0-7, median of 1.0 and seven 1.2 values
636
- assert np.isclose(cf_corr.iloc[0], 1.2)
637
-
638
- def test_ebc_cf_method_2_is_mean(self):
639
- # only 3 values within +/- 7 days, method 2 uses the mean
640
- cf = self._daily_cf([np.nan] * 30)
641
- cf.iloc[[10, 12, 13]] = [1.0, 1.1, 1.6]
642
- cf_corr, method = QaQc._ebc_cf_moving_windows(cf)
643
- assert method.iloc[11] == 2
644
- assert np.isclose(cf_corr.iloc[11], np.mean([1.0, 1.1, 1.6]))
645
- # no values within +/- 5 days
646
- assert np.isnan(cf_corr.iloc[25]) and np.isnan(method.iloc[25])
647
-
648
- def test_ebc_cf_method_3_previous_and_next_years(self):
649
- cf = self._daily_cf([1.0] * 365 + [1.5] * 365 + [1.4] * 365,
650
- start='2010-01-01')
651
- adjacent = QaQc._ebc_cf_adjacent_years(cf)
652
- # middle year, mean of previous (1.0) and next (1.4) years
653
- assert np.isclose(adjacent.loc['2011-06-15'], 1.2)
654
- # first year, only the next year exists
655
- assert np.isclose(adjacent.loc['2010-06-15'], 1.5)
656
- # values in a gap of the middle year come from the other years
657
- cf.loc['2011-06-01':'2011-07-31'] = np.nan
658
- adjacent = QaQc._ebc_cf_adjacent_years(cf)
659
- assert np.isclose(adjacent.loc['2011-06-15'], 1.2)
660
-
661
- def test_ebc_cf_climatology_lines_up_with_day_of_year(self):
662
- # 200 days from March 1, climatology used to shift by record start
663
- cf = self._daily_cf(np.linspace(0.8, 1.4, 200))
664
- clim = QaQc._ebc_cf_climatology(cf)
665
- doy = cf.index.dayofyear
666
- day = cf.index[100]
667
- expected = cf[(doy >= day.dayofyear - 5) & (doy <= day.dayofyear + 5)]
668
- assert np.isclose(clim.loc[day.dayofyear], expected.mean())
669
- # no data in January
670
- assert np.isnan(clim.loc[15])
671
-
672
- def test_ebc_cf_out_of_limits_falls_through(self, data, monkeypatch):
673
- # method 3 values outside of 0.5-2 are not used, method 4 fills
674
- config = data['package_root_dir']\
675
- /'examples'/'Basic_usage'/'US-Tw3_config.ini'
676
- q = QaQc(Data(config))
677
- q.correct_data(et_gap_fill=False)
678
- method_3_days = q.df.ebc_cf_method == 3
679
- assert method_3_days.any()
680
- monkeypatch.setattr(
681
- QaQc, '_ebc_cf_adjacent_years',
682
- staticmethod(lambda cf, half_win=5: cf * 0 + 2.5)
683
- )
684
- q = QaQc(Data(config))
685
- q.correct_data(et_gap_fill=False)
686
- methods = q.df.ebc_cf_method
687
- assert not (methods == 3).any()
688
- assert (methods[method_3_days] == 4).all()
689
-
690
- def test_ebr_correction_outputs(self, data):
691
- config = data['package_root_dir']\
692
- /'examples'/'Basic_usage'/'US-Tw3_config.ini'
693
- q = QaQc(Data(config))
694
- q.correct_data(et_gap_fill=False)
695
- df = q.df.rename(columns=q.inv_map)
696
- methods = df.ebc_cf_method.dropna()
697
- assert set(methods.unique()).issubset({1, 2, 3, 4})
698
- cf = df.ebc_cf.dropna()
699
- assert ((cf > 0.5) & (cf < 2)).all()
700
- assert np.allclose(df.LE_corr, df.LE * df.ebc_cf, equal_nan=True)
701
- assert np.allclose(df.ebr_corr, 1 / df.ebc_cf, equal_nan=True)
702
- # method flag is daily only
703
- assert 'ebc_cf_method' not in q.monthly_df.columns
704
-
705
612
  def _qc_example_config(self, data, tmp_path, **kwargs):
706
613
  """Daily US-AR1 example copied to a temp dir (gridMET writes)"""
707
614
  config = (
File without changes
File without changes
File without changes
File without changes
File without changes