From b606af246857ea354431f0a9914efb8311c75807 Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Tue, 25 Jan 2022 13:00:38 -0700 Subject: [PATCH 1/9] modify_fsurdat: Add dom_cft as alternative user-choice to dom_nat_pft --- .../ctsm/modify_fsurdat/fsurdat_modifier.py | 10 +++ python/ctsm/modify_fsurdat/modify_fsurdat.py | 73 ++++++++++++++++--- tools/modify_fsurdat/modify_template.cfg | 3 + 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/python/ctsm/modify_fsurdat/fsurdat_modifier.py b/python/ctsm/modify_fsurdat/fsurdat_modifier.py index 76b2374a05..39fc9f12a5 100644 --- a/python/ctsm/modify_fsurdat/fsurdat_modifier.py +++ b/python/ctsm/modify_fsurdat/fsurdat_modifier.py @@ -70,6 +70,10 @@ def fsurdat_modifier(cfg_path): item='dom_nat_pft', file_path=cfg_path, allowed_values=range(15), # integers from 0 to 14 convert_to_type=int, can_be_unset=True) + dom_cft = get_config_value(config=config, section=section, + item='dom_cft', file_path=cfg_path, + allowed_values=range(15, 79), # integers from 15 to 78 + convert_to_type=int, can_be_unset=True) lai = get_config_value(config=config, section=section, item='lai', file_path=cfg_path, is_list=True, @@ -132,6 +136,12 @@ def fsurdat_modifier(cfg_path): if zero_nonveg: modify_fsurdat.zero_nonveg() + # The set_dom_cft call follows zero_nonveg because it modifies PCT_NATVEG + # and PCT_CROP in the user-defined rectangle + if dom_cft is not None and dom_nat_pft is None: + modify_fsurdat.set_dom_cft(dom_cft=dom_cft, + lai=lai, sai=sai, + hgt_top=hgt_top, hgt_bot=hgt_bot) # ---------------------------------------------- # Output the now modified CTSM surface data file # ---------------------------------------------- diff --git a/python/ctsm/modify_fsurdat/modify_fsurdat.py b/python/ctsm/modify_fsurdat/modify_fsurdat.py index bf1a5e8c9b..3f9e319950 100644 --- a/python/ctsm/modify_fsurdat/modify_fsurdat.py +++ b/python/ctsm/modify_fsurdat/modify_fsurdat.py @@ -25,7 +25,7 @@ def __init__(self, my_data, lon_1, lon_2, lat_1, lat_2, landmask_file): self.file = my_data - self.not_rectangle = self._get_not_rectangle( + self.rectangle = self._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=self.file.LONGXY, latixy=self.file.LATIXY) @@ -34,8 +34,9 @@ def __init__(self, my_data, lon_1, lon_2, lat_1, lat_2, landmask_file): # overwrite self.not_rectangle with data from # user-specified .nc file in the .cfg file self._landmask_file = xr.open_dataset(landmask_file) - rectangle = self._landmask_file.landmask - self.not_rectangle = np.logical_not(rectangle) + self.rectangle = self._landmask_file.landmask + + self.not_rectangle = np.logical_not(self.rectangle) @classmethod @@ -47,7 +48,7 @@ def init_from_file(cls, fsurdat_in, lon_1, lon_2, lat_1, lat_2, landmask_file): @staticmethod - def _get_not_rectangle(lon_1, lon_2, lat_1, lat_2, longxy, latixy): + def _get_rectangle(lon_1, lon_2, lat_1, lat_2, longxy, latixy): """ Description ----------- @@ -86,7 +87,7 @@ def _get_not_rectangle(lon_1, lon_2, lat_1, lat_2, longxy, latixy): rectangle = np.logical_and(union_1, union_2) not_rectangle = np.logical_not(rectangle) - return not_rectangle + return rectangle def write_output(self, fsurdat_in, fsurdat_out): @@ -165,26 +166,76 @@ def set_dom_nat_pft(self, dom_nat_pft, lai, sai, hgt_top, hgt_bot): 'MONTHLY_HEIGHT_BOT': hgt_bot} for var, val in vars_4d.items(): if val is not None: - self.set_lai_sai_hgts(dom_nat_pft=dom_nat_pft, + self.set_lai_sai_hgts(dom_plant=dom_nat_pft, + var=var, val=val) + + + def set_dom_cft(self, dom_cft, lai, sai, hgt_top, hgt_bot): + """ + Description + ----------- + In rectangle selected by user (or default -90 to 90 and 0 to 360), + replace fsurdat file's PCT_CFT with: + - 100 for dom_cft selected by user + - 0 for all other PFTs/CFTs + If user has specified lai, sai, hgt_top, hgt_bot, replace these with + values selected by the user for dom_cft + + Arguments + --------- + dom_cft: + (int) User's entry of CFT to be set to 100% everywhere + lai: + (float) User's entry of MONTHLY_LAI for their dom_cft + sai: + (float) User's entry of MONTHLY_SAI for their dom_cft + hgt_top: + (float) User's entry of MONTHLY_HEIGHT_TOP for their dom_cft + hgt_bot: + (float) User's entry of MONTHLY_HEIGHT_BOT for their dom_cft + """ + + # Add PCT_NATVEG to PCT_CROP in the rectangle; remove from PCT_NATVEG + self.file['PCT_CROP'] = \ + self.file['PCT_CROP'] + \ + self.file['PCT_NATVEG'].where(self.rectangle, other=0) + self.setvar_lev0('PCT_NATVEG', 0) + + for cft in self.file.cft: + cft_local = cft - (max(self.file.natpft) + 1) + # initialize 3D variable; set outside the loop below + self.setvar_lev1('PCT_CFT', val=0, lev1_dim=cft_local) + + # set 3D variable + self.setvar_lev1('PCT_CFT', val=100, lev1_dim=dom_cft-(max(self.file.natpft)+1)) + + # dictionary of 4d variables to loop over + vars_4d = {'MONTHLY_LAI': lai, + 'MONTHLY_SAI': sai, + 'MONTHLY_HEIGHT_TOP': hgt_top, + 'MONTHLY_HEIGHT_BOT': hgt_bot} + for var, val in vars_4d.items(): + if val is not None: + self.set_lai_sai_hgts(dom_plant=dom_cft, var=var, val=val) - def set_lai_sai_hgts(self, dom_nat_pft, var, val): + def set_lai_sai_hgts(self, dom_plant, var, val): """ Description ----------- If user has specified lai, sai, hgt_top, hgt_bot, replace these with - values selected by the user for dom_nat_pft. Else do nothing. + values selected by the user for dom_plant. Else do nothing. """ - if dom_nat_pft == 0: # bare soil: var must equal 0 + if dom_plant == 0: # bare soil: var must equal 0 val = [0] * 12 if len(val) != 12: errmsg = 'Error: Variable should have exactly 12 ' \ 'entries in the configure file: ' + var abort(errmsg) for mon in self.file.time - 1: # loop over 12 months - # set 4D variable to value for dom_nat_pft - self.setvar_lev2(var, val[int(mon)], lev1_dim=dom_nat_pft, + # set 4D variable to value for dom_plant + self.setvar_lev2(var, val[int(mon)], lev1_dim=dom_plant, lev2_dim=mon) diff --git a/tools/modify_fsurdat/modify_template.cfg b/tools/modify_fsurdat/modify_template.cfg index 56e8221635..e5efa41ced 100644 --- a/tools/modify_fsurdat/modify_template.cfg +++ b/tools/modify_fsurdat/modify_template.cfg @@ -60,6 +60,9 @@ landmask_file = UNSET # If idealized = True and dom_nat_pft = UNSET, the latter defaults to 0 # (bare soil). Valid values 0 to 14 (int). dom_nat_pft = UNSET +# Crop (CFT) to be set to 100% according to user-defined mask. +# If dom_nat_pft >= 0, dom_cft defaults to UNSET. Valid values 15 to 78 (int). +dom_cft = UNSET # LAI, SAI, HEIGHT_TOP, and HEIGHT_BOT values by month for dom_nat_pft # If dom_nat_pft = 0, the next four default to 0 (space-delimited list From 00c8ba9d360aa1a257e2f8b1ad0cd10f0cfe1885 Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Tue, 25 Jan 2022 18:58:36 -0700 Subject: [PATCH 2/9] Revisions in response to Erik's code review - Removed a bunch of "magic" numbers - Consolidated two options (dom_nat_pft and dom_cft) into one (dom_plant) - Not in Erik's review, but I added some new logger.info lines to indicate the progress of a run when using --verbose --- .../ctsm/modify_fsurdat/fsurdat_modifier.py | 48 ++++---- python/ctsm/modify_fsurdat/modify_fsurdat.py | 110 ++++++------------ tools/modify_fsurdat/fsurdat_modifier | 1 + tools/modify_fsurdat/modify_template.cfg | 15 +-- 4 files changed, 69 insertions(+), 105 deletions(-) diff --git a/python/ctsm/modify_fsurdat/fsurdat_modifier.py b/python/ctsm/modify_fsurdat/fsurdat_modifier.py index 39fc9f12a5..46365999fc 100644 --- a/python/ctsm/modify_fsurdat/fsurdat_modifier.py +++ b/python/ctsm/modify_fsurdat/fsurdat_modifier.py @@ -65,14 +65,15 @@ def fsurdat_modifier(cfg_path): landmask_file = get_config_value(config=config, section=section, item='landmask_file', file_path=cfg_path, can_be_unset=True) + # Create ModifyFsurdat object + modify_fsurdat = ModifyFsurdat.init_from_file(fsurdat_in, + lnd_lon_1, lnd_lon_2, lnd_lat_1, lnd_lat_2, landmask_file) + # not required: user may set these in the .cfg file - dom_nat_pft = get_config_value(config=config, section=section, - item='dom_nat_pft', file_path=cfg_path, - allowed_values=range(15), # integers from 0 to 14 - convert_to_type=int, can_be_unset=True) - dom_cft = get_config_value(config=config, section=section, - item='dom_cft', file_path=cfg_path, - allowed_values=range(15, 79), # integers from 15 to 78 + max_pft = int(max(modify_fsurdat.file.lsmpft)) + dom_plant = get_config_value(config=config, section=section, + item='dom_plant', file_path=cfg_path, + allowed_values=range(max_pft + 1), # integers from 0 to max_pft convert_to_type=int, can_be_unset=True) lai = get_config_value(config=config, section=section, item='lai', @@ -88,9 +89,10 @@ def fsurdat_modifier(cfg_path): item='hgt_bot', file_path=cfg_path, is_list=True, convert_to_type=float, can_be_unset=True) + max_soil_color = int(modify_fsurdat.file.mxsoil_color) soil_color = get_config_value(config=config, section=section, item='soil_color', file_path=cfg_path, - allowed_values=range(1, 21), # integers from 1 to 20 + allowed_values=range(1, max_soil_color + 1), # 1 to max_soil_color convert_to_type=int, can_be_unset=True) std_elev = get_config_value(config=config, section=section, @@ -100,10 +102,6 @@ def fsurdat_modifier(cfg_path): item='max_sat_area', file_path=cfg_path, convert_to_type=float, can_be_unset=True) - # Create ModifyFsurdat object - modify_fsurdat = ModifyFsurdat.init_from_file(fsurdat_in, - lnd_lon_1, lnd_lon_2, lnd_lat_1, lnd_lat_2, landmask_file) - # ------------------------------ # modify surface data properties # ------------------------------ @@ -116,32 +114,34 @@ def fsurdat_modifier(cfg_path): if idealized: modify_fsurdat.set_idealized() # set 2D variables # set 3D and 4D variables pertaining to natural vegetation - modify_fsurdat.set_dom_nat_pft(dom_nat_pft=0, lai=[], sai=[], - hgt_top=[], hgt_bot=[]) - - if dom_nat_pft is not None: # overwrite "idealized" value - modify_fsurdat.set_dom_nat_pft(dom_nat_pft=dom_nat_pft, - lai=lai, sai=sai, - hgt_top=hgt_top, hgt_bot=hgt_bot) + modify_fsurdat.set_dom_plant(dom_plant=0, lai=[], sai=[], + hgt_top=[], hgt_bot=[]) + logger.info('idealized complete') if max_sat_area is not None: # overwrite "idealized" value modify_fsurdat.setvar_lev0('FMAX', max_sat_area) + logger.info('max_sat_area complete') if std_elev is not None: # overwrite "idealized" value modify_fsurdat.setvar_lev0('STD_ELEV', std_elev) + logger.info('std_elev complete') if soil_color is not None: # overwrite "idealized" value modify_fsurdat.setvar_lev0('SOIL_COLOR', soil_color) + logger.info('soil_color complete') if zero_nonveg: modify_fsurdat.zero_nonveg() + logger.info('zero_nonveg complete') - # The set_dom_cft call follows zero_nonveg because it modifies PCT_NATVEG + # The set_dom_plant call follows zero_nonveg because it modifies PCT_NATVEG # and PCT_CROP in the user-defined rectangle - if dom_cft is not None and dom_nat_pft is None: - modify_fsurdat.set_dom_cft(dom_cft=dom_cft, - lai=lai, sai=sai, - hgt_top=hgt_top, hgt_bot=hgt_bot) + if dom_plant is not None: + modify_fsurdat.set_dom_plant(dom_plant=dom_plant, + lai=lai, sai=sai, + hgt_top=hgt_top, hgt_bot=hgt_bot) + logger.info('dom_plant complete') + # ---------------------------------------------- # Output the now modified CTSM surface data file # ---------------------------------------------- diff --git a/python/ctsm/modify_fsurdat/modify_fsurdat.py b/python/ctsm/modify_fsurdat/modify_fsurdat.py index 3f9e319950..4197fb5aaf 100644 --- a/python/ctsm/modify_fsurdat/modify_fsurdat.py +++ b/python/ctsm/modify_fsurdat/modify_fsurdat.py @@ -42,7 +42,7 @@ def __init__(self, my_data, lon_1, lon_2, lat_1, lat_2, landmask_file): @classmethod def init_from_file(cls, fsurdat_in, lon_1, lon_2, lat_1, lat_2, landmask_file): """Initialize a ModifyFsurdat object from file fsurdat_in""" - logger.info( 'Opening fsurdat_in file to be modified: %s', fsurdat_in) + logger.info('Opening fsurdat_in file to be modified: %s', fsurdat_in) my_file = xr.open_dataset(fsurdat_in) return cls(my_file, lon_1, lon_2, lat_1, lat_2, landmask_file) @@ -128,86 +128,52 @@ def write_output(self, fsurdat_in, fsurdat_out): self.file.close() - def set_dom_nat_pft(self, dom_nat_pft, lai, sai, hgt_top, hgt_bot): + def set_dom_plant(self, dom_plant, lai, sai, hgt_top, hgt_bot): """ Description ----------- In rectangle selected by user (or default -90 to 90 and 0 to 360), - replace fsurdat file's PCT_NAT_PFT with: - - 100 for dom_nat_pft selected by user - - 0 for all other non-crop PFTs - If user has specified lai, sai, hgt_top, hgt_bot, replace these with - values selected by the user for dom_nat_pft - - Arguments - --------- - dom_nat_pft: - (int) User's entry of PFT to be set to 100% everywhere - lai: - (float) User's entry of MONTHLY_LAI for their dom_nat_pft - sai: - (float) User's entry of MONTHLY_SAI for their dom_nat_pft - hgt_top: - (float) User's entry of MONTHLY_HEIGHT_TOP for their dom_nat_pft - hgt_bot: - (float) User's entry of MONTHLY_HEIGHT_BOT for their dom_nat_pft - """ - - for pft in self.file.natpft: - # initialize 3D variable; set outside the loop below - self.setvar_lev1('PCT_NAT_PFT', val=0, lev1_dim=pft) - # set 3D variable value for dom_nat_pft - self.setvar_lev1('PCT_NAT_PFT', val=100, lev1_dim=dom_nat_pft) - - # dictionary of 4d variables to loop over - vars_4d = {'MONTHLY_LAI': lai, - 'MONTHLY_SAI': sai, - 'MONTHLY_HEIGHT_TOP': hgt_top, - 'MONTHLY_HEIGHT_BOT': hgt_bot} - for var, val in vars_4d.items(): - if val is not None: - self.set_lai_sai_hgts(dom_plant=dom_nat_pft, - var=var, val=val) - - - def set_dom_cft(self, dom_cft, lai, sai, hgt_top, hgt_bot): - """ - Description - ----------- - In rectangle selected by user (or default -90 to 90 and 0 to 360), - replace fsurdat file's PCT_CFT with: - - 100 for dom_cft selected by user + replace fsurdat file's PCT_NAT_PFT or PCT_CFT with: + - 100 for dom_plant selected by user - 0 for all other PFTs/CFTs If user has specified lai, sai, hgt_top, hgt_bot, replace these with - values selected by the user for dom_cft + values selected by the user for dom_plant Arguments --------- - dom_cft: - (int) User's entry of CFT to be set to 100% everywhere + dom_plant: + (int) User's entry of PFT/CFT to be set to 100% everywhere lai: - (float) User's entry of MONTHLY_LAI for their dom_cft + (float) User's entry of MONTHLY_LAI for their dom_plant sai: - (float) User's entry of MONTHLY_SAI for their dom_cft + (float) User's entry of MONTHLY_SAI for their dom_plant hgt_top: - (float) User's entry of MONTHLY_HEIGHT_TOP for their dom_cft + (float) User's entry of MONTHLY_HEIGHT_TOP for their dom_plant hgt_bot: - (float) User's entry of MONTHLY_HEIGHT_BOT for their dom_cft + (float) User's entry of MONTHLY_HEIGHT_BOT for their dom_plant """ - # Add PCT_NATVEG to PCT_CROP in the rectangle; remove from PCT_NATVEG - self.file['PCT_CROP'] = \ - self.file['PCT_CROP'] + \ - self.file['PCT_NATVEG'].where(self.rectangle, other=0) - self.setvar_lev0('PCT_NATVEG', 0) - - for cft in self.file.cft: - cft_local = cft - (max(self.file.natpft) + 1) - # initialize 3D variable; set outside the loop below - self.setvar_lev1('PCT_CFT', val=0, lev1_dim=cft_local) - - # set 3D variable - self.setvar_lev1('PCT_CFT', val=100, lev1_dim=dom_cft-(max(self.file.natpft)+1)) + # If dom_plant is a cft, add PCT_NATVEG to PCT_CROP in the rectangle + # and remove same from PCT_NATVEG, i.e. set PCT_NATVEG = 0. + if dom_plant > max(self.file.natpft): # dom_plant is a cft (crop) + self.file['PCT_CROP'] = \ + self.file['PCT_CROP'] + \ + self.file['PCT_NATVEG'].where(self.rectangle, other=0) + self.setvar_lev0('PCT_NATVEG', 0) + + for cft in self.file.cft: + cft_local = cft - (max(self.file.natpft) + 1) + # initialize 3D variable; set outside the loop below + self.setvar_lev1('PCT_CFT', val=0, lev1_dim=cft_local) + + # set 3D variable + self.setvar_lev1('PCT_CFT', val=100, lev1_dim=dom_plant-(max(self.file.natpft)+1)) + else: # dom_plant is a pft (not a crop) + for pft in self.file.natpft: + # initialize 3D variable; set outside the loop below + self.setvar_lev1('PCT_NAT_PFT', val=0, lev1_dim=pft) + # set 3D variable value for dom_plant + self.setvar_lev1('PCT_NAT_PFT', val=100, lev1_dim=dom_plant) # dictionary of 4d variables to loop over vars_4d = {'MONTHLY_LAI': lai, @@ -216,8 +182,7 @@ def set_dom_cft(self, dom_cft, lai, sai, hgt_top, hgt_bot): 'MONTHLY_HEIGHT_BOT': hgt_bot} for var, val in vars_4d.items(): if val is not None: - self.set_lai_sai_hgts(dom_plant=dom_cft, - var=var, val=val) + self.set_lai_sai_hgts(dom_plant=dom_plant, var=var, val=val) def set_lai_sai_hgts(self, dom_plant, var, val): @@ -227,11 +192,12 @@ def set_lai_sai_hgts(self, dom_plant, var, val): If user has specified lai, sai, hgt_top, hgt_bot, replace these with values selected by the user for dom_plant. Else do nothing. """ + months = int(max(self.file.time)) # 12 months if dom_plant == 0: # bare soil: var must equal 0 - val = [0] * 12 - if len(val) != 12: - errmsg = 'Error: Variable should have exactly 12 ' \ - 'entries in the configure file: ' + var + val = [0] * months + if len(val) != months: + errmsg = 'Error: Variable should have exactly ' + months + \ + ' entries in the configure file: ' + var abort(errmsg) for mon in self.file.time - 1: # loop over 12 months # set 4D variable to value for dom_plant diff --git a/tools/modify_fsurdat/fsurdat_modifier b/tools/modify_fsurdat/fsurdat_modifier index fdf3d48756..8c2031b548 100755 --- a/tools/modify_fsurdat/fsurdat_modifier +++ b/tools/modify_fsurdat/fsurdat_modifier @@ -37,6 +37,7 @@ ncar_pylib contains all the arguments needed by the script. 3) Run the script ./fsurdat_modifier pointing to the copied/modified .cfg file, e.g. modify_users_copy.cfg +4) Use the --verbose option to see progress output on your screen Example ------- diff --git a/tools/modify_fsurdat/modify_template.cfg b/tools/modify_fsurdat/modify_template.cfg index e5efa41ced..6b18cedc36 100644 --- a/tools/modify_fsurdat/modify_template.cfg +++ b/tools/modify_fsurdat/modify_template.cfg @@ -56,16 +56,13 @@ lnd_lon_2 = 360 # user-defined mask in a file, as alternative to setting lat/lon values landmask_file = UNSET -# Non-crop PFT to be set to 100% according to user-defined mask. -# If idealized = True and dom_nat_pft = UNSET, the latter defaults to 0 -# (bare soil). Valid values 0 to 14 (int). -dom_nat_pft = UNSET -# Crop (CFT) to be set to 100% according to user-defined mask. -# If dom_nat_pft >= 0, dom_cft defaults to UNSET. Valid values 15 to 78 (int). -dom_cft = UNSET +# PFT/CFT to be set to 100% according to user-defined mask. +# If idealized = True and dom_plant = UNSET, the latter defaults to 0 +# (bare soil). Valid values 0 to 78 (int). +dom_plant = UNSET -# LAI, SAI, HEIGHT_TOP, and HEIGHT_BOT values by month for dom_nat_pft -# If dom_nat_pft = 0, the next four default to 0 (space-delimited list +# LAI, SAI, HEIGHT_TOP, and HEIGHT_BOT values by month for dom_plant +# If dom_plant = 0, the next four default to 0 (space-delimited list # of floats without brackets). lai = UNSET sai = UNSET From 37ff41934f1a42625f11923e01b8c2aeaa5ab113 Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Wed, 26 Jan 2022 15:38:47 -0700 Subject: [PATCH 3/9] Updates for unit/sys tests to pass --- python/ctsm/test/test_sys_fsurdat_modifier.py | 4 ++-- python/ctsm/test/test_unit_modify_fsurdat.py | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/python/ctsm/test/test_sys_fsurdat_modifier.py b/python/ctsm/test/test_sys_fsurdat_modifier.py index 7d2819261c..4f51ee68b5 100755 --- a/python/ctsm/test/test_sys_fsurdat_modifier.py +++ b/python/ctsm/test/test_sys_fsurdat_modifier.py @@ -116,8 +116,8 @@ def _create_config_file_complete(self): line = 'lnd_lon_1 = 295\n' elif re.match(r' *lnd_lon_2 *=', line): line = 'lnd_lon_2 = 300\n' - elif re.match(r' *dom_nat_pft *=', line): - line = 'dom_nat_pft = 1' + elif re.match(r' *dom_plant *=', line): + line = 'dom_plant = 1' elif re.match(r' *lai *=', line): line = 'lai = 0 1 2 3 4 5 5 4 3 2 1 0\n' elif re.match(r' *sai *=', line): diff --git a/python/ctsm/test/test_unit_modify_fsurdat.py b/python/ctsm/test/test_unit_modify_fsurdat.py index 19c53dac6a..33fe459fe6 100755 --- a/python/ctsm/test/test_unit_modify_fsurdat.py +++ b/python/ctsm/test/test_unit_modify_fsurdat.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Unit tests for _get_not_rectangle +Unit tests for _get_rectangle """ import unittest @@ -21,7 +21,7 @@ class TestModifyFsurdat(unittest.TestCase): """Tests the setvar_lev functions and the - _get_not_rectangle function + _get_rectangle function """ def test_setvarLev(self): @@ -103,9 +103,10 @@ def test_getNotRectangle_lon1leLon2Lat1leLat2(self): lon_2 = 5 # lon_1 < lon_2 lat_1 = 6 lat_2 = 8 # lat_1 < lat_2 - not_rectangle = ModifyFsurdat._get_not_rectangle( + rectangle = ModifyFsurdat._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=longxy, latixy=latixy) + not_rectangle = np.logical_not(rectangle) compare = np.ones((rows,cols)) # assert this to confirm intuitive understanding of these matrices self.assertEqual(np.size(not_rectangle), np.size(compare)) @@ -140,9 +141,10 @@ def test_getNotRectangle_lon1leLon2Lat1gtLat2(self): lon_2 = 4 # lon_1 < lon_2 lat_1 = 4 lat_2 = 0 # lat_1 > lat_2 - not_rectangle = ModifyFsurdat._get_not_rectangle( + rectangle = ModifyFsurdat._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=longxy, latixy=latixy) + not_rectangle = np.logical_not(rectangle) compare = np.ones((rows,cols)) # assert this to confirm intuitive understanding of these matrices self.assertEqual(np.size(not_rectangle), np.size(compare)) @@ -178,9 +180,10 @@ def test_getNotRectangle_lon1gtLon2Lat1leLat2(self): lon_2 = 2 # lon_1 > lon_2 lat_1 = 2 lat_2 = 3 # lat_1 < lat_2 - not_rectangle = ModifyFsurdat._get_not_rectangle( + rectangle = ModifyFsurdat._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=longxy, latixy=latixy) + not_rectangle = np.logical_not(rectangle) compare = np.ones((rows,cols)) # assert this to confirm intuitive understanding of these matrices self.assertEqual(np.size(not_rectangle), np.size(compare)) @@ -216,9 +219,10 @@ def test_getNotRectangle_lon1gtLon2Lat1gtLat2(self): lon_2 = -6 # lon_1 > lon_2 lat_1 = 0 lat_2 = -3 # lat_1 > lat_2 - not_rectangle = ModifyFsurdat._get_not_rectangle( + rectangle = ModifyFsurdat._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=longxy, latixy=latixy) + not_rectangle = np.logical_not(rectangle) compare = np.ones((rows,cols)) # assert this to confirm intuitive understanding of these matrices self.assertEqual(np.size(not_rectangle), np.size(compare)) @@ -256,9 +260,10 @@ def test_getNotRectangle_lonsStraddle0deg(self): lon_2 = 5 # lon_1 > lon_2 lat_1 = -4 lat_2 = -6 # lat_1 > lat_2 - not_rectangle = ModifyFsurdat._get_not_rectangle( + rectangle = ModifyFsurdat._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=longxy, latixy=latixy) + not_rectangle = np.logical_not(rectangle) compare = np.ones((rows,cols)) # assert this to confirm intuitive understanding of these matrices self.assertEqual(np.size(not_rectangle), np.size(compare)) @@ -294,7 +299,7 @@ def test_getNotRectangle_latsOutOfBounds(self): lat_2 = 91 with self.assertRaisesRegex(SystemExit, "lat_1 and lat_2 need to be in the range -90 to 90"): - _ = ModifyFsurdat._get_not_rectangle( + _ = ModifyFsurdat._get_rectangle( lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, longxy=longxy, latixy=latixy) From 06106ee0a8415a060d1e40adadc35b516c4d5832 Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Wed, 26 Jan 2022 16:07:22 -0700 Subject: [PATCH 4/9] Updates based on pylint or black recommendations --- python/ctsm/modify_fsurdat/modify_fsurdat.py | 1 - python/ctsm/test/test_sys_fsurdat_modifier.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/python/ctsm/modify_fsurdat/modify_fsurdat.py b/python/ctsm/modify_fsurdat/modify_fsurdat.py index 4197fb5aaf..9c5d71af9e 100644 --- a/python/ctsm/modify_fsurdat/modify_fsurdat.py +++ b/python/ctsm/modify_fsurdat/modify_fsurdat.py @@ -85,7 +85,6 @@ def _get_rectangle(lon_1, lon_2, lat_1, lat_2, longxy, latixy): # union rectangles overlap rectangle = np.logical_and(union_1, union_2) - not_rectangle = np.logical_not(rectangle) return rectangle diff --git a/python/ctsm/test/test_sys_fsurdat_modifier.py b/python/ctsm/test/test_sys_fsurdat_modifier.py index 4f51ee68b5..c7a6f380f5 100755 --- a/python/ctsm/test/test_sys_fsurdat_modifier.py +++ b/python/ctsm/test/test_sys_fsurdat_modifier.py @@ -87,25 +87,25 @@ def test_allInfo(self): def _create_config_file_minimal(self): - with open (self._cfg_file_path,'w') as cfg_out: - with open (self._cfg_template_path,'r') as cfg_in: + with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: + with open (self._cfg_template_path, 'r', encoding='utf-8') as cfg_in: for line in cfg_in: if re.match(r' *fsurdat_in *=', line): - line = 'fsurdat_in = {}'.format(self._fsurdat_in) + line = f'fsurdat_in = {self._fsurdat_in}' elif re.match(r' *fsurdat_out *=', line): - line = 'fsurdat_out = {}'.format(self._fsurdat_out) + line = f'fsurdat_out = {self._fsurdat_out}' cfg_out.write(line) def _create_config_file_complete(self): - with open (self._cfg_file_path,'w') as cfg_out: - with open (self._cfg_template_path,'r') as cfg_in: + with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: + with open (self._cfg_template_path, 'r', encoding='utf-8') as cfg_in: for line in cfg_in: if re.match(r' *fsurdat_in *=', line): - line = 'fsurdat_in = {}'.format(self._fsurdat_in) + line = f'fsurdat_in = {self._fsurdat_in}' elif re.match(r' *fsurdat_out *=', line): - line = 'fsurdat_out = {}'.format(self._fsurdat_out) + line = f'fsurdat_out = {self._fsurdat_out}' elif re.match(r' *idealized *=', line): line = 'idealized = True' elif re.match(r' *lnd_lat_1 *=', line): From 91eaf58470dee4e1102d4c836761b1eb3c408375 Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Wed, 26 Jan 2022 16:48:59 -0700 Subject: [PATCH 5/9] New sys test for dom_plant set to a crop in the .cfg file --- python/ctsm/test/test_sys_fsurdat_modifier.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/python/ctsm/test/test_sys_fsurdat_modifier.py b/python/ctsm/test/test_sys_fsurdat_modifier.py index c7a6f380f5..8603769776 100755 --- a/python/ctsm/test/test_sys_fsurdat_modifier.py +++ b/python/ctsm/test/test_sys_fsurdat_modifier.py @@ -59,6 +59,32 @@ def test_minimalInfo(self): self.assertTrue(fsurdat_out_data.equals(fsurdat_in_data)) + def test_crop(self): + """ + This version replances the vegetation with a crop + """ + + self._create_config_file_crop() + + # run the fsurdat_modifier tool + fsurdat_modifier(self._cfg_file_path) + # the critical piece of this test is that the above command + # doesn't generate errors; however, we also do some assertions below + + # compare fsurdat_out to fsurdat_in + fsurdat_in_data = xr.open_dataset(self._fsurdat_in) + fsurdat_out_data = xr.open_dataset(self._fsurdat_out) + # assert that fsurdat_out does not equal fsurdat_in + self.assertFalse(fsurdat_out_data.equals(fsurdat_in_data)) + + # compare fsurdat_out to fsurdat_out_baseline + fsurdat_out_baseline = self._fsurdat_in[:-3] + '_modified_with_crop' + \ + self._fsurdat_in[-3:] + fsurdat_out_base_data = xr.open_dataset(fsurdat_out_baseline) + # assert that fsurdat_out equals fsurdat_out_baseline + self.assertTrue(fsurdat_out_data.equals(fsurdat_out_base_data)) + + def test_allInfo(self): """ This version specifies all possible information @@ -97,6 +123,36 @@ def _create_config_file_minimal(self): cfg_out.write(line) + def _create_config_file_crop(self): + + with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: + with open (self._cfg_template_path, 'r', encoding='utf-8') as cfg_in: + for line in cfg_in: + if re.match(r' *fsurdat_in *=', line): + line = f'fsurdat_in = {self._fsurdat_in}' + elif re.match(r' *fsurdat_out *=', line): + line = f'fsurdat_out = {self._fsurdat_out}' + elif re.match(r' *lnd_lat_1 *=', line): + line = 'lnd_lat_1 = -10\n' + elif re.match(r' *lnd_lat_2 *=', line): + line = 'lnd_lat_2 = -7\n' + elif re.match(r' *lnd_lon_1 *=', line): + line = 'lnd_lon_1 = 295\n' + elif re.match(r' *lnd_lon_2 *=', line): + line = 'lnd_lon_2 = 300\n' + elif re.match(r' *dom_plant *=', line): + line = 'dom_plant = 15' + elif re.match(r' *lai *=', line): + line = 'lai = 0 1 2 3 4 5 5 4 3 2 1 0\n' + elif re.match(r' *sai *=', line): + line = 'sai = 1 1 1 1 1 1 1 1 1 1 1 1\n' + elif re.match(r' *hgt_top *=', line): + line = 'hgt_top = 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5\n' + elif re.match(r' *hgt_bot *=', line): + line = 'hgt_bot = 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1\n' + cfg_out.write(line) + + def _create_config_file_complete(self): with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: From 4a66bf3eace94c931178778ffe5d5f7249be317b Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Wed, 26 Jan 2022 17:01:01 -0700 Subject: [PATCH 6/9] Committing fsurdat_out_baseline file used by new sys test --- ..._16pfts_Irrig_CMIP6_simyr2000_c171214_modified_with_crop.nc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 python/ctsm/test/testinputs/surfdata_5x5_amazon_16pfts_Irrig_CMIP6_simyr2000_c171214_modified_with_crop.nc diff --git a/python/ctsm/test/testinputs/surfdata_5x5_amazon_16pfts_Irrig_CMIP6_simyr2000_c171214_modified_with_crop.nc b/python/ctsm/test/testinputs/surfdata_5x5_amazon_16pfts_Irrig_CMIP6_simyr2000_c171214_modified_with_crop.nc new file mode 100644 index 0000000000..69f28b2239 --- /dev/null +++ b/python/ctsm/test/testinputs/surfdata_5x5_amazon_16pfts_Irrig_CMIP6_simyr2000_c171214_modified_with_crop.nc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0217926e5dea2f563a01ad7149be68cf6d0acb0a140715a5402fdf39a925b3e7 +size 247880 From 3ac329f92332fe7dfbd00818a4c715dc5c2f9f48 Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Thu, 27 Jan 2022 10:00:16 -0700 Subject: [PATCH 7/9] ChangeLog/ChangeSum drafts --- doc/ChangeLog | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ doc/ChangeSum | 1 + 2 files changed, 85 insertions(+) diff --git a/doc/ChangeLog b/doc/ChangeLog index dba2842cc1..901725d21c 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,4 +1,88 @@ =============================================================== +Tag name: ctsm5.1.dev073 +Originator(s): slevis (Samuel Levis,SLevis Consulting,303-665-1310) +Date: Thu Jan 27 09:52:00 MST 2022 +One-line Summary: Replace dom_nat_pft with dom_plant to enable crop in fsurdat_modifier tool + +Purpose and description of changes +---------------------------------- + + Allow user to replace vegetation in fsurdat files with any pft/cft using the + fsurdat_modifier tool option dom_plant. This option replaces now obsolete + option dom_nat_pft which handled pfts only and not cfts. + + +Significant changes to scientifically-supported configurations +-------------------------------------------------------------- + +Does this tag change answers significantly for any of the following physics configurations? +(Details of any changes will be given in the "Answer changes" section below.) + + [Put an [X] in the box for any configuration with significant answer changes.] + +[ ] clm5_1 + +[ ] clm5_0 + +[ ] ctsm5_0-nwp + +[ ] clm4_5 + + +Notes of particular relevance for users +--------------------------------------- +Changes to CTSM's user interface (e.g., new/renamed XML or namelist variables): + Instead of dom_nat_pft = UNSET, modify_template.cfg now includes the line + dom_plant = UNSET to allow users to set the pft/cft of their choice to replace + the existing vegetation. + +Changes to the datasets (e.g., parameter, surface or initial files): + New system test that checks the new code compares a generated file to a + baseline file. I added the baseline file to this PR: + .../python/ctsm/test/testinputs/surfdata_5x5_amazon_16pfts_Irrig_CMIP6_simyr2000_c171214_modified_with_crop.nc + + +Notes of particular relevance for developers: +--------------------------------------------- +Changes to tests or testing: + Added a system test to test_sys_fsurdat_modifier.py to run with the new option + dom_plant set to 15 (i.e. a crop). + + +Testing summary: +---------------- + + [PASS means all tests PASS; OK means tests PASS other than expected fails.] + + python testing (if python code has changed; see instructions in python/README.md; document testing done): + + (any machine) - cheyenne PASS + + [If python code has changed and you are NOT running aux_clm (e.g., because the only changes are in python + code) then also run the clm_pymods test suite; this is a small subset of aux_clm that runs the system + tests impacted by python changes. The best way to do this, if you expect no changes from the last tag in + either model output or namelists, is: create sym links pointing to the last tag's baseline directory, + named with the upcoming tag; then run the clm_pymods test suite comparing against these baselines but NOT + doing their own baseline generation. If you are already running the full aux_clm then you do NOT need to + separately run the clm_pymods test suite, and you can remove the following line.] + + clm_pymods test suite on cheyenne - PASS + + any other testing (give details below): + + +Answer changes +-------------- +Changes answers relative to baseline: NO + + +Other details +------------- +Pull Requests that document the changes (include PR ids): + https://github.com/ESCOMP/ctsm/pull/1615 + +=============================================================== +=============================================================== Tag name: ctsm5.1.dev072 Originator(s): negins (Negin Sobhani,UCAR/TSS,303-497-1224) Date: Mon Jan 17 10:50:25 MST 2022 diff --git a/doc/ChangeSum b/doc/ChangeSum index 9392ae82b0..cabc3a3e81 100644 --- a/doc/ChangeSum +++ b/doc/ChangeSum @@ -1,5 +1,6 @@ Tag Who Date Summary ============================================================================================================================ + ctsm5.1.dev073 slevis 01/26/2022 Replace dom_nat_pft with dom_plant to enable crop in fsurdat_modifier tool ctsm5.1.dev072 negins 01/17/2022 mksurfdat toolchain part 1: gen_mksurf_namelist ctsm5.1.dev071 glemieux 01/16/2022 Small changes to enable new fates dimension and update fates tag ctsm5.1.dev070 multiple 01/10/2022 Update externals, remove need for LND_DOMAIN_FILE and LND_DOMAIN_PATH, etc. From f6c1479554e2fd093abbe3c8098922a08c56dc9e Mon Sep 17 00:00:00 2001 From: Samuel Levis Date: Fri, 25 Feb 2022 11:55:59 -0700 Subject: [PATCH 8/9] Various comments updated; python tests PASS; clm_pymods tests PASS --- doc/ChangeLog | 8 ----- python/ctsm/test/test_sys_fsurdat_modifier.py | 36 +++++++++++++++---- tools/modify_fsurdat/modify_template.cfg | 5 ++- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/doc/ChangeLog b/doc/ChangeLog index b116ff2588..87ce73b2ca 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -58,14 +58,6 @@ Testing summary: (any machine) - cheyenne PASS - [If python code has changed and you are NOT running aux_clm (e.g., because the only changes are in python - code) then also run the clm_pymods test suite; this is a small subset of aux_clm that runs the system - tests impacted by python changes. The best way to do this, if you expect no changes from the last tag in - either model output or namelists, is: create sym links pointing to the last tag's baseline directory, - named with the upcoming tag; then run the clm_pymods test suite comparing against these baselines but NOT - doing their own baseline generation. If you are already running the full aux_clm then you do NOT need to - separately run the clm_pymods test suite, and you can remove the following line.] - clm_pymods test suite on cheyenne - PASS any other testing (give details below): diff --git a/python/ctsm/test/test_sys_fsurdat_modifier.py b/python/ctsm/test/test_sys_fsurdat_modifier.py index 8603769776..5754269d59 100755 --- a/python/ctsm/test/test_sys_fsurdat_modifier.py +++ b/python/ctsm/test/test_sys_fsurdat_modifier.py @@ -25,6 +25,15 @@ class TestSysFsurdatModifier(unittest.TestCase): """System tests for fsurdat_modifier""" def setUp(self): + """ + Obtain path to the existing: + - modify_template.cfg file + - /testinputs directory and fsurdat_in, located in /testinputs + Make /_tempdir for use by these tests. + Obtain path and names for the files being created in /_tempdir: + - modify_fsurdat.cfg + - fsurdat_out.nc + """ self._cfg_template_path = os.path.join(path_to_ctsm_root(), 'tools/modify_fsurdat/modify_template.cfg') testinputs_path = os.path.join(path_to_ctsm_root(), @@ -44,6 +53,7 @@ def tearDown(self): def test_minimalInfo(self): """ This test specifies a minimal amount of information + Create .cfg file, run the tool, compare fsurdat_in to fsurdat_out """ self._create_config_file_minimal() @@ -61,7 +71,8 @@ def test_minimalInfo(self): def test_crop(self): """ - This version replances the vegetation with a crop + This version replaces the vegetation with a crop + Create .cfg file, run the tool, compare fsurdat_in to fsurdat_out """ self._create_config_file_crop() @@ -77,7 +88,7 @@ def test_crop(self): # assert that fsurdat_out does not equal fsurdat_in self.assertFalse(fsurdat_out_data.equals(fsurdat_in_data)) - # compare fsurdat_out to fsurdat_out_baseline + # compare fsurdat_out to fsurdat_out_baseline located in /testinputs fsurdat_out_baseline = self._fsurdat_in[:-3] + '_modified_with_crop' + \ self._fsurdat_in[-3:] fsurdat_out_base_data = xr.open_dataset(fsurdat_out_baseline) @@ -88,6 +99,7 @@ def test_crop(self): def test_allInfo(self): """ This version specifies all possible information + Create .cfg file, run the tool, compare fsurdat_in to fsurdat_out """ self._create_config_file_complete() @@ -103,7 +115,7 @@ def test_allInfo(self): # assert that fsurdat_out does not equal fsurdat_in self.assertFalse(fsurdat_out_data.equals(fsurdat_in_data)) - # compare fsurdat_out to fsurdat_out_baseline + # compare fsurdat_out to fsurdat_out_baseline located in /testinputs fsurdat_out_baseline = self._fsurdat_in[:-3] + '_modified' + \ self._fsurdat_in[-3:] fsurdat_out_base_data = xr.open_dataset(fsurdat_out_baseline) @@ -112,7 +124,11 @@ def test_allInfo(self): def _create_config_file_minimal(self): - + """ + Open the new and the template .cfg files + Loop line by line through the template .cfg file + When string matches, replace that line's content + """ with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: with open (self._cfg_template_path, 'r', encoding='utf-8') as cfg_in: for line in cfg_in: @@ -124,7 +140,11 @@ def _create_config_file_minimal(self): def _create_config_file_crop(self): - + """ + Open the new and the template .cfg files + Loop line by line through the template .cfg file + When string matches, replace that line's content + """ with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: with open (self._cfg_template_path, 'r', encoding='utf-8') as cfg_in: for line in cfg_in: @@ -154,7 +174,11 @@ def _create_config_file_crop(self): def _create_config_file_complete(self): - + """ + Open the new and the template .cfg files + Loop line by line through the template .cfg file + When string matches, replace that line's content + """ with open (self._cfg_file_path, 'w', encoding='utf-8') as cfg_out: with open (self._cfg_template_path, 'r', encoding='utf-8') as cfg_in: for line in cfg_in: diff --git a/tools/modify_fsurdat/modify_template.cfg b/tools/modify_fsurdat/modify_template.cfg index 6b18cedc36..fa134d34e3 100644 --- a/tools/modify_fsurdat/modify_template.cfg +++ b/tools/modify_fsurdat/modify_template.cfg @@ -58,7 +58,10 @@ landmask_file = UNSET # PFT/CFT to be set to 100% according to user-defined mask. # If idealized = True and dom_plant = UNSET, the latter defaults to 0 -# (bare soil). Valid values 0 to 78 (int). +# (bare soil). Valid values range from 0 to a max value (int) that one can +# obtain from the fsurdat_in file using ncdump (or method preferred by user). +# The max valid value will equal (lsmpft - 1) and will also equal the last +# value of cft(cft). dom_plant = UNSET # LAI, SAI, HEIGHT_TOP, and HEIGHT_BOT values by month for dom_plant From 2d9989fece6c6643ebb35740e5f795cfc6f7bae1 Mon Sep 17 00:00:00 2001 From: Erik Kluzek Date: Mon, 28 Feb 2022 10:12:29 -0700 Subject: [PATCH 9/9] Update date on change files --- doc/ChangeLog | 2 +- doc/ChangeSum | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ChangeLog b/doc/ChangeLog index 87ce73b2ca..2c3a65192a 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,7 +1,7 @@ =============================================================== Tag name: ctsm5.1.dev082 Originator(s): slevis (Samuel Levis,SLevis Consulting,303-665-1310) -Date: Fri Feb 25 10:42:41 MST 2022 +Date: Mon Feb 28 10:12:16 MST 2022 One-line Summary: Replace dom_nat_pft with dom_plant to enable crop in fsurdat_modifier tool Purpose and description of changes diff --git a/doc/ChangeSum b/doc/ChangeSum index 8ab5dc6631..5eaedfc0a6 100644 --- a/doc/ChangeSum +++ b/doc/ChangeSum @@ -1,6 +1,6 @@ Tag Who Date Summary ============================================================================================================================ - ctsm5.1.dev082 slevis 02/25/2022 Replace dom_nat_pft with dom_plant to enable crop in fsurdat_modifier tool + ctsm5.1.dev082 slevis 02/28/2022 Replace dom_nat_pft with dom_plant to enable crop in fsurdat_modifier tool ctsm5.1.dev081 swensosc 02/24/2022 Do not subtract irrigation from QRUNOFF diagnostic ctsm5.1.dev080 sacks 02/24/2022 Use avg days per year when converting param units ctsm5.1.dev079 sacks 02/24/2022 Changes to CropPhenology timing