diff --git a/doc/ChangeLog b/doc/ChangeLog index 4aa053c9cb..2c3a65192a 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,4 +1,80 @@ =============================================================== +Tag name: ctsm5.1.dev082 +Originator(s): slevis (Samuel Levis,SLevis Consulting,303-665-1310) +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 +---------------------------------- + + 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 + + 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.dev081 Originator(s): swensosc (Sean Swenson) Date: Thu Feb 24 21:33:35 MST 2022 diff --git a/doc/ChangeSum b/doc/ChangeSum index 146f690647..5eaedfc0a6 100644 --- a/doc/ChangeSum +++ b/doc/ChangeSum @@ -1,5 +1,6 @@ Tag Who Date Summary ============================================================================================================================ + 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 diff --git a/python/ctsm/modify_fsurdat/fsurdat_modifier.py b/python/ctsm/modify_fsurdat/fsurdat_modifier.py index d4f62a1fee..f9c0ac9682 100644 --- a/python/ctsm/modify_fsurdat/fsurdat_modifier.py +++ b/python/ctsm/modify_fsurdat/fsurdat_modifier.py @@ -65,10 +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 + 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', @@ -84,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, @@ -96,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 # ------------------------------ @@ -112,25 +114,33 @@ 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_plant call follows zero_nonveg because it modifies PCT_NATVEG + # and PCT_CROP in the user-defined rectangle + 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 13f07e60c0..10803875c7 100644 --- a/python/ctsm/modify_fsurdat/modify_fsurdat.py +++ b/python/ctsm/modify_fsurdat/modify_fsurdat.py @@ -27,7 +27,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) @@ -36,20 +36,21 @@ 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 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) @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,9 +87,8 @@ def _get_not_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 not_rectangle + return rectangle def write_output(self, fsurdat_in, fsurdat_out): @@ -129,36 +129,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 + 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_nat_pft + values selected by the user for dom_plant Arguments --------- - dom_nat_pft: - (int) User's entry of PFT 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_nat_pft + (float) User's entry of MONTHLY_LAI for their dom_plant sai: - (float) User's entry of MONTHLY_SAI for their dom_nat_pft + (float) User's entry of MONTHLY_SAI for their dom_plant hgt_top: - (float) User's entry of MONTHLY_HEIGHT_TOP for their dom_nat_pft + (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_nat_pft + (float) User's entry of MONTHLY_HEIGHT_BOT for their dom_plant """ - 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) + # 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, @@ -167,26 +183,26 @@ 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, - var=var, val=val) + self.set_lai_sai_hgts(dom_plant=dom_plant, 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 - val = [0] * 12 - if len(val) != 12: - errmsg = 'Error: Variable should have exactly 12 ' \ - 'entries in the configure file: ' + var + months = int(max(self.file.time)) # 12 months + if dom_plant == 0: # bare soil: var must equal 0 + 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_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/python/ctsm/test/test_sys_fsurdat_modifier.py b/python/ctsm/test/test_sys_fsurdat_modifier.py index 7d2819261c..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() @@ -59,9 +69,37 @@ def test_minimalInfo(self): self.assertTrue(fsurdat_out_data.equals(fsurdat_in_data)) + def test_crop(self): + """ + 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() + + # 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 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) + # 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 + Create .cfg file, run the tool, compare fsurdat_in to fsurdat_out """ self._create_config_file_complete() @@ -77,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) @@ -86,26 +124,68 @@ 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: + 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}' + cfg_out.write(line) + - with open (self._cfg_file_path,'w') as cfg_out: - with open (self._cfg_template_path,'r') as cfg_in: + 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: 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' *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') as cfg_out: - with open (self._cfg_template_path,'r') as cfg_in: + """ + 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: 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): @@ -116,8 +196,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 e3aa9320c7..a5182ae6e4 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 @@ -22,7 +22,7 @@ class TestModifyFsurdat(unittest.TestCase): """Tests the setvar_lev functions and the - _get_not_rectangle function + _get_rectangle function """ def test_setvarLev(self): @@ -121,15 +121,11 @@ 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( - lon_1=lon_1, - lon_2=lon_2, - lat_1=lat_1, - lat_2=lat_2, - longxy=longxy, - latixy=latixy, - ) - compare = np.ones((rows, cols)) + 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)) @@ -166,15 +162,11 @@ 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( - lon_1=lon_1, - lon_2=lon_2, - lat_1=lat_1, - lat_2=lat_2, - longxy=longxy, - latixy=latixy, - ) - compare = np.ones((rows, cols)) + 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)) @@ -210,15 +202,11 @@ 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( - lon_1=lon_1, - lon_2=lon_2, - lat_1=lat_1, - lat_2=lat_2, - longxy=longxy, - latixy=latixy, - ) - compare = np.ones((rows, cols)) + 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)) @@ -254,15 +242,11 @@ 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( - lon_1=lon_1, - lon_2=lon_2, - lat_1=lat_1, - lat_2=lat_2, - longxy=longxy, - latixy=latixy, - ) - compare = np.ones((rows, cols)) + 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)) @@ -300,15 +284,11 @@ 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( - lon_1=lon_1, - lon_2=lon_2, - lat_1=lat_1, - lat_2=lat_2, - longxy=longxy, - latixy=latixy, - ) - compare = np.ones((rows, cols)) + 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)) @@ -342,17 +322,11 @@ def test_getNotRectangle_latsOutOfBounds(self): lon_2 = 5 lat_1 = -91 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( - lon_1=lon_1, - lon_2=lon_2, - lat_1=lat_1, - lat_2=lat_2, - longxy=longxy, - latixy=latixy, - ) + with self.assertRaisesRegex(SystemExit, + "lat_1 and lat_2 need to be in the range -90 to 90"): + _ = ModifyFsurdat._get_rectangle( + lon_1=lon_1, lon_2=lon_2, lat_1=lat_1, lat_2=lat_2, + longxy=longxy, latixy=latixy) def _get_longxy_latixy(self, _min_lon, _max_lon, _min_lat, _max_lat): """ 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 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 56e8221635..fa134d34e3 100644 --- a/tools/modify_fsurdat/modify_template.cfg +++ b/tools/modify_fsurdat/modify_template.cfg @@ -56,13 +56,16 @@ 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 +# 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 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_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