From f3a220d83ea0cc01283b6b6a76d988f81f06dee2 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 28 Aug 2024 14:00:43 -0600 Subject: [PATCH 01/81] included a min_depth parameter and overwite this in MOM_input --- regional_mom6/regional_mom6.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 55d6120b..e86a78d4 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -485,6 +485,7 @@ def __init__( self.repeat_year_forcing = repeat_year_forcing self.ocean_mask = None self.layout = None # This should be a tuple. Leaving in a dummy 'None' makes it easy to remind the user to provide a value later on. + self.min_depth = 0.0 # Minimum depth. Shallower water will be masked out. This value is overwritten when running "setup_bathymetry" method. if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -1356,7 +1357,7 @@ def tidy_bathymetry( ## REMOVE INLAND LAKES min_depth = self.vgrid.zi[minimum_layers] - + self.min_depth = min_depth ocean_mask = bathymetry.copy(deep=True).depth.where( bathymetry.depth <= min_depth, 1 ) @@ -1698,7 +1699,7 @@ def setup_run_directory( print("Number of CPUs required: ", ncpus) - ## Modify the input namelists to give the correct layouts + ## Modify the MOM_layout file to have correct horizontal dimensions and CPU layout # TODO Re-implement with package that works for this file type? or at least tidy up code with open(self.mom_run_dir / "MOM_layout", "r") as file: lines = file.readlines() @@ -1716,10 +1717,21 @@ def setup_run_directory( if "NJGLOBAL" in lines[jj]: lines[jj] = f"NJGLOBAL = {self.hgrid.ny.shape[0]//2}\n" - with open(self.mom_run_dir / "MOM_layout", "w") as f: f.writelines(lines) + # Overwrite values pertaining to vertical structure in the MOM_input file + with open(self.mom_run_dir / "MOM_input", "r") as file: + lines = file.readlines() + for jj in range(len(lines)): + if "MINIMUM_DEPTH" in lines[jj]: + lines[jj] = f'MINIMUM_DEPTH = "{self.min_depth}"\n' + if "NK =" in lines[jj]: + lines[jj] = f'NK = {len(self.vgrid.zl.values)}\n' + with open(self.mom_run_dir / "MOM_input", "w") as f: + f.writelines(lines) + + ## If using payu to run the model, create a payu configuration file if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): os.remove(f"{self.mom_run_dir}/config.yaml") From 0523f1d595b3d2bb7fc8a250dfe8a168d02adcd1 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 28 Aug 2024 14:02:04 -0600 Subject: [PATCH 02/81] black --- regional_mom6/regional_mom6.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index e86a78d4..8cd57c0d 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -485,7 +485,7 @@ def __init__( self.repeat_year_forcing = repeat_year_forcing self.ocean_mask = None self.layout = None # This should be a tuple. Leaving in a dummy 'None' makes it easy to remind the user to provide a value later on. - self.min_depth = 0.0 # Minimum depth. Shallower water will be masked out. This value is overwritten when running "setup_bathymetry" method. + self.min_depth = 0.0 # Minimum depth. Shallower water will be masked out. This value is overwritten when running "setup_bathymetry" method. if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -1727,11 +1727,10 @@ def setup_run_directory( if "MINIMUM_DEPTH" in lines[jj]: lines[jj] = f'MINIMUM_DEPTH = "{self.min_depth}"\n' if "NK =" in lines[jj]: - lines[jj] = f'NK = {len(self.vgrid.zl.values)}\n' + lines[jj] = f"NK = {len(self.vgrid.zl.values)}\n" with open(self.mom_run_dir / "MOM_input", "w") as f: f.writelines(lines) - ## If using payu to run the model, create a payu configuration file if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): os.remove(f"{self.mom_run_dir}/config.yaml") From d86b147b84c268f1ebad42b5916481c04f2d2b08 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Thu, 29 Aug 2024 13:41:25 -0600 Subject: [PATCH 03/81] Remove problematic comment at top of override file. Causes issus for CESM when parsing --- demos/premade_run_directories/common_files/MOM_override | 2 -- 1 file changed, 2 deletions(-) diff --git a/demos/premade_run_directories/common_files/MOM_override b/demos/premade_run_directories/common_files/MOM_override index 7b0f9f37..c11872b7 100644 --- a/demos/premade_run_directories/common_files/MOM_override +++ b/demos/premade_run_directories/common_files/MOM_override @@ -1,4 +1,2 @@ -## Add override files here - #override DT=50 #override DT_THERM=300 From 3270057ae5f6db84694d018d09c5541b67fed818 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Thu, 29 Aug 2024 15:14:20 -0600 Subject: [PATCH 04/81] having mask table and layout no longer required --- regional_mom6/regional_mom6.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 8cd57c0d..6feba794 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1682,8 +1682,8 @@ def setup_run_directory( if mask_table == None: if self.layout == None: - raise AttributeError( - "No mask table found, and the cpu layout has not been set. At least one of these is requiret to set up the experiment." + print( + "WARNING: No mask table found, and the cpu layout has not been set. At least one of these is requiret to set up the experiment if you're running MOM6 standalone with the FMS coupler. If you're running within CESM, ignore this message." ) print( f"No mask table found, but the cpu layout has been set to {self.layout} This suggests the domain is mostly water, so there are " From 42c28409a6bb4419748260506ebefea4f2eb2608 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Thu, 29 Aug 2024 15:30:15 -0600 Subject: [PATCH 05/81] fix layout requirements --- regional_mom6/regional_mom6.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 6feba794..605e1380 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1685,19 +1685,20 @@ def setup_run_directory( print( "WARNING: No mask table found, and the cpu layout has not been set. At least one of these is requiret to set up the experiment if you're running MOM6 standalone with the FMS coupler. If you're running within CESM, ignore this message." ) - print( - f"No mask table found, but the cpu layout has been set to {self.layout} This suggests the domain is mostly water, so there are " - + "no `non compute` cells that are entirely land. If this doesn't seem right, " - + "ensure you've already run the `FRE_tools` method which sets up the cpu mask table. Keep an eye on any errors that might print while" - + "the FRE tools (which run C++ in the background) are running." - ) - # Here we define a local copy of the layout just for use within this function. - # This prevents the layout from being overwritten in the main class in case - # in case the user accidentally loads in the wrong mask table. - layout = self.layout - ncpus = layout[0] * layout[1] + else: + print( + f"No mask table found, but the cpu layout has been set to {self.layout} This suggests the domain is mostly water, so there are " + + "no `non compute` cells that are entirely land. If this doesn't seem right, " + + "ensure you've already run the `FRE_tools` method which sets up the cpu mask table. Keep an eye on any errors that might print while" + + "the FRE tools (which run C++ in the background) are running." + ) + # Here we define a local copy of the layout just for use within this function. + # This prevents the layout from being overwritten in the main class in case + # in case the user accidentally loads in the wrong mask table. + layout = self.layout + ncpus = layout[0] * layout[1] - print("Number of CPUs required: ", ncpus) + print("Number of CPUs required: ", ncpus) ## Modify the MOM_layout file to have correct horizontal dimensions and CPU layout # TODO Re-implement with package that works for this file type? or at least tidy up code @@ -1734,7 +1735,8 @@ def setup_run_directory( ## If using payu to run the model, create a payu configuration file if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): os.remove(f"{self.mom_run_dir}/config.yaml") - + elif ncpus == None: + print("WARNING: Layout has not been set! Cannot create payu configuration file. Run the FRE_tools first.") else: with open(f"{self.mom_run_dir}/config.yaml", "r") as file: lines = file.readlines() From 005fb4469fa8a50defe3dd59ef8c9209a8ba4f9b Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Thu, 29 Aug 2024 15:50:15 -0600 Subject: [PATCH 06/81] fixed and working --- regional_mom6/regional_mom6.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 605e1380..3f981f27 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1679,11 +1679,15 @@ def setup_run_directory( print( f"Mask table {p.name} read. Using this to infer the cpu layout {layout}, total masked out cells {masked}, and total number of CPUs {ncpus}." ) - + # Case where there's no mask table. Either because user hasn't run FRE tools, or because the domain is mostly water. if mask_table == None: - if self.layout == None: + # Here we define a local copy of the layout just for use within this function. + # This prevents the layout from being overwritten in the main class in case + # in case the user accidentally loads in the wrong mask table. + layout = self.layout + if layout == None: print( - "WARNING: No mask table found, and the cpu layout has not been set. At least one of these is requiret to set up the experiment if you're running MOM6 standalone with the FMS coupler. If you're running within CESM, ignore this message." + "WARNING: No mask table found, and the cpu layout has not been set. \nAt least one of these is requiret to set up the experiment if you're running MOM6 standalone with the FMS coupler. \nIf you're running within CESM, ignore this message." ) else: print( @@ -1692,12 +1696,9 @@ def setup_run_directory( + "ensure you've already run the `FRE_tools` method which sets up the cpu mask table. Keep an eye on any errors that might print while" + "the FRE tools (which run C++ in the background) are running." ) - # Here we define a local copy of the layout just for use within this function. - # This prevents the layout from being overwritten in the main class in case - # in case the user accidentally loads in the wrong mask table. - layout = self.layout - ncpus = layout[0] * layout[1] + + ncpus = layout[0] * layout[1] print("Number of CPUs required: ", ncpus) ## Modify the MOM_layout file to have correct horizontal dimensions and CPU layout @@ -1710,7 +1711,7 @@ def setup_run_directory( lines[jj] = f'MASKTABLE = "{mask_table}"\n' else: lines[jj] = "# MASKTABLE = no mask table" - if "LAYOUT =" in lines[jj] and "IO" not in lines[jj]: + if "LAYOUT =" in lines[jj] and "IO" not in lines[jj] and layout != None: lines[jj] = f"LAYOUT = {layout[1]},{layout[0]}\n" if "NIGLOBAL" in lines[jj]: From 2268c8f40ca790f382c171dd5fced75849c3401a Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 4 Sep 2024 15:16:05 -0600 Subject: [PATCH 07/81] fix typo in find replace --- regional_mom6/regional_mom6.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 3f981f27..ac54c4dd 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1726,7 +1726,7 @@ def setup_run_directory( with open(self.mom_run_dir / "MOM_input", "r") as file: lines = file.readlines() for jj in range(len(lines)): - if "MINIMUM_DEPTH" in lines[jj]: + if "MINIMUM_DEPTH = " in lines[jj]: lines[jj] = f'MINIMUM_DEPTH = "{self.min_depth}"\n' if "NK =" in lines[jj]: lines[jj] = f"NK = {len(self.vgrid.zl.values)}\n" From 61478039d1af096bab61b5cfecd941e0bc7b5ed9 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 4 Sep 2024 15:51:57 -0600 Subject: [PATCH 08/81] replace minimum layers with min depth --- regional_mom6/regional_mom6.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index ac54c4dd..7d1a64f8 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -442,6 +442,7 @@ class experiment: the grids and the ocean mask are being read from within the ``mom_input_dir`` and ``mom_run_dir`` directories. Useful for modifying or troubleshooting experiments. Default: ``False``. + minimum_depth (Optional[int]): The minimum depth in meters of a grid cell allowed before it is masked out and treated as land. """ def __init__( @@ -460,6 +461,7 @@ def __init__( grid_type="even_spacing", repeat_year_forcing=False, read_existing_grids=False, + minimum_depth = 4 ): ## in case list was given, convert to tuples self.longitude_extent = tuple(longitude_extent) @@ -485,7 +487,7 @@ def __init__( self.repeat_year_forcing = repeat_year_forcing self.ocean_mask = None self.layout = None # This should be a tuple. Leaving in a dummy 'None' makes it easy to remind the user to provide a value later on. - self.min_depth = 0.0 # Minimum depth. Shallower water will be masked out. This value is overwritten when running "setup_bathymetry" method. + self.min_depth = minimum_depth # Minimum depth. Shallower water will be masked out. if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -1106,7 +1108,6 @@ def setup_bathymetry( latitude_coordinate_name="lat", vertical_coordinate_name="elevation", fill_channels=False, - minimum_layers=3, positive_down=False, chunks="auto", ): @@ -1130,10 +1131,6 @@ def setup_bathymetry( fill_channels (Optional[bool]): Whether or not to fill in diagonal channels. This removes more narrow inlets, but can also connect extra islands to land. Default: ``False``. - minimum_layers (Optional[int]): The minimum depth allowed as an integer - number of layers. Anything shallower than the ``minimum_layers`` - (as specified by the vertical coordinate file ``vcoord.nc``) is deemed land. - Default: 3. positive_down (Optional[bool]): If ``True``, it assumes that bathymetry vertical coordinate is positive down. Default: ``False``. chunks (Optional Dict[str, str]): Horizontal chunking scheme for the bathymetry, e.g., @@ -1307,10 +1304,10 @@ def setup_bathymetry( "Regridding finished. Now calling `tidy_bathymetry` method for some finishing touches..." ) - self.tidy_bathymetry(fill_channels, minimum_layers, positive_down) + self.tidy_bathymetry(fill_channels, positive_down) def tidy_bathymetry( - self, fill_channels=False, minimum_layers=3, positive_down=True + self, fill_channels=False, positive_down=True ): """ An auxiliary function for bathymetry used to fix up the metadata and remove inland @@ -1326,10 +1323,6 @@ def tidy_bathymetry( fill_channels (Optional[bool]): Whether to fill in diagonal channels. This removes more narrow inlets, but can also connect extra islands to land. Default: ``False``. - minimum_layers (Optional[int]): The minimum depth allowed - as an integer number of layers. The default value of ``3`` - layers means that anything shallower than the 3rd - layer (as specified by the ``vcoord``) is deemed land. positive_down (Optional[bool]): If ``True`` (default), assume that bathymetry vertical coordinate is positive down. """ @@ -1356,10 +1349,8 @@ def tidy_bathymetry( ## REMOVE INLAND LAKES - min_depth = self.vgrid.zi[minimum_layers] - self.min_depth = min_depth ocean_mask = bathymetry.copy(deep=True).depth.where( - bathymetry.depth <= min_depth, 1 + bathymetry.depth <= self.min_depth, 1 ) land_mask = np.abs(ocean_mask - 1) From 7caae56d72e8be77ca94e44cfefbad551873a354 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 4 Sep 2024 15:52:40 -0600 Subject: [PATCH 09/81] black --- regional_mom6/regional_mom6.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 7d1a64f8..b87ef01f 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -461,7 +461,7 @@ def __init__( grid_type="even_spacing", repeat_year_forcing=False, read_existing_grids=False, - minimum_depth = 4 + minimum_depth=4, ): ## in case list was given, convert to tuples self.longitude_extent = tuple(longitude_extent) @@ -487,7 +487,9 @@ def __init__( self.repeat_year_forcing = repeat_year_forcing self.ocean_mask = None self.layout = None # This should be a tuple. Leaving in a dummy 'None' makes it easy to remind the user to provide a value later on. - self.min_depth = minimum_depth # Minimum depth. Shallower water will be masked out. + self.min_depth = ( + minimum_depth # Minimum depth. Shallower water will be masked out. + ) if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -1306,9 +1308,7 @@ def setup_bathymetry( self.tidy_bathymetry(fill_channels, positive_down) - def tidy_bathymetry( - self, fill_channels=False, positive_down=True - ): + def tidy_bathymetry(self, fill_channels=False, positive_down=True): """ An auxiliary function for bathymetry used to fix up the metadata and remove inland lakes after regridding the bathymetry. Having `tidy_bathymetry` as a separate @@ -1688,7 +1688,6 @@ def setup_run_directory( + "the FRE tools (which run C++ in the background) are running." ) - ncpus = layout[0] * layout[1] print("Number of CPUs required: ", ncpus) @@ -1728,7 +1727,9 @@ def setup_run_directory( if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): os.remove(f"{self.mom_run_dir}/config.yaml") elif ncpus == None: - print("WARNING: Layout has not been set! Cannot create payu configuration file. Run the FRE_tools first.") + print( + "WARNING: Layout has not been set! Cannot create payu configuration file. Run the FRE_tools first." + ) else: with open(f"{self.mom_run_dir}/config.yaml", "r") as file: lines = file.readlines() From 718dc1c19ab964d073049a9eae30661b55eafae2 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 4 Sep 2024 15:59:24 -0600 Subject: [PATCH 10/81] fix tests --- tests/test_expt_class.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index d0713c2f..a601102f 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -93,7 +93,6 @@ def test_setup_bathymetry( longitude_coordinate_name="silly_lon", latitude_coordinate_name="silly_lat", vertical_coordinate_name="silly_depth", - minimum_layers=1, chunks={"longitude": 10, "latitude": 10}, ) From 88938a2e0dea5e28c37b1815449e74bc796fc189 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 4 Sep 2024 16:07:03 -0600 Subject: [PATCH 11/81] update notebook --- demos/reanalysis-forced.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index f66e8333..9f1dcf2d 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -135,6 +135,7 @@ " number_vertical_layers = 75,\n", " layer_thickness_ratio = 10,\n", " depth = 4500,\n", + " minimum_depth = 5,\n", " mom_run_dir = run_dir,\n", " mom_input_dir = input_dir,\n", " toolpath_dir = toolpath_dir\n", @@ -217,7 +218,6 @@ " longitude_coordinate_name='lon',\n", " latitude_coordinate_name='lat',\n", " vertical_coordinate_name='elevation',\n", - " minimum_layers=1\n", " )" ] }, From a21e5640090ce3dda04610106c0c1876d2485c7f Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 4 Sep 2024 16:33:14 -0600 Subject: [PATCH 12/81] bugfix setup rundir when overwriting --- regional_mom6/regional_mom6.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index b87ef01f..ee0a0937 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1624,7 +1624,7 @@ def setup_run_directory( if not overwrite: for file in base_run_dir.glob( "*" - ): ## copy each file individually if it doesn't already exist OR overwrite = True + ): ## copy each file individually if it doesn't already exist if not os.path.exists(self.mom_run_dir / file.name): ## Check whether this file exists in an override directory or not if ( @@ -1637,7 +1637,7 @@ def setup_run_directory( else: shutil.copytree(base_run_dir, self.mom_run_dir, dirs_exist_ok=True) if overwrite_run_dir != False: - shutil.copy(base_run_dir / file, self.mom_run_dir) + shutil.copytree(base_run_dir, self.mom_run_dir, dirs_exist_ok=True) ## Make symlinks between run and input directories inputdir_in_rundir = self.mom_run_dir / "inputdir" From 2488100087a498e7a143128a09bb81799cd20c50 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Thu, 5 Sep 2024 16:04:30 -0600 Subject: [PATCH 13/81] fix issue where ocean mask would fail to remove values lower than ocean depth --- regional_mom6/regional_mom6.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index ee0a0937..a7d5f476 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1349,8 +1349,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): ## REMOVE INLAND LAKES - ocean_mask = bathymetry.copy(deep=True).depth.where( - bathymetry.depth <= self.min_depth, 1 + ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= self.min_depth, 0,1 ) land_mask = np.abs(ocean_mask - 1) @@ -1717,7 +1716,7 @@ def setup_run_directory( lines = file.readlines() for jj in range(len(lines)): if "MINIMUM_DEPTH = " in lines[jj]: - lines[jj] = f'MINIMUM_DEPTH = "{self.min_depth}"\n' + lines[jj] = f'MINIMUM_DEPTH = {float(self.min_depth)}\n' if "NK =" in lines[jj]: lines[jj] = f"NK = {len(self.vgrid.zl.values)}\n" with open(self.mom_run_dir / "MOM_input", "w") as f: From d28a5d3325f8f844aaea7a0d114352f6c38fdfe3 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 17 Sep 2024 10:59:01 -0600 Subject: [PATCH 14/81] Start Setup Tides --- regional_mom6/regional_mom6.py | 250 ++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 6 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index a7d5f476..8bd7612f 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1101,7 +1101,51 @@ def simple_boundary( seg.rectangular_brushcut() print("Done.") return + + def setup_tides( + self, path_to_td, segment_number, arakawa_grid="A" + ): + """ + Here, we subset our tidal data and generate more boundary files! + Args: + path_to_td (str): Path to boundary tidal file. Ideally this should be a pre cut-out + netCDF file containing only the boundary region and 3 extra boundary points on either + side. Users can also provide a large dataset containing their entire domain but this + will be slower. + segment_number (int): Number the segments according to how they'll be specified in + the ``MOM_input``. + arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. + Either ``'A'`` (default), ``'B'``, or ``'C'``. + Returns: + *.nc files + Tidal input files for the boundaries from the TPXO dataaset + """ + orientation = ["North", "South", "West","East"] + tidal_constituents = ["M1"] + for o in orientation: + for t in tidal_constituents: + + print("Processing tides at {} boundary...".format(orientation), end="") + if not path_to_td.exists(): + raise FileNotFoundError( + f"Boundary file not found at {path_to_td}. Please ensure that the files are named in the format `FILLIN`." + ) + seg = segment( + hgrid=self.hgrid, + infile_td=path_to_td, # location of raw boundary + outfolder=self.mom_input_dir, + varnames=None, + segment_name="segment_{:03d}".format(segment_number), + orientation=orientation, # orienataion + startdate=self.date_range[0], + arakawa_grid=arakawa_grid, + repeat_year_forcing=self.repeat_year_forcing, + ) + + seg.regrid_tides() + print("Done.") + return def setup_bathymetry( self, *, @@ -1866,8 +1910,8 @@ def setup_era5(self, era5_path): class segment: """ - Class to turn raw boundary segment data into MOM6 boundary - segments. + Class to turn raw boundary and tidal segment data into MOM6 boundary + and tidal segments. Boundary segments should only contain the necessary data for that segment. No horizontal chunking is done here, so big fat segments @@ -1882,7 +1926,8 @@ class segment: Args: hgrid (xarray.Dataset): The horizontal grid used for domain. - infile (Union[str, Path]): Path to the raw, unprocessed boundary segment. + infile_bc (Union[str, Path]): Path to the raw, unprocessed boundary segment. + infile_td (Union[str, Path]): Path to the raw, unprocessed tidal segment. outfolder (Union[str, Path]): Path to folder where the model inputs will be stored. varnames (Dict[str, str]): Mapping between the variable/dimension names and @@ -1910,7 +1955,7 @@ def __init__( self, *, hgrid, - infile, + infile_bc, outfolder, varnames, segment_name, @@ -1918,6 +1963,7 @@ def __init__( startdate, arakawa_grid="A", time_units="days", + infile_td = None, tidal_constituents=None, repeat_year_forcing=False, ): @@ -1956,7 +2002,8 @@ def __init__( raise ValueError("arakawa_grid must be one of: 'A', 'B', or 'C'") self.arakawa_grid = arakawa_grid - self.infile = infile + self.infile_bc = infile_bc + self.infile_td = infile_td self.outfolder = outfolder self.hgrid = hgrid self.segment_name = segment_name @@ -2008,7 +2055,7 @@ def rectangular_brushcut(self): } ).set_coords(["lat", "lon"]) - rawseg = xr.open_dataset(self.infile, decode_times=False, engine="netcdf4") + rawseg = xr.open_dataset(self.infile_bc, decode_times=False, engine="netcdf4") if self.arakawa_grid == "A": rawseg = rawseg.rename({self.x: "lon", self.y: "lat"}) @@ -2279,3 +2326,194 @@ def rectangular_brushcut(self): ) return segment_out, encoding_dict + + def ap2ep(uc, vc): + """Convert complex tidal u and v to tidal ellipse. + Adapted from ap2ep.m for matlab + Original copyright notice: + %Authorship Copyright: + % + % The author retains the copyright of this program, while you are welcome + % to use and distribute it as long as you credit the author properly and respect + % the program name itself. Particularly, you are expected to retain the original + % author's name in this original version or any of its modified version that + % you might make. You are also expected not to essentially change the name of + % the programs except for adding possible extension for your own version you + % might create, e.g. ap2ep_xx is acceptable. Any suggestions are welcome and + % enjoy my program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + % Release Date: Nov. 2000, Revised on May. 2002 to adopt Foreman's northern semi + % major axis convention. + + Args: + uc: complex tidal u velocity + vc: complex tidal v velocity + + Returns: + (semi-major axis, eccentricity, inclination [radians], phase [radians]) + """ + wp = (uc + 1j * vc) / 2.0 + wm = np.conj(uc - 1j * vc) / 2.0 + + Wp = np.abs(wp) + Wm = np.abs(wm) + THETAp = np.angle(wp) + THETAm = np.angle(wm) + + SEMA = Wp + Wm + SEMI = Wp - Wm + ECC = SEMI / SEMA + PHA = (THETAm - THETAp) / 2.0 + INC = (THETAm + THETAp) / 2.0 + + return SEMA, ECC, INC, PHA + + def ep2ap(SEMA, ECC, INC, PHA): + """Convert tidal ellipse to real u and v amplitude and phase. + Adapted from ep2ap.m for matlab. + Original copyright notice: + %Authorship Copyright: + % + % The author of this program retains the copyright of this program, while + % you are welcome to use and distribute this program as long as you credit + % the author properly and respect the program name itself. Particularly, + % you are expected to retain the original author's name in this original + % version of the program or any of its modified version that you might make. + % You are also expected not to essentially change the name of the programs + % except for adding possible extension for your own version you might create, + % e.g. app2ep_xx is acceptable. Any suggestions are welcome and enjoy my + % program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + %Release Date: Nov. 2000 + + Args: + SEMA: semi-major axis + ECC: eccentricity + INC: inclination [radians] + PHA: phase [radians] + + Returns: + (u amplitude, u phase [radians], v amplitude, v phase [radians]) + + """ + Wp = (1 + ECC) / 2. * SEMA + Wm = (1 - ECC) / 2. * SEMA + THETAp = INC - PHA + THETAm = INC + PHA + + wp = Wp * np.exp(1j * THETAp) + wm = Wm * np.exp(1j * THETAm) + + cu = wp + np.conj(wm) + cv = -1j * (wp - np.conj(wm)) + + ua = np.abs(cu) + va = np.abs(cv) + up = -np.angle(cu) + vp = -np.angle(cv) + + return ua, va, up, vp + + def regrid_tides(self): + """ + The function regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. It accomplishes: + - Read in raw tidal data (all constituents) + - Perform minor transformations/conversions + - Regridded the tidal elevation, and tidal velocity + - Encoding the output + + Parameters (taken at initialization) + ------------------------------------- + infile_td : str + Raw Tidal File/Dir + tidal_constituents: list[str] + Specific tidal constiuents we are setting it up for + Returns + ------- + *.nc files + Regridded tidal velocity and elevation files in 'inputdir/forcing' + """ + + # Check if parameters are valid + if type(self.infile_td) != type("Sample String") or len(self.tidal_constituents) == 0 or type(self.tidal_constituents[0]) != type("Sample String"): + raise ValueError( + "The input parameters for tides were empty or not valid! (tidal_constituents, infile_td), try reinitializing Segment or recalling setup_tides with different parameters" + ) + if not os.path.exists(self.infile_td): + raise ValueError ( + "Tidal Files don't exist:" + self.infile_td + ) + + + # Read raw tidal data + tpxo_ds = xr.open_dataset(self.infile_td, engine="netcdf4") + + + return + + def encode_tidal_files_and_output(self, ds, filename): + """Like GFDL NWA25 changed to RM6 segment class, add metadata to tidal files, format, and output them. + + Parameters (taken at initialization) + ------------------------------------- + self.outfolder: str/path + The output folder to save the tidal files into + dataset : xarray.Dataset + The processed tidal dataset + filename: str + The output file name + Returns + ------- + *.nc files + Regridded [FILENAME] files in 'self.outfolder/[filename]_[num].nc' + + """ + for v in ds: + ds[v].encoding['_FillValue']= 1.0e20 + fname = f'{filename}_{self.segment_name}.nc' + # Set format and attributes for coordinates, including time if it does not already have calendar attribute + # (may change this to detect whether time is a time type or a float). + # Need to include the fillvalue or it will be back to nan + encoding = { + 'time': dict(_FillValue=1.0e20), + f'lon_{self.segstr}': dict(dtype='float64', _FillValue=1.0e20), + f'lat_{self.segstr}': dict(dtype='float64', _FillValue=1.0e20) + } + if 'calendar' not in ds['time'].attrs and 'modulo' not in ds['time'].attrs: + encoding.update({'time': dict(dtype='float64', calendar='gregorian', _FillValue=1.0e20)}) + ds.to_netcdf( + os.path.join(self.outfolder, fname), + engine='netcdf4', + encoding=encoding, + unlimited_dims='time' + ) + return + From 09a1a2fa04eb37e4e3f785024f0ebc86bfbb1396 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 17 Sep 2024 16:09:01 -0600 Subject: [PATCH 15/81] First Attempt: RM6 Tides --- regional_mom6/regional_mom6.py | 436 +++++++++++++++++++++++++-------- 1 file changed, 331 insertions(+), 105 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 8bd7612f..52e7b7c6 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -15,7 +15,7 @@ import importlib.resources import datetime from .utils import quadrilateral_areas - +import pandas as pd warnings.filterwarnings("ignore") @@ -1103,7 +1103,7 @@ def simple_boundary( return def setup_tides( - self, path_to_td, segment_number, arakawa_grid="A" + self, path_to_td,filename, horizontal_subset, arakawa_grid="A" ): """ Here, we subset our tidal data and generate more boundary files! @@ -1113,39 +1113,69 @@ def setup_tides( netCDF file containing only the boundary region and 3 extra boundary points on either side. Users can also provide a large dataset containing their entire domain but this will be slower. - segment_number (int): Number the segments according to how they'll be specified in - the ``MOM_input``. + filename(str): Name of the tpxo product that's used in the filename. Should be h_{filename}, u_{filename} arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. Either ``'A'`` (default), ``'B'``, or ``'C'``. + Returns: - *.nc files + *.nc files in inputdir/forcing Tidal input files for the boundaries from the TPXO dataaset """ - orientation = ["North", "South", "West","East"] - tidal_constituents = ["M1"] - for o in orientation: - for t in tidal_constituents: - - print("Processing tides at {} boundary...".format(orientation), end="") - if not path_to_td.exists(): - raise FileNotFoundError( - f"Boundary file not found at {path_to_td}. Please ensure that the files are named in the format `FILLIN`." - ) + + if not os.path.exists(path_to_td) or not os.path.exists(os.path.join(path_to_td,"h_"+filename)) or not os.path.exists(os.path.join(path_to_td,"u_"+filename)) : + raise ValueError ( + "Tidal Files don't exist at " + path_to_td+"/h[or]u_"+filename + ) + tidal_constituents = [0] + tpxo_h = ( + xr.open_dataset(os.path.join(path_to_td, f'h_{filename}')) + .rename({'lon_z': 'lon', 'lat_z': 'lat', 'nc': 'constituent'}) + .isel(constituent=tidal_constituents, **horizontal_subset) + ) + h = tpxo_h['ha'] * np.exp(-1j * np.radians(tpxo_h['hp'])) + tpxo_h['hRe'] = np.real(h) + tpxo_h['hIm'] = np.imag(h) + tpxo_u = ( + xr.open_dataset(os.path.join(path_to_td, f'u_{filename}')) + .rename({'lon_u': 'lon', 'lat_u': 'lat', 'nc': 'constituent'}) + .isel(constituent=tidal_constituents, **horizontal_subset) + ) + tpxo_u['ua'] *= 0.01 # convert to m/s + u = tpxo_u['ua'] * np.exp(-1j * np.radians(tpxo_u['up'])) + tpxo_u['uRe'] = np.real(u) + tpxo_u['uIm'] = np.imag(u) + tpxo_v = ( + xr.open_dataset(os.path.join(path_to_td, f'u_{filename}')) + .rename({'lon_v': 'lon', 'lat_v': 'lat', 'nc': 'constituent'}) + .isel(constituent=tidal_constituents, **horizontal_subset) + ) + tpxo_v['va'] *= 0.01 # convert to m/s + v = tpxo_v['va'] * np.exp(-1j * np.radians(tpxo_v['vp'])) + tpxo_v['vRe'] = np.real(v) + tpxo_v['vIm'] = np.imag(v) + times = xr.DataArray( + pd.date_range(self.date_range[0], periods=1), # Import pandas for this shouldn't be a big deal b/c it's already required in rm6 dependencies + dims=['time'] + ) + orientations = ["south", "north", "west", "east"] + for ind,o in enumerate(orientations): + print("Processing tides at {} boundary...".format(o), end="") seg = segment( hgrid=self.hgrid, - infile_td=path_to_td, # location of raw boundary + infile_bc = None, # location of raw boundary outfolder=self.mom_input_dir, varnames=None, - segment_name="segment_{:03d}".format(segment_number), - orientation=orientation, # orienataion + segment_name="segment_{:03d}".format(ind), + orientation=o, # orienataion startdate=self.date_range[0], arakawa_grid=arakawa_grid, repeat_year_forcing=self.repeat_year_forcing, + tidal_constituents=tidal_constituents ) - seg.regrid_tides() + seg.regrid_tides(filename, tpxo_v, tpxo_u, tpxo_h, times) print("Done.") - return + return def setup_bathymetry( self, *, @@ -1963,12 +1993,11 @@ def __init__( startdate, arakawa_grid="A", time_units="days", - infile_td = None, tidal_constituents=None, repeat_year_forcing=False, ): ## Store coordinate names - if arakawa_grid == "A": + if arakawa_grid == "A" and varnames is not None: self.x = varnames["x"] self.y = varnames["y"] @@ -1979,15 +2008,17 @@ def __init__( self.yh = varnames["yh"] ## Store velocity names - self.u = varnames["u"] - self.v = varnames["v"] - self.z = varnames["zl"] - self.eta = varnames["eta"] - self.time = varnames["time"] + if varnames is not None: + self.u = varnames["u"] + self.v = varnames["v"] + self.z = varnames["zl"] + self.eta = varnames["eta"] + self.time = varnames["time"] self.startdate = startdate ## Store tracer names - self.tracers = varnames["tracers"] + if varnames is not None: + self.tracers = varnames["tracers"] self.time_units = time_units ## Store other data @@ -2003,13 +2034,51 @@ def __init__( self.arakawa_grid = arakawa_grid self.infile_bc = infile_bc - self.infile_td = infile_td self.outfolder = outfolder self.hgrid = hgrid self.segment_name = segment_name self.tidal_constituents = tidal_constituents self.repeat_year_forcing = repeat_year_forcing + @property + def coords(self): + # Rename nxp and nyp to locations + if self.orientation == 'south': + rcoord = xr.Dataset({ + 'lon': self.hgrid['x'].isel(nyp=0), + 'lat': self.hgrid['y'].isel(nyp=0), + 'angle': self.hgrid['angle_dx'].isel(nyp=0) + }) + rcoord = rcoord.rename_dims({'nxp': 'locations'}) + elif self.orientation == 'north': + rcoord = xr.Dataset({ + 'lon': self.hgrid['x'].isel(nyp=-1), + 'lat': self.hgrid['y'].isel(nyp=-1), + 'angle': self.hgrid['angle_dx'].isel(nyp=-1) + }) + rcoord = rcoord.rename_dims({'nxp': 'locations'}) + elif self.orientation == 'west': + rcoord = xr.Dataset({ + 'lon': self.hgrid['x'].isel(nxp=0), + 'lat': self.hgrid['y'].isel(nxp=0), + 'angle': self.hgrid['angle_dx'].isel(nxp=0) + }) + rcoord = rcoord.rename_dims({'nyp': 'locations'}) + elif self.orientation == 'east': + rcoord = xr.Dataset({ + 'lon': self.hgrid['x'].isel(nxp=-1), + 'lat': self.hgrid['y'].isel(nxp=-1), + 'angle': self.hgrid['angle_dx'].isel(nxp=-1) + }) + rcoord = rcoord.rename_dims({'nyp': 'locations'}) + + # Make lat and lon coordinates + rcoord = rcoord.assign_coords( + lat=rcoord['lat'], + lon=rcoord['lon'] + ) + + return rcoord def rectangular_brushcut(self): """ Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary @@ -2327,7 +2396,238 @@ def rectangular_brushcut(self): return segment_out, encoding_dict - def ap2ep(uc, vc): + + def regrid_tides(self, filename, tpxo_v, tpxo_u, tpxo_h, times, + method='nearest_s2d', periodic=False): + """ + The function regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. It accomplishes: + - Read in raw tidal data (all constituents) + - Perform minor transformations/conversions + - Regridded the tidal elevation, and tidal velocity + - Encoding the output + + Parameters (taken at initialization) + ------------------------------------- + infile_td : str + Raw Tidal File/Dir + tidal_constituents: list[str] + Specific tidal constiuents we are setting it up for + filename: str + The specific product name used in the tpxo + Returns + ------- + *.nc files + Regridded tidal velocity and elevation files in 'inputdir/forcing' + """ + + # Check if parameters are valid + if len(self.tidal_constituents) == 0: + raise ValueError( + "The input parameters for tides were empty or not valid! (tidal_constituents, infile_td), try reinitializing Segment or recalling setup_tides with different parameters" + ) + + # Tidal Elevation: Horizontally interpolate elevation components + regrid = xe.Regridder( + tpxo_h[['lon', 'lat', 'hRe']], + self.coords, + method='nearest_s2d', + locstream_out=True, + periodic=False, + filename=os.path.join(self.outfolder,"forcing", f'regrid_{self.segment_name}_tidal_elev.nc'), + reuse_weights=False + ) + redest = regrid(tpxo_h[['lon', 'lat', 'hRe']]) + imdest = regrid(tpxo_h[['lon', 'lat', 'hIm']]) + + # Fill missing data. + # Need to do this first because complex would get converted to real + redest = redest.ffill(dim="locations", limit=None)['hRe'] + imdest = imdest.ffill(dim="locations", limit=None)['hIm'] + + # Convert complex + cplex = redest + 1j * imdest + + # Convert to real amplitude and phase. + ds_ap = xr.Dataset({ + f'zamp_{self.segment_name}': np.abs(cplex) + }) + # np.angle doesn't return dataarray + ds_ap[f'zphase_{self.segment_name}'] = (('constituent', 'locations'), -1 * np.angle(cplex)) # radians + + # Add time coordinate and transpose so that time is first, + # so that it can be the unlimited dimension + ds_ap, _ = xr.broadcast(ds_ap, times) + ds_ap = ds_ap.transpose('time', 'constituent', 'locations') + + ds_ap = self.expand_dims(ds_ap) + ds_ap = self.rename_dims(ds_ap) + + self.encode_tidal_files_and_output(ds_ap, 'tz') + + # Regrid Tidal Velocity: + regrid_u = xe.Regridder( + tpxo_u[['lon', 'lat', 'uRe']], + self.coords, + method=method, + locstream_out=True, + periodic=periodic, + reuse_weights=False + ) + + regrid_v = xe.Regridder( + tpxo_v[['lon', 'lat', 'vRe']], + self.coords, + method=method, + locstream_out=True, + periodic=periodic, + reuse_weights=False + ) + + # Interpolate each real and imaginary parts to segment. + uredest = regrid_u(tpxo_u[['lon', 'lat', 'uRe']])['uRe'] + uimdest = regrid_u(tpxo_u[['lon', 'lat', 'uIm']])['uIm'] + vredest = regrid_v(tpxo_v[['lon', 'lat', 'vRe']])['vRe'] + vimdest = regrid_v(tpxo_v[['lon', 'lat', 'vIm']])['vIm'] + + # Fill missing data. + # Need to do this first because complex would get converted to real + uredest = uredest.ffill(dim="locations", limit=None) + uimdest = uimdest.ffill(dim="locations", limit=None) + vredest = vredest.ffill(dim="locations", limit=None) + vimdest = vimdest.ffill(dim="locations", limit=None) + + # Convert to complex, remaining separate for u and v. + ucplex = uredest + 1j * uimdest + vcplex = vredest + 1j * vimdest + + # Convert complex u and v to ellipse, + # rotate ellipse from earth-relative to model-relative, + # and convert ellipse back to amplitude and phase. + # There is probably a complicated trig identity for this? But + # this works too. + # if self.border in ['south', 'north']: + # angle = self.coords['angle'].rename({'nxp': 'locations'}) + # elif self.border in ['west', 'east']: + # angle = self.coords['angle'].rename({'nyp': 'locations'}) + SEMA, ECC, INC, PHA = ap2ep(ucplex, vcplex) + + + + # Rotate to the model grid by adjusting the inclination. + # Requries that angle is in radians. + # INC is np array but angle is xr + # INC -= angle.data[np.newaxis, :] + ua, va, up, vp = ep2ap(SEMA, ECC, INC, PHA) + + ds_ap = xr.Dataset({ + f'uamp_{self.segment_name}': ua, + f'vamp_{self.segment_name}': va + }) + # up, vp aren't dataarrays + ds_ap[f'uphase_{self.segment_name}'] = (('constituent', 'locations'), up) # radians + ds_ap[f'vphase_{self.segment_name}'] = (('constituent', 'locations'), vp) # radians + + ds_ap, _ = xr.broadcast(ds_ap, times) + + # Need to transpose so that time is first, + # so that it can be the unlimited dimension + ds_ap = ds_ap.transpose('time', 'constituent', 'locations') + + # Some things may have become missing during the transformation + ds_ap = ds_ap.ffill(dim="locations", limit=None) + + ds_ap = self.expand_dims(ds_ap) + ds_ap = self.rename_dims(ds_ap) + + self.encode_tidal_files_and_output(ds_ap, 'tu') + + + return + + def encode_tidal_files_and_output(self, ds, filename): + """Like GFDL NWA25 changed to RM6 segment class, add metadata to tidal files, format, and output them. + + Parameters (taken at initialization) + ------------------------------------- + self.outfolder: str/path + The output folder to save the tidal files into + dataset : xarray.Dataset + The processed tidal dataset + filename: str + The output file name + Returns + ------- + *.nc files + Regridded [FILENAME] files in 'self.outfolder/[filename]_[num].nc' + + """ + for v in ds: + ds[v].encoding['_FillValue']= 1.0e20 + fname = f'{filename}_{self.segment_name}.nc' + # Set format and attributes for coordinates, including time if it does not already have calendar attribute + # (may change this to detect whether time is a time type or a float). + # Need to include the fillvalue or it will be back to nan + encoding = { + 'time': dict(_FillValue=1.0e20), + f'lon_{self.segment_name}': dict(dtype='float64', _FillValue=1.0e20), + f'lat_{self.segment_name}': dict(dtype='float64', _FillValue=1.0e20) + } + if 'calendar' not in ds['time'].attrs and 'modulo' not in ds['time'].attrs: + encoding.update({'time': dict(dtype='float64', calendar='gregorian', _FillValue=1.0e20)}) + ds.to_netcdf( + os.path.join(self.outfolder,"forcing", fname), + engine='netcdf4', + encoding=encoding, + unlimited_dims='time' + ) + return + + def expand_dims(self, ds): + """Add a length-1 dimension to the variables in a boundary dataset or array. + Named 'ny_segment_{self.segment_name}' if the border runs west to east (a south or north boundary), + or 'nx_segment_{self.segment_name}' if the border runs north to south (an east or west boundary). + + Args: + ds: boundary array with dimensions + + Returns: + modified array with new length-1 dimension. + """ + # having z or constituent as second dimension is optional, so offset determines where to place + # added dim + if 'z' in ds.coords or 'constituent' in ds.dims: + offset = 0 + else: + offset = 1 + if self.orientation in ['south', 'north']: + return ds.expand_dims(f'ny_{self.segment_name}', 2-offset) + elif self.orientation in ['west', 'east']: + return ds.expand_dims(f'nx_{self.segment_name}', 3-offset) + + def rename_dims(self, ds): + """Rename dimensions to be unique to the segment. + + Args: + ds (xarray.Dataset): Dataset that might contain 'lon', 'lat', 'z', and/or 'locations'. + + Returns: + xarray.Dataset: Dataset with dimensions renamed to include the segment identifier and to + match MOM6 expectations. + """ + ds = ds.rename({ + 'lon': f'lon_{self.segment_name}', + 'lat': f'lat_{self.segment_name}' + }) + if 'z' in ds.coords: + ds = ds.rename({ + 'z': f'nz_{self.segment_name}' + }) + if self.orientation in ['south', 'north']: + return ds.rename({'locations': f'nx_{self.segment_name}'}) + elif self.orientation in ['west', 'east']: + return ds.rename({'locations': f'ny_{self.segment_name}'}) + +def ap2ep(uc, vc): """Convert complex tidal u and v to tidal ellipse. Adapted from ap2ep.m for matlab Original copyright notice: @@ -2382,7 +2682,7 @@ def ap2ep(uc, vc): return SEMA, ECC, INC, PHA - def ep2ap(SEMA, ECC, INC, PHA): +def ep2ap(SEMA, ECC, INC, PHA): """Convert tidal ellipse to real u and v amplitude and phase. Adapted from ep2ap.m for matlab. Original copyright notice: @@ -2442,78 +2742,4 @@ def ep2ap(SEMA, ECC, INC, PHA): return ua, va, up, vp - def regrid_tides(self): - """ - The function regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. It accomplishes: - - Read in raw tidal data (all constituents) - - Perform minor transformations/conversions - - Regridded the tidal elevation, and tidal velocity - - Encoding the output - - Parameters (taken at initialization) - ------------------------------------- - infile_td : str - Raw Tidal File/Dir - tidal_constituents: list[str] - Specific tidal constiuents we are setting it up for - Returns - ------- - *.nc files - Regridded tidal velocity and elevation files in 'inputdir/forcing' - """ - - # Check if parameters are valid - if type(self.infile_td) != type("Sample String") or len(self.tidal_constituents) == 0 or type(self.tidal_constituents[0]) != type("Sample String"): - raise ValueError( - "The input parameters for tides were empty or not valid! (tidal_constituents, infile_td), try reinitializing Segment or recalling setup_tides with different parameters" - ) - if not os.path.exists(self.infile_td): - raise ValueError ( - "Tidal Files don't exist:" + self.infile_td - ) - - - # Read raw tidal data - tpxo_ds = xr.open_dataset(self.infile_td, engine="netcdf4") - - - return - - def encode_tidal_files_and_output(self, ds, filename): - """Like GFDL NWA25 changed to RM6 segment class, add metadata to tidal files, format, and output them. - - Parameters (taken at initialization) - ------------------------------------- - self.outfolder: str/path - The output folder to save the tidal files into - dataset : xarray.Dataset - The processed tidal dataset - filename: str - The output file name - Returns - ------- - *.nc files - Regridded [FILENAME] files in 'self.outfolder/[filename]_[num].nc' - - """ - for v in ds: - ds[v].encoding['_FillValue']= 1.0e20 - fname = f'{filename}_{self.segment_name}.nc' - # Set format and attributes for coordinates, including time if it does not already have calendar attribute - # (may change this to detect whether time is a time type or a float). - # Need to include the fillvalue or it will be back to nan - encoding = { - 'time': dict(_FillValue=1.0e20), - f'lon_{self.segstr}': dict(dtype='float64', _FillValue=1.0e20), - f'lat_{self.segstr}': dict(dtype='float64', _FillValue=1.0e20) - } - if 'calendar' not in ds['time'].attrs and 'modulo' not in ds['time'].attrs: - encoding.update({'time': dict(dtype='float64', calendar='gregorian', _FillValue=1.0e20)}) - ds.to_netcdf( - os.path.join(self.outfolder, fname), - engine='netcdf4', - encoding=encoding, - unlimited_dims='time' - ) - return From d73e84a82529a1790ce009b3822b693143f58866 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Wed, 18 Sep 2024 09:53:42 -0600 Subject: [PATCH 16/81] Clean up setup_tides and adjust rect_boundaries with hard-coded segment num After talking with Ashley, the find_MOM6_orientation will likely be removed --- regional_mom6/regional_mom6.py | 254 ++++++++++----------------------- regional_mom6/utils.py | 117 +++++++++++++++ 2 files changed, 194 insertions(+), 177 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 52e7b7c6..25aac797 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,7 +14,7 @@ import os import importlib.resources import datetime -from .utils import quadrilateral_areas +from .utils import quadrilateral_areas, ap2ep, ep2ap import pandas as pd warnings.filterwarnings("ignore") @@ -144,6 +144,30 @@ def longitude_slicer(data, longitude_extent, longitude_coords): return data +def find_MOM6_orientation(input): + """ + Convert between MOM6 boundary and the specific segment number needed, or the inverse + """ + direction_dir = { + "south": 1, + "north": 2, + "west": 3, + "east": 4, + } + direction_dir_inv = {v: k for k, v in direction_dir.items()} + + if type(input) == str: + try: + return direction_dir[input] + except: + raise ValueError("Invalid Input. Did you spell the direction wrong, it should be lowercase?") + elif type(input) == int: + try: + return direction_dir_inv[input] + except: + raise ValueError("Invalid Input. Did you pick a number 1 through 4?") + else: + raise ValueError("Invalid type of Input, can only be string or int.") from pathlib import Path @@ -513,6 +537,7 @@ def __init__( input_rundir = self.mom_input_dir / "rundir" if not input_rundir.exists(): input_rundir.symlink_to(self.mom_run_dir.resolve()) + self.segments = {} # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) def __getattr__(self, name): available_methods = [ @@ -1050,12 +1075,12 @@ def rectangular_boundaries( "This method only supports up to four boundaries. To set up more complex boundary shapes you can manually call the 'simple_boundary' method for each boundary." ) # Now iterate through our four boundaries - for i, orientation in enumerate(boundaries, start=1): + for orientation in boundaries: self.simple_boundary( Path(raw_boundaries_path) / (orientation + "_unprocessed.nc"), varnames, orientation, # The cardinal direction of the boundary - i, # A number to identify the boundary; indexes from 1 + find_MOM6_orientation(orientation), # A number to identify the boundary; indexes from 1 arakawa_grid=arakawa_grid, ) @@ -1099,11 +1124,14 @@ def simple_boundary( ) seg.rectangular_brushcut() + + # Save Segment to Experiment + self.segments[orientation] = seg print("Done.") return def setup_tides( - self, path_to_td,filename, horizontal_subset, arakawa_grid="A" + self, path_to_td,tidal_filename, horizontal_subset, ): """ Here, we subset our tidal data and generate more boundary files! @@ -1113,22 +1141,20 @@ def setup_tides( netCDF file containing only the boundary region and 3 extra boundary points on either side. Users can also provide a large dataset containing their entire domain but this will be slower. - filename(str): Name of the tpxo product that's used in the filename. Should be h_{filename}, u_{filename} - arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. - Either ``'A'`` (default), ``'B'``, or ``'C'``. + tidal_filename(str): Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} Returns: *.nc files in inputdir/forcing Tidal input files for the boundaries from the TPXO dataaset """ - if not os.path.exists(path_to_td) or not os.path.exists(os.path.join(path_to_td,"h_"+filename)) or not os.path.exists(os.path.join(path_to_td,"u_"+filename)) : + if not os.path.exists(path_to_td) or not os.path.exists(os.path.join(path_to_td,"h_"+tidal_filename)) or not os.path.exists(os.path.join(path_to_td,"u_"+tidal_filename)) : raise ValueError ( - "Tidal Files don't exist at " + path_to_td+"/h[or]u_"+filename + "Tidal Files don't exist at " + path_to_td+"/[h.or.u]_"+tidal_filename+".nc" ) tidal_constituents = [0] tpxo_h = ( - xr.open_dataset(os.path.join(path_to_td, f'h_{filename}')) + xr.open_dataset(os.path.join(path_to_td, f'h_{tidal_filename}')) .rename({'lon_z': 'lon', 'lat_z': 'lat', 'nc': 'constituent'}) .isel(constituent=tidal_constituents, **horizontal_subset) ) @@ -1136,7 +1162,7 @@ def setup_tides( tpxo_h['hRe'] = np.real(h) tpxo_h['hIm'] = np.imag(h) tpxo_u = ( - xr.open_dataset(os.path.join(path_to_td, f'u_{filename}')) + xr.open_dataset(os.path.join(path_to_td, f'u_{tidal_filename}')) .rename({'lon_u': 'lon', 'lat_u': 'lat', 'nc': 'constituent'}) .isel(constituent=tidal_constituents, **horizontal_subset) ) @@ -1145,7 +1171,7 @@ def setup_tides( tpxo_u['uRe'] = np.real(u) tpxo_u['uIm'] = np.imag(u) tpxo_v = ( - xr.open_dataset(os.path.join(path_to_td, f'u_{filename}')) + xr.open_dataset(os.path.join(path_to_td, f'u_{tidal_filename}')) .rename({'lon_v': 'lon', 'lat_v': 'lat', 'nc': 'constituent'}) .isel(constituent=tidal_constituents, **horizontal_subset) ) @@ -1157,25 +1183,27 @@ def setup_tides( pd.date_range(self.date_range[0], periods=1), # Import pandas for this shouldn't be a big deal b/c it's already required in rm6 dependencies dims=['time'] ) - orientations = ["south", "north", "west", "east"] - for ind,o in enumerate(orientations): - print("Processing tides at {} boundary...".format(o), end="") + boundaries = ["south", "north", "west", "east"] + for b in enumerate(boundaries): + if b not in self.segments: seg = segment( - hgrid=self.hgrid, - infile_bc = None, # location of raw boundary - outfolder=self.mom_input_dir, - varnames=None, - segment_name="segment_{:03d}".format(ind), - orientation=o, # orienataion - startdate=self.date_range[0], - arakawa_grid=arakawa_grid, - repeat_year_forcing=self.repeat_year_forcing, - tidal_constituents=tidal_constituents + hgrid=self.hgrid, + infile=None, # location of raw boundary + outfolder=self.mom_input_dir, + varnames=None, + segment_name="segment_{:03d}".format(find_MOM6_orientation(b)), + orientation=b, # orienataion + startdate=self.date_range[0], + arakawa_grid=None, + repeat_year_forcing=self.repeat_year_forcing, ) + else: + seg = self.segments[b] + seg.regrid_tides(tpxo_v, tpxo_u,tpxo_h, times) - seg.regrid_tides(filename, tpxo_v, tpxo_u, tpxo_h, times) - print("Done.") - return + + + def setup_bathymetry( self, *, @@ -1993,11 +2021,10 @@ def __init__( startdate, arakawa_grid="A", time_units="days", - tidal_constituents=None, repeat_year_forcing=False, ): ## Store coordinate names - if arakawa_grid == "A" and varnames is not None: + if arakawa_grid == "A" and infile_bc is not None: self.x = varnames["x"] self.y = varnames["y"] @@ -2008,7 +2035,7 @@ def __init__( self.yh = varnames["yh"] ## Store velocity names - if varnames is not None: + if infile_bc is not None: self.u = varnames["u"] self.v = varnames["v"] self.z = varnames["zl"] @@ -2017,7 +2044,7 @@ def __init__( self.startdate = startdate ## Store tracer names - if varnames is not None: + if infile_bc is not None: self.tracers = varnames["tracers"] self.time_units = time_units @@ -2037,11 +2064,13 @@ def __init__( self.outfolder = outfolder self.hgrid = hgrid self.segment_name = segment_name - self.tidal_constituents = tidal_constituents self.repeat_year_forcing = repeat_year_forcing @property def coords(self): + """ + This function allows us to call the self.coords for use in the xesmf.Regridder in the regrid_tides function. + """ # Rename nxp and nyp to locations if self.orientation == 'south': rcoord = xr.Dataset({ @@ -2135,7 +2164,7 @@ def rectangular_brushcut(self): "bilinear", locstream_out=True, reuse_weights=False, - filename=self.outfolder + tidal_filename=self.outfolder / f"weights/bilinear_velocity_weights_{self.orientation}.nc", ) @@ -2397,7 +2426,7 @@ def rectangular_brushcut(self): return segment_out, encoding_dict - def regrid_tides(self, filename, tpxo_v, tpxo_u, tpxo_h, times, + def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, method='nearest_s2d', periodic=False): """ The function regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. It accomplishes: @@ -2410,23 +2439,17 @@ def regrid_tides(self, filename, tpxo_v, tpxo_u, tpxo_h, times, ------------------------------------- infile_td : str Raw Tidal File/Dir - tidal_constituents: list[str] - Specific tidal constiuents we are setting it up for - filename: str - The specific product name used in the tpxo + tpxo_v, tpxo_u, tpxo_h: xr.Datasets + Specific adjusted for MOM6 tpxo datasets (Adjusted with setup_tides) + times: list[pd datetime] + The start date of our model period Returns ------- *.nc files Regridded tidal velocity and elevation files in 'inputdir/forcing' """ - # Check if parameters are valid - if len(self.tidal_constituents) == 0: - raise ValueError( - "The input parameters for tides were empty or not valid! (tidal_constituents, infile_td), try reinitializing Segment or recalling setup_tides with different parameters" - ) - - # Tidal Elevation: Horizontally interpolate elevation components + ########## Tidal Elevation: Horizontally interpolate elevation components ############ regrid = xe.Regridder( tpxo_h[['lon', 'lat', 'hRe']], self.coords, @@ -2459,12 +2482,12 @@ def regrid_tides(self, filename, tpxo_v, tpxo_u, tpxo_h, times, ds_ap, _ = xr.broadcast(ds_ap, times) ds_ap = ds_ap.transpose('time', 'constituent', 'locations') - ds_ap = self.expand_dims(ds_ap) - ds_ap = self.rename_dims(ds_ap) + ds_ap = self.expand_tidal_dims(ds_ap) + ds_ap = self.rename_tidal_dims(ds_ap) self.encode_tidal_files_and_output(ds_ap, 'tz') - # Regrid Tidal Velocity: + ########### Regrid Tidal Velocity ###################### regrid_u = xe.Regridder( tpxo_u[['lon', 'lat', 'uRe']], self.coords, @@ -2503,20 +2526,13 @@ def regrid_tides(self, filename, tpxo_v, tpxo_u, tpxo_h, times, # Convert complex u and v to ellipse, # rotate ellipse from earth-relative to model-relative, # and convert ellipse back to amplitude and phase. - # There is probably a complicated trig identity for this? But - # this works too. - # if self.border in ['south', 'north']: - # angle = self.coords['angle'].rename({'nxp': 'locations'}) - # elif self.border in ['west', 'east']: - # angle = self.coords['angle'].rename({'nyp': 'locations'}) SEMA, ECC, INC, PHA = ap2ep(ucplex, vcplex) # Rotate to the model grid by adjusting the inclination. # Requries that angle is in radians. - # INC is np array but angle is xr - # INC -= angle.data[np.newaxis, :] + ua, va, up, vp = ep2ap(SEMA, ECC, INC, PHA) ds_ap = xr.Dataset({ @@ -2536,8 +2552,8 @@ def regrid_tides(self, filename, tpxo_v, tpxo_u, tpxo_h, times, # Some things may have become missing during the transformation ds_ap = ds_ap.ffill(dim="locations", limit=None) - ds_ap = self.expand_dims(ds_ap) - ds_ap = self.rename_dims(ds_ap) + ds_ap = self.expand_tidal_dims(ds_ap) + ds_ap = self.rename_tidal_dims(ds_ap) self.encode_tidal_files_and_output(ds_ap, 'tu') @@ -2582,7 +2598,7 @@ def encode_tidal_files_and_output(self, ds, filename): ) return - def expand_dims(self, ds): + def expand_tidal_dims(self, ds): """Add a length-1 dimension to the variables in a boundary dataset or array. Named 'ny_segment_{self.segment_name}' if the border runs west to east (a south or north boundary), or 'nx_segment_{self.segment_name}' if the border runs north to south (an east or west boundary). @@ -2604,7 +2620,7 @@ def expand_dims(self, ds): elif self.orientation in ['west', 'east']: return ds.expand_dims(f'nx_{self.segment_name}', 3-offset) - def rename_dims(self, ds): + def rename_tidal_dims(self, ds): """Rename dimensions to be unique to the segment. Args: @@ -2627,119 +2643,3 @@ def rename_dims(self, ds): elif self.orientation in ['west', 'east']: return ds.rename({'locations': f'ny_{self.segment_name}'}) -def ap2ep(uc, vc): - """Convert complex tidal u and v to tidal ellipse. - Adapted from ap2ep.m for matlab - Original copyright notice: - %Authorship Copyright: - % - % The author retains the copyright of this program, while you are welcome - % to use and distribute it as long as you credit the author properly and respect - % the program name itself. Particularly, you are expected to retain the original - % author's name in this original version or any of its modified version that - % you might make. You are also expected not to essentially change the name of - % the programs except for adding possible extension for your own version you - % might create, e.g. ap2ep_xx is acceptable. Any suggestions are welcome and - % enjoy my program(s)! - % - % - %Author Info: - %_______________________________________________________________________ - % Zhigang Xu, Ph.D. - % (pronounced as Tsi Gahng Hsu) - % Research Scientist - % Coastal Circulation - % Bedford Institute of Oceanography - % 1 Challenge Dr. - % P.O. Box 1006 Phone (902) 426-2307 (o) - % Dartmouth, Nova Scotia Fax (902) 426-7827 - % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca - %_______________________________________________________________________ - % - % Release Date: Nov. 2000, Revised on May. 2002 to adopt Foreman's northern semi - % major axis convention. - - Args: - uc: complex tidal u velocity - vc: complex tidal v velocity - - Returns: - (semi-major axis, eccentricity, inclination [radians], phase [radians]) - """ - wp = (uc + 1j * vc) / 2.0 - wm = np.conj(uc - 1j * vc) / 2.0 - - Wp = np.abs(wp) - Wm = np.abs(wm) - THETAp = np.angle(wp) - THETAm = np.angle(wm) - - SEMA = Wp + Wm - SEMI = Wp - Wm - ECC = SEMI / SEMA - PHA = (THETAm - THETAp) / 2.0 - INC = (THETAm + THETAp) / 2.0 - - return SEMA, ECC, INC, PHA - -def ep2ap(SEMA, ECC, INC, PHA): - """Convert tidal ellipse to real u and v amplitude and phase. - Adapted from ep2ap.m for matlab. - Original copyright notice: - %Authorship Copyright: - % - % The author of this program retains the copyright of this program, while - % you are welcome to use and distribute this program as long as you credit - % the author properly and respect the program name itself. Particularly, - % you are expected to retain the original author's name in this original - % version of the program or any of its modified version that you might make. - % You are also expected not to essentially change the name of the programs - % except for adding possible extension for your own version you might create, - % e.g. app2ep_xx is acceptable. Any suggestions are welcome and enjoy my - % program(s)! - % - % - %Author Info: - %_______________________________________________________________________ - % Zhigang Xu, Ph.D. - % (pronounced as Tsi Gahng Hsu) - % Research Scientist - % Coastal Circulation - % Bedford Institute of Oceanography - % 1 Challenge Dr. - % P.O. Box 1006 Phone (902) 426-2307 (o) - % Dartmouth, Nova Scotia Fax (902) 426-7827 - % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca - %_______________________________________________________________________ - % - %Release Date: Nov. 2000 - - Args: - SEMA: semi-major axis - ECC: eccentricity - INC: inclination [radians] - PHA: phase [radians] - - Returns: - (u amplitude, u phase [radians], v amplitude, v phase [radians]) - - """ - Wp = (1 + ECC) / 2. * SEMA - Wm = (1 - ECC) / 2. * SEMA - THETAp = INC - PHA - THETAm = INC + PHA - - wp = Wp * np.exp(1j * THETAp) - wm = Wm * np.exp(1j * THETAm) - - cu = wp + np.conj(wm) - cv = -1j * (wp - np.conj(wm)) - - ua = np.abs(cu) - va = np.abs(cv) - up = -np.angle(cu) - vp = -np.angle(cv) - - return ua, va, up, vp - - diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index fb0ce865..89d07a84 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -177,3 +177,120 @@ def quadrilateral_areas(lat, lon, R=1): return quadrilateral_area( coords[:-1, :-1, :], coords[:-1, 1:, :], coords[1:, 1:, :], coords[1:, :-1, :] ) + +def ap2ep(uc, vc): + """Convert complex tidal u and v to tidal ellipse. + Adapted from ap2ep.m for matlab + Original copyright notice: + %Authorship Copyright: + % + % The author retains the copyright of this program, while you are welcome + % to use and distribute it as long as you credit the author properly and respect + % the program name itself. Particularly, you are expected to retain the original + % author's name in this original version or any of its modified version that + % you might make. You are also expected not to essentially change the name of + % the programs except for adding possible extension for your own version you + % might create, e.g. ap2ep_xx is acceptable. Any suggestions are welcome and + % enjoy my program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + % Release Date: Nov. 2000, Revised on May. 2002 to adopt Foreman's northern semi + % major axis convention. + + Args: + uc: complex tidal u velocity + vc: complex tidal v velocity + + Returns: + (semi-major axis, eccentricity, inclination [radians], phase [radians]) + """ + wp = (uc + 1j * vc) / 2.0 + wm = np.conj(uc - 1j * vc) / 2.0 + + Wp = np.abs(wp) + Wm = np.abs(wm) + THETAp = np.angle(wp) + THETAm = np.angle(wm) + + SEMA = Wp + Wm + SEMI = Wp - Wm + ECC = SEMI / SEMA + PHA = (THETAm - THETAp) / 2.0 + INC = (THETAm + THETAp) / 2.0 + + return SEMA, ECC, INC, PHA + +def ep2ap(SEMA, ECC, INC, PHA): + """Convert tidal ellipse to real u and v amplitude and phase. + Adapted from ep2ap.m for matlab. + Original copyright notice: + %Authorship Copyright: + % + % The author of this program retains the copyright of this program, while + % you are welcome to use and distribute this program as long as you credit + % the author properly and respect the program name itself. Particularly, + % you are expected to retain the original author's name in this original + % version of the program or any of its modified version that you might make. + % You are also expected not to essentially change the name of the programs + % except for adding possible extension for your own version you might create, + % e.g. app2ep_xx is acceptable. Any suggestions are welcome and enjoy my + % program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + %Release Date: Nov. 2000 + + Args: + SEMA: semi-major axis + ECC: eccentricity + INC: inclination [radians] + PHA: phase [radians] + + Returns: + (u amplitude, u phase [radians], v amplitude, v phase [radians]) + + """ + Wp = (1 + ECC) / 2. * SEMA + Wm = (1 - ECC) / 2. * SEMA + THETAp = INC - PHA + THETAm = INC + PHA + + wp = Wp * np.exp(1j * THETAp) + wm = Wm * np.exp(1j * THETAm) + + cu = wp + np.conj(wm) + cv = -1j * (wp - np.conj(wm)) + + ua = np.abs(cu) + va = np.abs(cv) + up = -np.angle(cu) + vp = -np.angle(cv) + + return ua, va, up, vp + + From 86a349acbcfe0642095f14e86a0ac0d4818f7b71 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Wed, 18 Sep 2024 14:59:41 -0600 Subject: [PATCH 17/81] SOFT BREAK: Change Function Names, Add Rough Horiz Subset (See Below) The function names for rectangular and sinmple boundaries were changed because the tides are a kind of boundary function, the old names now give a warning and call the correct function. GFDL had rough horizontal subsetting for the tpxo dataset (probably for efficiency?), implemented in setup_tides_rectangular_boundaries. --- regional_mom6/regional_mom6.py | 81 +++++++++++++++++++++++----------- regional_mom6/utils.py | 8 +++- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 25aac797..cdcb48ad 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,7 +14,7 @@ import os import importlib.resources import datetime -from .utils import quadrilateral_areas, ap2ep, ep2ap +from .utils import quadrilateral_areas, ap2ep, ep2ap,find_roughly_nearest_ny_nx import pandas as pd warnings.filterwarnings("ignore") @@ -527,6 +527,9 @@ def __init__( else: self.hgrid = self._make_hgrid() self.vgrid = self._make_vgrid() + + self.segments = {} # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) + # create additional directories and links (self.mom_input_dir / "weights").mkdir(exist_ok=True) (self.mom_input_dir / "forcing").mkdir(exist_ok=True) @@ -537,7 +540,6 @@ def __init__( input_rundir = self.mom_input_dir / "rundir" if not input_rundir.exists(): input_rundir.symlink_to(self.mom_run_dir.resolve()) - self.segments = {} # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) def __getattr__(self, name): available_methods = [ @@ -1044,6 +1046,18 @@ def rectangular_boundaries( varnames, boundaries=["south", "north", "west", "east"], arakawa_grid="A", + ): + warnings.filterwarnings("default") # Set warnings back to on + warnings.warn("rectangular_boundaries is changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_rectangular_boundaries\"") + warnings.filterwarnings("ignore") # Set warnings back off + return self.setup_ocean_state_rectangular_boundaries( raw_boundaries_path,varnames, boundaries=boundaries,arakawa_grid=arakawa_grid) + + def setup_ocean_state_rectangular_boundaries( + self, + raw_boundaries_path, + varnames, + boundaries=["south", "north", "west", "east"], + arakawa_grid="A", ): """ This function is a wrapper for `simple_boundary`. Given a list of up to four cardinal directions, @@ -1076,7 +1090,7 @@ def rectangular_boundaries( ) # Now iterate through our four boundaries for orientation in boundaries: - self.simple_boundary( + self.setup_ocean_state_simple_boundary( Path(raw_boundaries_path) / (orientation + "_unprocessed.nc"), varnames, orientation, # The cardinal direction of the boundary @@ -1086,6 +1100,14 @@ def rectangular_boundaries( def simple_boundary( self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" + ): + warnings.filterwarnings("default") # Set warnings back to on + warnings.warn("simple_boundary is changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_simple_boundary\"") + warnings.filterwarnings("ignore") # Turn warnings off + return self.setup_ocean_state_simple_boundary( path_to_bc, varnames, orientation, segment_number, arakawa_grid="A") + + def setup_ocean_state_simple_boundary( + self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" ): """ Here 'simple' refers to boundaries that are parallel to lines of constant longitude or latitude. @@ -1130,8 +1152,8 @@ def simple_boundary( print("Done.") return - def setup_tides( - self, path_to_td,tidal_filename, horizontal_subset, + def setup_tides_rectangle_boundaries( + self, path_to_td,tidal_filename, ): """ Here, we subset our tidal data and generate more boundary files! @@ -1152,12 +1174,21 @@ def setup_tides( raise ValueError ( "Tidal Files don't exist at " + path_to_td+"/[h.or.u]_"+tidal_filename+".nc" ) + + ### Find Rough Horizontal Subset (with 0.5 Buffer)### tidal_constituents = [0] tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f'h_{tidal_filename}')) .rename({'lon_z': 'lon', 'lat_z': 'lat', 'nc': 'constituent'}) - .isel(constituent=tidal_constituents, **horizontal_subset) + .isel(constituent=tidal_constituents) ) + ny0,nx0 = find_roughly_nearest_ny_nx(self.latitude_extent[0]-0.5,self.longitude_extent[0]-0.5,tpxo_h ) + ny1,nx1 = find_roughly_nearest_ny_nx(self.latitude_extent[1]+0.5,self.longitude_extent[1]+0.5,tpxo_h) + horizontal_subset = dict(ny=slice(ny0,ny1), nx=slice(nx0,nx1)) + + tpxo_h = tpxo_h.isel( **horizontal_subset) + + h = tpxo_h['ha'] * np.exp(-1j * np.radians(tpxo_h['hp'])) tpxo_h['hRe'] = np.real(h) tpxo_h['hIm'] = np.imag(h) @@ -1184,7 +1215,12 @@ def setup_tides( dims=['time'] ) boundaries = ["south", "north", "west", "east"] - for b in enumerate(boundaries): + + # Initialize or find boundary segment + for b in boundaries: + print("Processing {} boundary...".format(b), end="") + + # If the GLORYS ocean_state has already created segments, we don't create them again. if b not in self.segments: seg = segment( hgrid=self.hgrid, @@ -1194,16 +1230,15 @@ def setup_tides( segment_name="segment_{:03d}".format(find_MOM6_orientation(b)), orientation=b, # orienataion startdate=self.date_range[0], - arakawa_grid=None, repeat_year_forcing=self.repeat_year_forcing, ) else: seg = self.segments[b] - seg.regrid_tides(tpxo_v, tpxo_u,tpxo_h, times) - + # Output and regrid tides + seg.regrid_tides(tpxo_v, tpxo_u,tpxo_h, times) + print("Done") - def setup_bathymetry( self, *, @@ -1984,8 +2019,7 @@ class segment: Args: hgrid (xarray.Dataset): The horizontal grid used for domain. - infile_bc (Union[str, Path]): Path to the raw, unprocessed boundary segment. - infile_td (Union[str, Path]): Path to the raw, unprocessed tidal segment. + infile (Union[str, Path]): Path to the raw, unprocessed boundary segment. outfolder (Union[str, Path]): Path to folder where the model inputs will be stored. varnames (Dict[str, str]): Mapping between the variable/dimension names and @@ -2000,11 +2034,6 @@ class segment: Either ``'A'`` (default), ``'B'``, or ``'C'``. time_units (str): The units used by the raw forcing files, e.g., ``hours``, ``days`` (default). - tidal_constituents (Optional[int]): An integer determining the number of tidal - constituents to be included from the list: *M*:sub:`2`, *S*:sub:`2`, *N*:sub:`2`, - *K*:sub:`2`, *K*:sub:`1`, *O*:sub:`2`, *P*:sub:`1`, *Q*:sub:`1`, *Mm*, - *Mf*, and *M*:sub:`4`. For example, specifying ``1`` only includes *M*:sub:`2`; - specifying ``2`` includes *M*:sub:`2` and *S*:sub:`2`, etc. Default: ``None``. repeat_year_forcing (Optional[bool]): When ``True`` the experiment runs with repeat-year forcing. When ``False`` (default) then inter-annual forcing is used. """ @@ -2013,7 +2042,7 @@ def __init__( self, *, hgrid, - infile_bc, + infile, outfolder, varnames, segment_name, @@ -2024,7 +2053,7 @@ def __init__( repeat_year_forcing=False, ): ## Store coordinate names - if arakawa_grid == "A" and infile_bc is not None: + if arakawa_grid == "A" and infile is not None: self.x = varnames["x"] self.y = varnames["y"] @@ -2035,7 +2064,7 @@ def __init__( self.yh = varnames["yh"] ## Store velocity names - if infile_bc is not None: + if infile is not None: self.u = varnames["u"] self.v = varnames["v"] self.z = varnames["zl"] @@ -2044,7 +2073,7 @@ def __init__( self.startdate = startdate ## Store tracer names - if infile_bc is not None: + if infile is not None: self.tracers = varnames["tracers"] self.time_units = time_units @@ -2060,7 +2089,7 @@ def __init__( raise ValueError("arakawa_grid must be one of: 'A', 'B', or 'C'") self.arakawa_grid = arakawa_grid - self.infile_bc = infile_bc + self.infile = infile self.outfolder = outfolder self.hgrid = hgrid self.segment_name = segment_name @@ -2153,7 +2182,7 @@ def rectangular_brushcut(self): } ).set_coords(["lat", "lon"]) - rawseg = xr.open_dataset(self.infile_bc, decode_times=False, engine="netcdf4") + rawseg = xr.open_dataset(self.infile, decode_times=False, engine="netcdf4") if self.arakawa_grid == "A": rawseg = rawseg.rename({self.x: "lon", self.y: "lat"}) @@ -2164,7 +2193,7 @@ def rectangular_brushcut(self): "bilinear", locstream_out=True, reuse_weights=False, - tidal_filename=self.outfolder + filename=self.outfolder / f"weights/bilinear_velocity_weights_{self.orientation}.nc", ) @@ -2609,7 +2638,7 @@ def expand_tidal_dims(self, ds): Returns: modified array with new length-1 dimension. """ - # having z or constituent as second dimension is optional, so offset determines where to place + # having z or constituent as second dimension is optional, so offset determines where to place # added dim if 'z' in ds.coords or 'constituent' in ds.dims: offset = 0 diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index 89d07a84..2409545b 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -293,4 +293,10 @@ def ep2ap(SEMA, ECC, INC, PHA): return ua, va, up, vp - +def find_roughly_nearest_ny_nx(lat, lon, ds): + """ + Accepts a lat lon and returns a ROUGH closest ny,nx. in ds + """ + ny = (np.abs(ds.lat.values - lat)).argmin(axis=1)[0] # We're looking for an nx, I know it's not exact, but this works + nx = (np.abs(ds.lon.values - lon)).argmin(axis=0)[0] + return ny,nx From 5a40076e687601418f31be78d42fcf91189bdc3d Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 19 Sep 2024 09:11:48 -0600 Subject: [PATCH 18/81] Write MOM6 Vars --- regional_mom6/regional_mom6.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index cdcb48ad..dd151dd4 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1733,6 +1733,8 @@ def setup_run_directory( f"Cannot find the premade run directory files at {premade_rundir_path} either.\n\n" + "There may be an issue with package installation. Check that the `premade_run_directory` folder is present in one of these two locations" ) + else: + print("Found Premade Run Directories!") # Define the locations of the directories we'll copy files across from. Base contains most of the files, and overwrite replaces files in the base directory. base_run_dir = premade_rundir_path / "common_files" @@ -1897,6 +1899,21 @@ def setup_run_directory( ] nml.write(self.mom_run_dir / "input.nml", force=True) + + def write_MOM_input(self, variable_dict): + """ + Write MOM Input based on specific file variable format i.e. Var = Value""" + # Overwrite values pertaining to vertical structure in the MOM_input file + with open(self.mom_run_dir / "MOM_input", "r") as file: + lines = file.readlines() + for jj in range(len(lines)): + for var in variable_dict.keys(): + if "{} = ".format(var) in lines[jj]: + lines[jj] = f'{var} = {variable_dict[var]}\n' + with open(self.mom_run_dir / "MOM_input", "w") as f: + f.writelines(lines) + + def setup_era5(self, era5_path): """ Setup the ERA5 forcing files for the experiment. This assumes that @@ -2137,6 +2154,7 @@ def coords(self): ) return rcoord + def rectangular_brushcut(self): """ Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary @@ -2453,7 +2471,6 @@ def rectangular_brushcut(self): ) return segment_out, encoding_dict - def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, method='nearest_s2d', periodic=False): From 8adb479e52393fa39649db17a5af893906ec4caa Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 19 Sep 2024 10:24:22 -0600 Subject: [PATCH 19/81] Add Docstring Cite and Merge Functions Collapsed the *_tidal_dims functions into the encode_tides function in segment, and add citing documentation to the docstrings for now. --- regional_mom6/regional_mom6.py | 214 +++++++++++++++++++-------------- 1 file changed, 127 insertions(+), 87 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index dd151dd4..bff9a30b 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1156,18 +1156,29 @@ def setup_tides_rectangle_boundaries( self, path_to_td,tidal_filename, ): """ - Here, we subset our tidal data and generate more boundary files! + This function: + We subset our tidal data and generate more boundary files! Args: - path_to_td (str): Path to boundary tidal file. Ideally this should be a pre cut-out - netCDF file containing only the boundary region and 3 extra boundary points on either - side. Users can also provide a large dataset containing their entire domain but this - will be slower. - tidal_filename(str): Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} - + path_to_td (str): Path to boundary tidal file. + tidal_filename: Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} Returns: - *.nc files in inputdir/forcing - Tidal input files for the boundaries from the TPXO dataaset + *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 """ if not os.path.exists(path_to_td) or not os.path.exists(os.path.join(path_to_td,"h_"+tidal_filename)) or not os.path.exists(os.path.join(path_to_td,"u_"+tidal_filename)) : @@ -2115,7 +2126,31 @@ def __init__( @property def coords(self): """ - This function allows us to call the self.coords for use in the xesmf.Regridder in the regrid_tides function. + + + This function: + Allows us to call the self.coords for use in the xesmf.Regridder in the regrid_tides function. self.coords gives us the subset of the hgrid based on the orientation. + + Args: + None + Returns: + xr.Dataset: The correct coordinate space for the orientation + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 + """ # Rename nxp and nyp to locations if self.orientation == 'south': @@ -2475,24 +2510,34 @@ def rectangular_brushcut(self): def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, method='nearest_s2d', periodic=False): """ - The function regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. It accomplishes: + This function: + Regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. - Read in raw tidal data (all constituents) - Perform minor transformations/conversions - Regridded the tidal elevation, and tidal velocity - Encoding the output - Parameters (taken at initialization) - ------------------------------------- - infile_td : str - Raw Tidal File/Dir - tpxo_v, tpxo_u, tpxo_h: xr.Datasets - Specific adjusted for MOM6 tpxo datasets (Adjusted with setup_tides) - times: list[pd datetime] - The start date of our model period - Returns - ------- - *.nc files - Regridded tidal velocity and elevation files in 'inputdir/forcing' + Args: + infile_td (str): Raw Tidal File/Dir + tpxo_v, tpxo_u, tpxo_h (xarray.Dataset): Specific adjusted for MOM6 tpxo datasets (Adjusted with setup_tides) + times (pd.DateRange): The start date of our model period + Returns: + *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 """ ########## Tidal Elevation: Horizontally interpolate elevation components ############ @@ -2528,8 +2573,7 @@ def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, ds_ap, _ = xr.broadcast(ds_ap, times) ds_ap = ds_ap.transpose('time', 'constituent', 'locations') - ds_ap = self.expand_tidal_dims(ds_ap) - ds_ap = self.rename_tidal_dims(ds_ap) + self.encode_tidal_files_and_output(ds_ap, 'tz') @@ -2598,31 +2642,69 @@ def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, # Some things may have become missing during the transformation ds_ap = ds_ap.ffill(dim="locations", limit=None) - ds_ap = self.expand_tidal_dims(ds_ap) - ds_ap = self.rename_tidal_dims(ds_ap) - self.encode_tidal_files_and_output(ds_ap, 'tu') return def encode_tidal_files_and_output(self, ds, filename): - """Like GFDL NWA25 changed to RM6 segment class, add metadata to tidal files, format, and output them. - - Parameters (taken at initialization) - ------------------------------------- - self.outfolder: str/path - The output folder to save the tidal files into - dataset : xarray.Dataset - The processed tidal dataset - filename: str - The output file name - Returns - ------- - *.nc files - Regridded [FILENAME] files in 'self.outfolder/[filename]_[num].nc' + """ + This function: + - Expands the dimensions (with the segment name) + - Renames some dimensions to be more specific to the segment + - Provides an output file encoding + - Exports the files. + + Args: + self.outfolder (str/path): The output folder to save the tidal files into + dataset (xarray.Dataset): The processed tidal dataset + filename (str): The output file name + Returns: + *.nc files: Regridded [FILENAME] files in 'self.outfolder/forcing/[filename]_[segmentname].nc' + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 + """ + + ## Expand Tidal Dimensions ## + if 'z' in ds.coords or 'constituent' in ds.dims: + offset = 0 + else: + offset = 1 + if self.orientation in ['south', 'north']: + ds = ds.expand_dims(f'ny_{self.segment_name}', 2-offset) + elif self.orientation in ['west', 'east']: + ds = ds.expand_dims(f'nx_{self.segment_name}', 3-offset) + + ## Rename Tidal Dimensions ## + ds = ds.rename({ + 'lon': f'lon_{self.segment_name}', + 'lat': f'lat_{self.segment_name}' + }) + if 'z' in ds.coords: + ds = ds.rename({ + 'z': f'nz_{self.segment_name}' + }) + if self.orientation in ['south', 'north']: + ds = ds.rename({'locations': f'nx_{self.segment_name}'}) + elif self.orientation in ['west', 'east']: + ds = ds.rename({'locations': f'ny_{self.segment_name}'}) + + ## Perform Encoding ## for v in ds: ds[v].encoding['_FillValue']= 1.0e20 fname = f'{filename}_{self.segment_name}.nc' @@ -2636,6 +2718,8 @@ def encode_tidal_files_and_output(self, ds, filename): } if 'calendar' not in ds['time'].attrs and 'modulo' not in ds['time'].attrs: encoding.update({'time': dict(dtype='float64', calendar='gregorian', _FillValue=1.0e20)}) + + ## Export Files ## ds.to_netcdf( os.path.join(self.outfolder,"forcing", fname), engine='netcdf4', @@ -2644,48 +2728,4 @@ def encode_tidal_files_and_output(self, ds, filename): ) return - def expand_tidal_dims(self, ds): - """Add a length-1 dimension to the variables in a boundary dataset or array. - Named 'ny_segment_{self.segment_name}' if the border runs west to east (a south or north boundary), - or 'nx_segment_{self.segment_name}' if the border runs north to south (an east or west boundary). - - Args: - ds: boundary array with dimensions - - Returns: - modified array with new length-1 dimension. - """ - # having z or constituent as second dimension is optional, so offset determines where to place - # added dim - if 'z' in ds.coords or 'constituent' in ds.dims: - offset = 0 - else: - offset = 1 - if self.orientation in ['south', 'north']: - return ds.expand_dims(f'ny_{self.segment_name}', 2-offset) - elif self.orientation in ['west', 'east']: - return ds.expand_dims(f'nx_{self.segment_name}', 3-offset) - - def rename_tidal_dims(self, ds): - """Rename dimensions to be unique to the segment. - - Args: - ds (xarray.Dataset): Dataset that might contain 'lon', 'lat', 'z', and/or 'locations'. - - Returns: - xarray.Dataset: Dataset with dimensions renamed to include the segment identifier and to - match MOM6 expectations. - """ - ds = ds.rename({ - 'lon': f'lon_{self.segment_name}', - 'lat': f'lat_{self.segment_name}' - }) - if 'z' in ds.coords: - ds = ds.rename({ - 'z': f'nz_{self.segment_name}' - }) - if self.orientation in ['south', 'north']: - return ds.rename({'locations': f'nx_{self.segment_name}'}) - elif self.orientation in ['west', 'east']: - return ds.rename({'locations': f'ny_{self.segment_name}'}) - + \ No newline at end of file From 586832e955575913dbf7030d5423c88b171286f7 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 19 Sep 2024 14:26:23 -0600 Subject: [PATCH 20/81] Minor Path Function Changes --- regional_mom6/regional_mom6.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index dd151dd4..15427fea 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -194,7 +194,7 @@ def get_glorys_data( """ buffer = 0.24 # Pads downloads to ensure that interpolation onto desired domain doesn't fail. Default of 0.24 is twice Glorys cell width (12th degree) - path = Path(download_path) + path = os.path.join(download_path) if modify_existing: file = open(path / "get_glorysdata.sh", "r") @@ -1036,7 +1036,7 @@ def get_glorys_rectangular( ) print( - f"script `get_glorys_data.sh` has been greated at {raw_boundaries_path}.\n Run this script via bash to download the data from a terminal with internet access. \nYou will need to enter your Copernicus Marine username and password.\nIf you don't have an account, make one here:\nhttps://data.marine.copernicus.eu/register" + f"script `get_glorys_data.sh` has been created at {raw_boundaries_path}.\n Run this script via bash to download the data from a terminal with internet access. \nYou will need to enter your Copernicus Marine username and password.\nIf you don't have an account, make one here:\nhttps://data.marine.copernicus.eu/register" ) return @@ -1048,7 +1048,7 @@ def rectangular_boundaries( arakawa_grid="A", ): warnings.filterwarnings("default") # Set warnings back to on - warnings.warn("rectangular_boundaries is changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_rectangular_boundaries\"") + warnings.warn("The rectangular_boundaries function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_rectangular_boundaries\"") warnings.filterwarnings("ignore") # Set warnings back off return self.setup_ocean_state_rectangular_boundaries( raw_boundaries_path,varnames, boundaries=boundaries,arakawa_grid=arakawa_grid) @@ -1091,7 +1091,7 @@ def setup_ocean_state_rectangular_boundaries( # Now iterate through our four boundaries for orientation in boundaries: self.setup_ocean_state_simple_boundary( - Path(raw_boundaries_path) / (orientation + "_unprocessed.nc"), + Path(os.path.join((raw_boundaries_path),(orientation + "_unprocessed.nc"))), varnames, orientation, # The cardinal direction of the boundary find_MOM6_orientation(orientation), # A number to identify the boundary; indexes from 1 @@ -1102,7 +1102,7 @@ def simple_boundary( self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" ): warnings.filterwarnings("default") # Set warnings back to on - warnings.warn("simple_boundary is changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_simple_boundary\"") + warnings.warn("The simple_boundary function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_simple_boundary\"") warnings.filterwarnings("ignore") # Turn warnings off return self.setup_ocean_state_simple_boundary( path_to_bc, varnames, orientation, segment_number, arakawa_grid="A") @@ -1715,36 +1715,36 @@ def setup_run_directory( """ ## Get the path to the regional_mom package on this computer - premade_rundir_path = Path( - importlib.resources.files("regional_mom6") / "demos/premade_run_directories" - ) + premade_rundir_path = Path(os.path.join( + importlib.resources.files("regional_mom6"), "demos","premade_run_directories" + )) if not premade_rundir_path.exists(): print("Could not find premade run directories at ", premade_rundir_path) print( "Perhaps the package was imported directly rather than installed with conda. Checking if this is the case... " ) - premade_rundir_path = Path( + premade_rundir_path = Path(os.path.join( importlib.resources.files("regional_mom6").parent - / "demos/premade_run_directories" - ) + , "demos","premade_run_directories" + )) if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path} either.\n\n" + "There may be an issue with package installation. Check that the `premade_run_directory` folder is present in one of these two locations" ) else: - print("Found Premade Run Directories!") + print("It is! Found them!") # Define the locations of the directories we'll copy files across from. Base contains most of the files, and overwrite replaces files in the base directory. - base_run_dir = premade_rundir_path / "common_files" + base_run_dir = Path(os.path.join(premade_rundir_path , "common_files")) if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path}.\n\n" + "These files missing might be indicating an error during the package installation!" ) if surface_forcing: - overwrite_run_dir = premade_rundir_path / f"{surface_forcing}_surface" + overwrite_run_dir = Path(os.path.join(premade_rundir_path ,f"{surface_forcing}_surface")) if not overwrite_run_dir.exists(): available = [x for x in premade_rundir_path.iterdir() if x.is_dir()] raise ValueError( @@ -1937,7 +1937,7 @@ def setup_era5(self, era5_path): i for i in range(self.date_range[0].year, self.date_range[1].year + 1) ] # construct a list of all paths for all years to use for open_mfdataset - paths_per_year = [Path(f"{era5_path}/{fname}/{year}/") for year in years] + paths_per_year = [os.path.join(era5_path,fname,year) for year in years] all_files = [] for path in paths_per_year: # Use glob to find all files that match the pattern From c4ff6d3860942ac020157518e3d64a9b3c362db1 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 19 Sep 2024 16:35:22 -0600 Subject: [PATCH 21/81] First Implementation w/ Tides --- regional_mom6/regional_mom6.py | 91 +++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 5507de17..4a2ea4c8 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -16,7 +16,7 @@ import datetime from .utils import quadrilateral_areas, ap2ep, ep2ap,find_roughly_nearest_ny_nx import pandas as pd - +import re warnings.filterwarnings("ignore") __all__ = [ @@ -197,14 +197,14 @@ def get_glorys_data( path = os.path.join(download_path) if modify_existing: - file = open(path / "get_glorysdata.sh", "r") + file = open(os.path.join(path, "get_glorysdata.sh"), "r") lines = file.readlines() file.close() else: lines = ["#!/bin/bash\ncopernicusmarine login"] - file = open(path / "get_glorysdata.sh", "w") + file = open(os.path.join(path, "get_glorysdata.sh"), "w") lines.append( f""" @@ -1153,7 +1153,7 @@ def setup_ocean_state_simple_boundary( return def setup_tides_rectangle_boundaries( - self, path_to_td,tidal_filename, + self, path_to_td,tidal_filename,tidal_constituents = [0] ): """ This function: @@ -1162,6 +1162,7 @@ def setup_tides_rectangle_boundaries( Args: path_to_td (str): Path to boundary tidal file. tidal_filename: Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} + tidal_constiuents: List of tidal constituents to include in the regridding. Default is [0] which is the M2 constituent. Returns: *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' @@ -1187,7 +1188,8 @@ def setup_tides_rectangle_boundaries( ) ### Find Rough Horizontal Subset (with 0.5 Buffer)### - tidal_constituents = [0] + + self.tidal_constituents = tidal_constituents tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f'h_{tidal_filename}')) .rename({'lon_z': 'lon', 'lat_z': 'lat', 'nc': 'constituent'}) @@ -1709,6 +1711,7 @@ def setup_run_directory( surface_forcing=None, using_payu=False, overwrite=False, + with_tides = False ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify @@ -1765,6 +1768,14 @@ def setup_run_directory( ## In case there is additional forcing (e.g., tides) then we need to modify the run dir to include the additional forcing. overwrite_run_dir = False + # Check if we can implement tides + if with_tides: + tidal_files_exist = any("tidal" in filename for filename in os.listdir(os.path.join(self.mom_input_dir, "forcing"))) + if not tidal_files_exist: + raise ValueError("No files with 'tidal' in their names found in the forcing directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files.") + + + # 3 different cases to handle: # 1. User is creating a new run directory from scratch. Here we copy across all files and modify. # 2. User has already created a run directory, and wants to modify it. Here we only modify the MOM_layout file. @@ -1861,6 +1872,20 @@ def setup_run_directory( with open(self.mom_run_dir / "MOM_layout", "w") as f: f.writelines(lines) + + MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") + MOM_input_dict["MINIMUM_DEPTH"] = float(self.min_depth) + MOM_input_dict["NK"] = len(self.vgrid.zl.values) + if with_tides: + MOM_input_dict["TIDES"] = "True" + MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = self.tidal_constituents + MOM_input_dict["OBC_SEGMENT_001_DATA"] = "\"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)\"" + MOM_input_dict["OBC_SEGMENT_002_DATA"] = "\"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)\"" + MOM_input_dict["OBC_SEGMENT_003_DATA"] = "\"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)\"" + MOM_input_dict["OBC_SEGMENT_004_DATA"] = "\"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)\"" + + self.write_MOM_file(MOM_input_dict) + # Overwrite values pertaining to vertical structure in the MOM_input file with open(self.mom_run_dir / "MOM_input", "r") as file: lines = file.readlines() @@ -1910,19 +1935,51 @@ def setup_run_directory( ] nml.write(self.mom_run_dir / "input.nml", force=True) + def read_MOM_file_as_dict(self, filename): + """ + Read the MOM_input file and return a dictionary of the variables and their values. + """ + with open(os.path.join(self.mom_run_dir , filename), "r") as file: + lines = file.readlines() + MOM_file_dict = {"filename": filename} + for jj in range(len(lines)): + if "=" in lines[jj]: + var, value = lines[jj].split("=") + value = value.split("!")[0].strip() # Remove Comments + MOM_file_dict[var.strip()] = value.strip() - def write_MOM_input(self, variable_dict): - """ - Write MOM Input based on specific file variable format i.e. Var = Value""" - # Overwrite values pertaining to vertical structure in the MOM_input file - with open(self.mom_run_dir / "MOM_input", "r") as file: - lines = file.readlines() - for jj in range(len(lines)): - for var in variable_dict.keys(): - if "{} = ".format(var) in lines[jj]: - lines[jj] = f'{var} = {variable_dict[var]}\n' - with open(self.mom_run_dir / "MOM_input", "w") as f: - f.writelines(lines) + # Save a copy of the original dictionary + MOM_file_dict["original"] = MOM_file_dict.copy() + return MOM_file_dict + + def write_MOM_file(self, MOM_file_dict): + """ + Write the MOM_input file from a dictionary of variables and their values. Does not support removing fields. + """ + # Replace specific variable values + original_MOM_file_dict = MOM_file_dict.pop("original") + with open(os.path.join(self.mom_run_dir , MOM_file_dict["filename"]), "r") as file: + lines = file.readlines() + for jj in range(len(lines)): + if "=" in lines[jj]: + var = lines[jj].split("=")[0].strip() + if var in MOM_file_dict.keys() and MOM_file_dict[var] != original_MOM_file_dict[var]: + lines[jj] = f"{var} = {MOM_file_dict[var]}\n" + print("Changed", var, "from", original_MOM_file_dict[var], "to", MOM_file_dict[var], "in {}!".format(MOM_file_dict["filename"])) + + # Add new fields + for key in MOM_file_dict.keys(): + if key not in original_MOM_file_dict.keys(): + lines.append(f"{key} = {MOM_file_dict[key]}\n") + print("Added", key, "to", MOM_file_dict["filename"], "with value", MOM_file_dict[key]) + + # Check any fields removed + for key in original_MOM_file_dict.keys(): + if key not in MOM_file_dict.keys(): + print("WARNING: Field", key, "was not found in the new dictionary. Keeping the original value of", original_MOM_file_dict[key]) + + with open(os.path.join(self.mom_run_dir ,MOM_file_dict["filename"]), "w") as f: + f.writelines(lines) def setup_era5(self, era5_path): From ff8dc2c819c6a2e8de8b9bc1a8e2954b3ecf6d30 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 20 Sep 2024 09:32:41 -0600 Subject: [PATCH 22/81] Minor Edit --- regional_mom6/regional_mom6.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 4a2ea4c8..8929871b 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1878,7 +1878,7 @@ def setup_run_directory( MOM_input_dict["NK"] = len(self.vgrid.zl.values) if with_tides: MOM_input_dict["TIDES"] = "True" - MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = self.tidal_constituents + MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = len(self.tidal_constituents) MOM_input_dict["OBC_SEGMENT_001_DATA"] = "\"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)\"" MOM_input_dict["OBC_SEGMENT_002_DATA"] = "\"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)\"" MOM_input_dict["OBC_SEGMENT_003_DATA"] = "\"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)\"" @@ -1941,10 +1941,11 @@ def read_MOM_file_as_dict(self, filename): """ with open(os.path.join(self.mom_run_dir , filename), "r") as file: lines = file.readlines() + filtered_lines = [line for line in lines if '===' not in line] MOM_file_dict = {"filename": filename} - for jj in range(len(lines)): + for jj in range(len(filtered_lines)): if "=" in lines[jj]: - var, value = lines[jj].split("=") + var, value,_ = filtered_lines[jj].split("=") value = value.split("!")[0].strip() # Remove Comments MOM_file_dict[var.strip()] = value.strip() @@ -1960,11 +1961,12 @@ def write_MOM_file(self, MOM_file_dict): original_MOM_file_dict = MOM_file_dict.pop("original") with open(os.path.join(self.mom_run_dir , MOM_file_dict["filename"]), "r") as file: lines = file.readlines() - for jj in range(len(lines)): - if "=" in lines[jj]: - var = lines[jj].split("=")[0].strip() + filtered_lines = [line for line in lines if '===' not in line] + for jj in range(len(filtered_lines)): + if "=" in filtered_lines[jj]: + var = filtered_lines[jj].split("=")[0].strip() if var in MOM_file_dict.keys() and MOM_file_dict[var] != original_MOM_file_dict[var]: - lines[jj] = f"{var} = {MOM_file_dict[var]}\n" + filtered_lines[jj] = f"{var} = {MOM_file_dict[var]}\n" print("Changed", var, "from", original_MOM_file_dict[var], "to", MOM_file_dict[var], "in {}!".format(MOM_file_dict["filename"])) # Add new fields @@ -1979,7 +1981,7 @@ def write_MOM_file(self, MOM_file_dict): print("WARNING: Field", key, "was not found in the new dictionary. Keeping the original value of", original_MOM_file_dict[key]) with open(os.path.join(self.mom_run_dir ,MOM_file_dict["filename"]), "w") as f: - f.writelines(lines) + f.writelines(filtered_lines) def setup_era5(self, era5_path): From 6a9260437ba11a0f27c75aacce8e37cc11231644 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 20 Sep 2024 10:31:05 -0600 Subject: [PATCH 23/81] Additional Formatting Changes --- regional_mom6/regional_mom6.py | 66 ++++++++++++++-------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 8929871b..c977c581 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1853,24 +1853,19 @@ def setup_run_directory( ## Modify the MOM_layout file to have correct horizontal dimensions and CPU layout # TODO Re-implement with package that works for this file type? or at least tidy up code - with open(self.mom_run_dir / "MOM_layout", "r") as file: - lines = file.readlines() - for jj in range(len(lines)): - if "MASKTABLE" in lines[jj]: - if mask_table != None: - lines[jj] = f'MASKTABLE = "{mask_table}"\n' - else: - lines[jj] = "# MASKTABLE = no mask table" - if "LAYOUT =" in lines[jj] and "IO" not in lines[jj] and layout != None: - lines[jj] = f"LAYOUT = {layout[1]},{layout[0]}\n" - - if "NIGLOBAL" in lines[jj]: - lines[jj] = f"NIGLOBAL = {self.hgrid.nx.shape[0]//2}\n" - - if "NJGLOBAL" in lines[jj]: - lines[jj] = f"NJGLOBAL = {self.hgrid.ny.shape[0]//2}\n" - with open(self.mom_run_dir / "MOM_layout", "w") as f: - f.writelines(lines) + MOM_layout_dict = self.read_MOM_file_as_dict("MOM_layout") + if "MASKTABLE" in MOM_layout_dict.keys(): + if mask_table != None: + MOM_layout_dict["MASKTABLE"] = mask_table + else: + MOM_layout_dict["MASKTABLE"] = "# MASKTABLE = no mask table" + if "LAYOUT" in MOM_layout_dict.keys() and "IO" not in MOM_layout_dict.keys() and layout != None: + MOM_layout_dict["LAYOUT"] = str(layout[1])+","+str(layout[0]) + if "NIGLOBAL" in MOM_layout_dict.keys(): + MOM_layout_dict["NIGLOBAL"] = self.hgrid.nx.shape[0]//2 + if "NJGLOBAL" in MOM_layout_dict.keys(): + MOM_layout_dict["NJGLOBAL"] = self.hgrid.ny.shape[0]//2 + self.write_MOM_file(MOM_layout_dict) MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") @@ -1885,17 +1880,6 @@ def setup_run_directory( MOM_input_dict["OBC_SEGMENT_004_DATA"] = "\"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)\"" self.write_MOM_file(MOM_input_dict) - - # Overwrite values pertaining to vertical structure in the MOM_input file - with open(self.mom_run_dir / "MOM_input", "r") as file: - lines = file.readlines() - for jj in range(len(lines)): - if "MINIMUM_DEPTH = " in lines[jj]: - lines[jj] = f'MINIMUM_DEPTH = {float(self.min_depth)}\n' - if "NK =" in lines[jj]: - lines[jj] = f"NK = {len(self.vgrid.zl.values)}\n" - with open(self.mom_run_dir / "MOM_input", "w") as f: - f.writelines(lines) ## If using payu to run the model, create a payu configuration file if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): @@ -1934,6 +1918,7 @@ def setup_run_directory( 0, ] nml.write(self.mom_run_dir / "input.nml", force=True) + return def read_MOM_file_as_dict(self, filename): """ @@ -1941,11 +1926,12 @@ def read_MOM_file_as_dict(self, filename): """ with open(os.path.join(self.mom_run_dir , filename), "r") as file: lines = file.readlines() - filtered_lines = [line for line in lines if '===' not in line] MOM_file_dict = {"filename": filename} - for jj in range(len(filtered_lines)): - if "=" in lines[jj]: - var, value,_ = filtered_lines[jj].split("=") + for jj in range(len(lines)): + if "=" in lines[jj] and not "===" in lines[jj]: + split = lines[jj].split("=") + var = split[0] + value = split[1] value = value.split("!")[0].strip() # Remove Comments MOM_file_dict[var.strip()] = value.strip() @@ -1961,15 +1947,15 @@ def write_MOM_file(self, MOM_file_dict): original_MOM_file_dict = MOM_file_dict.pop("original") with open(os.path.join(self.mom_run_dir , MOM_file_dict["filename"]), "r") as file: lines = file.readlines() - filtered_lines = [line for line in lines if '===' not in line] - for jj in range(len(filtered_lines)): - if "=" in filtered_lines[jj]: - var = filtered_lines[jj].split("=")[0].strip() - if var in MOM_file_dict.keys() and MOM_file_dict[var] != original_MOM_file_dict[var]: - filtered_lines[jj] = f"{var} = {MOM_file_dict[var]}\n" + for jj in range(len(lines)): + if "=" in lines[jj] and not "===" in lines[jj]: + var = lines[jj].split("=")[0].strip() + if var in MOM_file_dict.keys() and (str(MOM_file_dict[var])) != original_MOM_file_dict[var]: + lines[jj] = lines[jj].replace(original_MOM_file_dict[var], str(MOM_file_dict[var])) print("Changed", var, "from", original_MOM_file_dict[var], "to", MOM_file_dict[var], "in {}!".format(MOM_file_dict["filename"])) # Add new fields + lines.append("! === Added with RM6 ===\n") for key in MOM_file_dict.keys(): if key not in original_MOM_file_dict.keys(): lines.append(f"{key} = {MOM_file_dict[key]}\n") @@ -1981,7 +1967,7 @@ def write_MOM_file(self, MOM_file_dict): print("WARNING: Field", key, "was not found in the new dictionary. Keeping the original value of", original_MOM_file_dict[key]) with open(os.path.join(self.mom_run_dir ,MOM_file_dict["filename"]), "w") as f: - f.writelines(filtered_lines) + f.writelines(lines) def setup_era5(self, era5_path): From 1aed99f8c5945358ba1f25261029295d1fb46876 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 20 Sep 2024 11:17:06 -0600 Subject: [PATCH 24/81] Additional Debugging --- regional_mom6/regional_mom6.py | 11 ++++++----- regional_mom6/utils.py | 6 ++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index c977c581..554c53c1 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,7 +14,7 @@ import os import importlib.resources import datetime -from .utils import quadrilateral_areas, ap2ep, ep2ap,find_roughly_nearest_ny_nx +from .utils import quadrilateral_areas, ap2ep, ep2ap,find_roughly_nearest_ny_nx,convert_lon_180_to_360 import pandas as pd import re warnings.filterwarnings("ignore") @@ -1195,8 +1195,9 @@ def setup_tides_rectangle_boundaries( .rename({'lon_z': 'lon', 'lat_z': 'lat', 'nc': 'constituent'}) .isel(constituent=tidal_constituents) ) - ny0,nx0 = find_roughly_nearest_ny_nx(self.latitude_extent[0]-0.5,self.longitude_extent[0]-0.5,tpxo_h ) - ny1,nx1 = find_roughly_nearest_ny_nx(self.latitude_extent[1]+0.5,self.longitude_extent[1]+0.5,tpxo_h) + tidal_360_lon = [convert_lon_180_to_360(self.longitude_extent[0]),convert_lon_180_to_360(self.longitude_extent[1])] + ny0,nx0 = find_roughly_nearest_ny_nx(self.latitude_extent[0]-0.5,tidal_360_lon[0]-0.5,tpxo_h ) + ny1,nx1 = find_roughly_nearest_ny_nx(self.latitude_extent[1]+0.5,tidal_360_lon[1]+0.5,tpxo_h) horizontal_subset = dict(ny=slice(ny0,ny1), nx=slice(nx0,nx1)) tpxo_h = tpxo_h.isel( **horizontal_subset) @@ -1929,7 +1930,7 @@ def read_MOM_file_as_dict(self, filename): MOM_file_dict = {"filename": filename} for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: - split = lines[jj].split("=") + split = lines[jj].split("=",1) var = split[0] value = split[1] value = value.split("!")[0].strip() # Remove Comments @@ -1949,7 +1950,7 @@ def write_MOM_file(self, MOM_file_dict): lines = file.readlines() for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: - var = lines[jj].split("=")[0].strip() + var = lines[jj].split("=",1)[0].strip() if var in MOM_file_dict.keys() and (str(MOM_file_dict[var])) != original_MOM_file_dict[var]: lines[jj] = lines[jj].replace(original_MOM_file_dict[var], str(MOM_file_dict[var])) print("Changed", var, "from", original_MOM_file_dict[var], "to", MOM_file_dict[var], "in {}!".format(MOM_file_dict["filename"])) diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index 2409545b..7bb0caa7 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -300,3 +300,9 @@ def find_roughly_nearest_ny_nx(lat, lon, ds): ny = (np.abs(ds.lat.values - lat)).argmin(axis=1)[0] # We're looking for an nx, I know it's not exact, but this works nx = (np.abs(ds.lon.values - lon)).argmin(axis=0)[0] return ny,nx + +def convert_lon_180_to_360(lon): + """ + Converts a longitude from -180 to 180 to 0 to 360 + """ + return lon + 180 From 533e3ed63c1cb806fc2cbff207c80bdc380407c1 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Mon, 23 Sep 2024 16:03:46 -0600 Subject: [PATCH 25/81] remove login step so that command can be run via subprocess --- regional_mom6/regional_mom6.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index a7d5f476..5c06412d 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -178,7 +178,7 @@ def get_glorys_data( file.close() else: - lines = ["#!/bin/bash\ncopernicusmarine login"] + lines = ["#!/bin/bash\n"] file = open(path / "get_glorysdata.sh", "w") @@ -625,13 +625,14 @@ def initial_condition( varnames, arakawa_grid="A", vcoord_type="height", + ): """ Reads the initial condition from files in ``ic_path``, interpolates to the model grid, fixes up metadata, and saves back to the input directory. Args: - raw_ic_path (Union[str, Path]): Path to raw initial condition file to read in. + raw_ic_path (Union[str, Path,list of str]): Path(s) to raw initial condition file(s) to read in. varnames (Dict[str, str]): Mapping from MOM6 variable/coordinate names to the names in the input dataset. For example, ``{'xq': 'lonq', 'yh': 'lath', 'salt': 'so', ...}``. arakawa_grid (Optional[str]): Arakawa grid staggering type of the initial condition. @@ -643,7 +644,7 @@ def initial_condition( # Remove time dimension if present in the IC. # Assume that the first time dim is the intended on if more than one is present - ic_raw = xr.open_dataset(raw_ic_path) + ic_raw = xr.open_mfdataset(raw_ic_path) if varnames["time"] in ic_raw.dims: ic_raw = ic_raw.isel({varnames["time"]: 0}) if varnames["time"] in ic_raw.coords: From b6aa46b6814a09dd47e5cf6a01d652d9acd480b3 Mon Sep 17 00:00:00 2001 From: Ashley Barnes <53282288+ashjbarnes@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:07:09 -0600 Subject: [PATCH 26/81] Update pyproject.toml Need to freeze xarray because there's an issue with zip with latest. They've changed a setting to "strict", causing an error that used to be no issue. @manishvenu found the issue when doing a fresh install --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dacad296..e9609ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "netCDF4", "numpy >= 1.17.0, < 2.0.0", "scipy >= 1.2.0", - "xarray", + "xarray == 2024.7.0", "xesmf >= 0.8.4", "f90nml >= 1.4.1", ] From 3d5c61ca5d7b3295f343ded2b2b7296963803a23 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 25 Sep 2024 15:12:29 -0600 Subject: [PATCH 27/81] black refomat --- regional_mom6/regional_mom6.py | 531 +++++++++++++++++++-------------- regional_mom6/utils.py | 236 ++++++++------- 2 files changed, 434 insertions(+), 333 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 9f4eb4ac..d5efc7a4 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,9 +14,16 @@ import os import importlib.resources import datetime -from .utils import quadrilateral_areas, ap2ep, ep2ap,find_roughly_nearest_ny_nx,convert_lon_180_to_360 +from .utils import ( + quadrilateral_areas, + ap2ep, + ep2ap, + find_roughly_nearest_ny_nx, + convert_lon_180_to_360, +) import pandas as pd -import re +import re + warnings.filterwarnings("ignore") __all__ = [ @@ -144,6 +151,7 @@ def longitude_slicer(data, longitude_extent, longitude_coords): return data + def find_MOM6_orientation(input): """ Convert between MOM6 boundary and the specific segment number needed, or the inverse @@ -160,7 +168,9 @@ def find_MOM6_orientation(input): try: return direction_dir[input] except: - raise ValueError("Invalid Input. Did you spell the direction wrong, it should be lowercase?") + raise ValueError( + "Invalid Input. Did you spell the direction wrong, it should be lowercase?" + ) elif type(input) == int: try: return direction_dir_inv[input] @@ -169,6 +179,7 @@ def find_MOM6_orientation(input): else: raise ValueError("Invalid type of Input, can only be string or int.") + from pathlib import Path @@ -528,7 +539,9 @@ def __init__( self.hgrid = self._make_hgrid() self.vgrid = self._make_vgrid() - self.segments = {} # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) + self.segments = ( + {} + ) # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) # create additional directories and links (self.mom_input_dir / "weights").mkdir(exist_ok=True) @@ -652,7 +665,6 @@ def initial_condition( varnames, arakawa_grid="A", vcoord_type="height", - ): """ Reads the initial condition from files in ``ic_path``, interpolates to the @@ -1048,10 +1060,17 @@ def rectangular_boundaries( boundaries=["south", "north", "west", "east"], arakawa_grid="A", ): - warnings.filterwarnings("default") # Set warnings back to on - warnings.warn("The rectangular_boundaries function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_rectangular_boundaries\"") - warnings.filterwarnings("ignore") # Set warnings back off - return self.setup_ocean_state_rectangular_boundaries( raw_boundaries_path,varnames, boundaries=boundaries,arakawa_grid=arakawa_grid) + warnings.filterwarnings("default") # Set warnings back to on + warnings.warn( + 'The rectangular_boundaries function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with "setup_ocean_state_rectangular_boundaries"' + ) + warnings.filterwarnings("ignore") # Set warnings back off + return self.setup_ocean_state_rectangular_boundaries( + raw_boundaries_path, + varnames, + boundaries=boundaries, + arakawa_grid=arakawa_grid, + ) def setup_ocean_state_rectangular_boundaries( self, @@ -1092,20 +1111,30 @@ def setup_ocean_state_rectangular_boundaries( # Now iterate through our four boundaries for orientation in boundaries: self.setup_ocean_state_simple_boundary( - Path(os.path.join((raw_boundaries_path),(orientation + "_unprocessed.nc"))), + Path( + os.path.join( + (raw_boundaries_path), (orientation + "_unprocessed.nc") + ) + ), varnames, orientation, # The cardinal direction of the boundary - find_MOM6_orientation(orientation), # A number to identify the boundary; indexes from 1 + find_MOM6_orientation( + orientation + ), # A number to identify the boundary; indexes from 1 arakawa_grid=arakawa_grid, ) def simple_boundary( self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" ): - warnings.filterwarnings("default") # Set warnings back to on - warnings.warn("The simple_boundary function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with \"setup_ocean_state_simple_boundary\"") - warnings.filterwarnings("ignore") # Turn warnings off - return self.setup_ocean_state_simple_boundary( path_to_bc, varnames, orientation, segment_number, arakawa_grid="A") + warnings.filterwarnings("default") # Set warnings back to on + warnings.warn( + 'The simple_boundary function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with "setup_ocean_state_simple_boundary"' + ) + warnings.filterwarnings("ignore") # Turn warnings off + return self.setup_ocean_state_simple_boundary( + path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" + ) def setup_ocean_state_simple_boundary( self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" @@ -1152,27 +1181,27 @@ def setup_ocean_state_simple_boundary( self.segments[orientation] = seg print("Done.") return - + def setup_tides_rectangle_boundaries( - self, path_to_td,tidal_filename,tidal_constituents = [0] + self, path_to_td, tidal_filename, tidal_constituents=[0] ): """ This function: We subset our tidal data and generate more boundary files! Args: - path_to_td (str): Path to boundary tidal file. + path_to_td (str): Path to boundary tidal file. tidal_filename: Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} tidal_constiuents: List of tidal constituents to include in the regridding. Default is [0] which is the M2 constituent. Returns: *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' - General Description: + General Description: This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: - Converted code for RM6 segment class - Implemented Horizontal Subsetting - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) - + Original Code was sourced from: Author(s): GFDL, James Simkins, Rob Cermak, etc.. @@ -1180,54 +1209,70 @@ def setup_tides_rectangle_boundaries( Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" Version: N/A Type: Python Functions, Source Code - Web Address: https://github.com/jsimkins2/nwa25 + Web Address: https://github.com/jsimkins2/nwa25 """ - if not os.path.exists(path_to_td) or not os.path.exists(os.path.join(path_to_td,"h_"+tidal_filename)) or not os.path.exists(os.path.join(path_to_td,"u_"+tidal_filename)) : - raise ValueError ( - "Tidal Files don't exist at " + path_to_td+"/[h.or.u]_"+tidal_filename+".nc" + if ( + not os.path.exists(path_to_td) + or not os.path.exists(os.path.join(path_to_td, "h_" + tidal_filename)) + or not os.path.exists(os.path.join(path_to_td, "u_" + tidal_filename)) + ): + raise ValueError( + "Tidal Files don't exist at " + + path_to_td + + "/[h.or.u]_" + + tidal_filename + + ".nc" ) - + ### Find Rough Horizontal Subset (with 0.5 Buffer)### - + self.tidal_constituents = tidal_constituents tpxo_h = ( - xr.open_dataset(os.path.join(path_to_td, f'h_{tidal_filename}')) - .rename({'lon_z': 'lon', 'lat_z': 'lat', 'nc': 'constituent'}) + xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) + .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) .isel(constituent=tidal_constituents) ) - tidal_360_lon = [convert_lon_180_to_360(self.longitude_extent[0]),convert_lon_180_to_360(self.longitude_extent[1])] - ny0,nx0 = find_roughly_nearest_ny_nx(self.latitude_extent[0]-0.5,tidal_360_lon[0]-0.5,tpxo_h ) - ny1,nx1 = find_roughly_nearest_ny_nx(self.latitude_extent[1]+0.5,tidal_360_lon[1]+0.5,tpxo_h) - horizontal_subset = dict(ny=slice(ny0,ny1), nx=slice(nx0,nx1)) - - tpxo_h = tpxo_h.isel( **horizontal_subset) - - - h = tpxo_h['ha'] * np.exp(-1j * np.radians(tpxo_h['hp'])) - tpxo_h['hRe'] = np.real(h) - tpxo_h['hIm'] = np.imag(h) + tidal_360_lon = [ + convert_lon_180_to_360(self.longitude_extent[0]), + convert_lon_180_to_360(self.longitude_extent[1]), + ] + ny0, nx0 = find_roughly_nearest_ny_nx( + self.latitude_extent[0] - 0.5, tidal_360_lon[0] - 0.5, tpxo_h + ) + ny1, nx1 = find_roughly_nearest_ny_nx( + self.latitude_extent[1] + 0.5, tidal_360_lon[1] + 0.5, tpxo_h + ) + horizontal_subset = dict(ny=slice(ny0, ny1), nx=slice(nx0, nx1)) + + tpxo_h = tpxo_h.isel(**horizontal_subset) + + h = tpxo_h["ha"] * np.exp(-1j * np.radians(tpxo_h["hp"])) + tpxo_h["hRe"] = np.real(h) + tpxo_h["hIm"] = np.imag(h) tpxo_u = ( - xr.open_dataset(os.path.join(path_to_td, f'u_{tidal_filename}')) - .rename({'lon_u': 'lon', 'lat_u': 'lat', 'nc': 'constituent'}) + xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) + .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) .isel(constituent=tidal_constituents, **horizontal_subset) ) - tpxo_u['ua'] *= 0.01 # convert to m/s - u = tpxo_u['ua'] * np.exp(-1j * np.radians(tpxo_u['up'])) - tpxo_u['uRe'] = np.real(u) - tpxo_u['uIm'] = np.imag(u) + tpxo_u["ua"] *= 0.01 # convert to m/s + u = tpxo_u["ua"] * np.exp(-1j * np.radians(tpxo_u["up"])) + tpxo_u["uRe"] = np.real(u) + tpxo_u["uIm"] = np.imag(u) tpxo_v = ( - xr.open_dataset(os.path.join(path_to_td, f'u_{tidal_filename}')) - .rename({'lon_v': 'lon', 'lat_v': 'lat', 'nc': 'constituent'}) + xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) + .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) .isel(constituent=tidal_constituents, **horizontal_subset) ) - tpxo_v['va'] *= 0.01 # convert to m/s - v = tpxo_v['va'] * np.exp(-1j * np.radians(tpxo_v['vp'])) - tpxo_v['vRe'] = np.real(v) - tpxo_v['vIm'] = np.imag(v) + tpxo_v["va"] *= 0.01 # convert to m/s + v = tpxo_v["va"] * np.exp(-1j * np.radians(tpxo_v["vp"])) + tpxo_v["vRe"] = np.real(v) + tpxo_v["vIm"] = np.imag(v) times = xr.DataArray( - pd.date_range(self.date_range[0], periods=1), # Import pandas for this shouldn't be a big deal b/c it's already required in rm6 dependencies - dims=['time'] + pd.date_range( + self.date_range[0], periods=1 + ), # Import pandas for this shouldn't be a big deal b/c it's already required in rm6 dependencies + dims=["time"], ) boundaries = ["south", "north", "west", "east"] @@ -1238,20 +1283,20 @@ def setup_tides_rectangle_boundaries( # If the GLORYS ocean_state has already created segments, we don't create them again. if b not in self.segments: seg = segment( - hgrid=self.hgrid, - infile=None, # location of raw boundary - outfolder=self.mom_input_dir, - varnames=None, - segment_name="segment_{:03d}".format(find_MOM6_orientation(b)), - orientation=b, # orienataion - startdate=self.date_range[0], - repeat_year_forcing=self.repeat_year_forcing, + hgrid=self.hgrid, + infile=None, # location of raw boundary + outfolder=self.mom_input_dir, + varnames=None, + segment_name="segment_{:03d}".format(find_MOM6_orientation(b)), + orientation=b, # orienataion + startdate=self.date_range[0], + repeat_year_forcing=self.repeat_year_forcing, ) else: seg = self.segments[b] # Output and regrid tides - seg.regrid_tides(tpxo_v, tpxo_u,tpxo_h, times) + seg.regrid_tides(tpxo_v, tpxo_u, tpxo_h, times) print("Done") def setup_bathymetry( @@ -1501,8 +1546,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): ## REMOVE INLAND LAKES - ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= self.min_depth, 0,1 - ) + ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= self.min_depth, 0, 1) land_mask = np.abs(ocean_mask - 1) changed = True ## keeps track of whether solution has converged or not @@ -1709,11 +1753,7 @@ def cpu_layout(self, layout): return def setup_run_directory( - self, - surface_forcing=None, - using_payu=False, - overwrite=False, - with_tides = False + self, surface_forcing=None, using_payu=False, overwrite=False, with_tides=False ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify @@ -1731,19 +1771,26 @@ def setup_run_directory( """ ## Get the path to the regional_mom package on this computer - premade_rundir_path = Path(os.path.join( - importlib.resources.files("regional_mom6"), "demos","premade_run_directories" - )) + premade_rundir_path = Path( + os.path.join( + importlib.resources.files("regional_mom6"), + "demos", + "premade_run_directories", + ) + ) if not premade_rundir_path.exists(): print("Could not find premade run directories at ", premade_rundir_path) print( "Perhaps the package was imported directly rather than installed with conda. Checking if this is the case... " ) - premade_rundir_path = Path(os.path.join( - importlib.resources.files("regional_mom6").parent - , "demos","premade_run_directories" - )) + premade_rundir_path = Path( + os.path.join( + importlib.resources.files("regional_mom6").parent, + "demos", + "premade_run_directories", + ) + ) if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path} either.\n\n" @@ -1753,14 +1800,16 @@ def setup_run_directory( print("It is! Found them!") # Define the locations of the directories we'll copy files across from. Base contains most of the files, and overwrite replaces files in the base directory. - base_run_dir = Path(os.path.join(premade_rundir_path , "common_files")) + base_run_dir = Path(os.path.join(premade_rundir_path, "common_files")) if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path}.\n\n" + "These files missing might be indicating an error during the package installation!" ) if surface_forcing: - overwrite_run_dir = Path(os.path.join(premade_rundir_path ,f"{surface_forcing}_surface")) + overwrite_run_dir = Path( + os.path.join(premade_rundir_path, f"{surface_forcing}_surface") + ) if not overwrite_run_dir.exists(): available = [x for x in premade_rundir_path.iterdir() if x.is_dir()] raise ValueError( @@ -1772,11 +1821,14 @@ def setup_run_directory( # Check if we can implement tides if with_tides: - tidal_files_exist = any("tidal" in filename for filename in os.listdir(os.path.join(self.mom_input_dir, "forcing"))) + tidal_files_exist = any( + "tidal" in filename + for filename in os.listdir(os.path.join(self.mom_input_dir, "forcing")) + ) if not tidal_files_exist: - raise ValueError("No files with 'tidal' in their names found in the forcing directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files.") - - + raise ValueError( + "No files with 'tidal' in their names found in the forcing directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files." + ) # 3 different cases to handle: # 1. User is creating a new run directory from scratch. Here we copy across all files and modify. @@ -1861,25 +1913,36 @@ def setup_run_directory( MOM_layout_dict["MASKTABLE"] = mask_table else: MOM_layout_dict["MASKTABLE"] = "# MASKTABLE = no mask table" - if "LAYOUT" in MOM_layout_dict.keys() and "IO" not in MOM_layout_dict.keys() and layout != None: - MOM_layout_dict["LAYOUT"] = str(layout[1])+","+str(layout[0]) + if ( + "LAYOUT" in MOM_layout_dict.keys() + and "IO" not in MOM_layout_dict.keys() + and layout != None + ): + MOM_layout_dict["LAYOUT"] = str(layout[1]) + "," + str(layout[0]) if "NIGLOBAL" in MOM_layout_dict.keys(): - MOM_layout_dict["NIGLOBAL"] = self.hgrid.nx.shape[0]//2 + MOM_layout_dict["NIGLOBAL"] = self.hgrid.nx.shape[0] // 2 if "NJGLOBAL" in MOM_layout_dict.keys(): - MOM_layout_dict["NJGLOBAL"] = self.hgrid.ny.shape[0]//2 + MOM_layout_dict["NJGLOBAL"] = self.hgrid.ny.shape[0] // 2 self.write_MOM_file(MOM_layout_dict) - MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") MOM_input_dict["MINIMUM_DEPTH"] = float(self.min_depth) MOM_input_dict["NK"] = len(self.vgrid.zl.values) if with_tides: MOM_input_dict["TIDES"] = "True" MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = len(self.tidal_constituents) - MOM_input_dict["OBC_SEGMENT_001_DATA"] = "\"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)\"" - MOM_input_dict["OBC_SEGMENT_002_DATA"] = "\"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)\"" - MOM_input_dict["OBC_SEGMENT_003_DATA"] = "\"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)\"" - MOM_input_dict["OBC_SEGMENT_004_DATA"] = "\"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)\"" + MOM_input_dict["OBC_SEGMENT_001_DATA"] = ( + '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)"' + ) + MOM_input_dict["OBC_SEGMENT_002_DATA"] = ( + '"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)"' + ) + MOM_input_dict["OBC_SEGMENT_003_DATA"] = ( + '"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)"' + ) + MOM_input_dict["OBC_SEGMENT_004_DATA"] = ( + '"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)"' + ) self.write_MOM_file(MOM_input_dict) @@ -1926,15 +1989,15 @@ def read_MOM_file_as_dict(self, filename): """ Read the MOM_input file and return a dictionary of the variables and their values. """ - with open(os.path.join(self.mom_run_dir , filename), "r") as file: + with open(os.path.join(self.mom_run_dir, filename), "r") as file: lines = file.readlines() MOM_file_dict = {"filename": filename} for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: - split = lines[jj].split("=",1) + split = lines[jj].split("=", 1) var = split[0] value = split[1] - value = value.split("!")[0].strip() # Remove Comments + value = value.split("!")[0].strip() # Remove Comments MOM_file_dict[var.strip()] = value.strip() # Save a copy of the original dictionary @@ -1947,30 +2010,56 @@ def write_MOM_file(self, MOM_file_dict): """ # Replace specific variable values original_MOM_file_dict = MOM_file_dict.pop("original") - with open(os.path.join(self.mom_run_dir , MOM_file_dict["filename"]), "r") as file: + with open( + os.path.join(self.mom_run_dir, MOM_file_dict["filename"]), "r" + ) as file: lines = file.readlines() for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: - var = lines[jj].split("=",1)[0].strip() - if var in MOM_file_dict.keys() and (str(MOM_file_dict[var])) != original_MOM_file_dict[var]: - lines[jj] = lines[jj].replace(original_MOM_file_dict[var], str(MOM_file_dict[var])) - print("Changed", var, "from", original_MOM_file_dict[var], "to", MOM_file_dict[var], "in {}!".format(MOM_file_dict["filename"])) - + var = lines[jj].split("=", 1)[0].strip() + if ( + var in MOM_file_dict.keys() + and (str(MOM_file_dict[var])) != original_MOM_file_dict[var] + ): + lines[jj] = lines[jj].replace( + original_MOM_file_dict[var], str(MOM_file_dict[var]) + ) + print( + "Changed", + var, + "from", + original_MOM_file_dict[var], + "to", + MOM_file_dict[var], + "in {}!".format(MOM_file_dict["filename"]), + ) + # Add new fields lines.append("! === Added with RM6 ===\n") for key in MOM_file_dict.keys(): if key not in original_MOM_file_dict.keys(): lines.append(f"{key} = {MOM_file_dict[key]}\n") - print("Added", key, "to", MOM_file_dict["filename"], "with value", MOM_file_dict[key]) + print( + "Added", + key, + "to", + MOM_file_dict["filename"], + "with value", + MOM_file_dict[key], + ) # Check any fields removed for key in original_MOM_file_dict.keys(): if key not in MOM_file_dict.keys(): - print("WARNING: Field", key, "was not found in the new dictionary. Keeping the original value of", original_MOM_file_dict[key]) - - with open(os.path.join(self.mom_run_dir ,MOM_file_dict["filename"]), "w") as f: - f.writelines(lines) - + print( + "WARNING: Field", + key, + "was not found in the new dictionary. Keeping the original value of", + original_MOM_file_dict[key], + ) + + with open(os.path.join(self.mom_run_dir, MOM_file_dict["filename"]), "w") as f: + f.writelines(lines) def setup_era5(self, era5_path): """ @@ -1995,7 +2084,7 @@ def setup_era5(self, era5_path): i for i in range(self.date_range[0].year, self.date_range[1].year + 1) ] # construct a list of all paths for all years to use for open_mfdataset - paths_per_year = [os.path.join(era5_path,fname,year) for year in years] + paths_per_year = [os.path.join(era5_path, fname, year) for year in years] all_files = [] for path in paths_per_year: # Use glob to find all files that match the pattern @@ -2173,7 +2262,7 @@ def __init__( @property def coords(self): """ - + This function: Allows us to call the self.coords for use in the xesmf.Regridder in the regrid_tides function. self.coords gives us the subset of the hgrid based on the orientation. @@ -2183,12 +2272,12 @@ def coords(self): Returns: xr.Dataset: The correct coordinate space for the orientation - General Description: + General Description: This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: - Converted code for RM6 segment class - Implemented Horizontal Subsetting - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) - + Original Code was sourced from: Author(s): GFDL, James Simkins, Rob Cermak, etc.. @@ -2196,47 +2285,52 @@ def coords(self): Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" Version: N/A Type: Python Functions, Source Code - Web Address: https://github.com/jsimkins2/nwa25 + Web Address: https://github.com/jsimkins2/nwa25 """ # Rename nxp and nyp to locations - if self.orientation == 'south': - rcoord = xr.Dataset({ - 'lon': self.hgrid['x'].isel(nyp=0), - 'lat': self.hgrid['y'].isel(nyp=0), - 'angle': self.hgrid['angle_dx'].isel(nyp=0) - }) - rcoord = rcoord.rename_dims({'nxp': 'locations'}) - elif self.orientation == 'north': - rcoord = xr.Dataset({ - 'lon': self.hgrid['x'].isel(nyp=-1), - 'lat': self.hgrid['y'].isel(nyp=-1), - 'angle': self.hgrid['angle_dx'].isel(nyp=-1) - }) - rcoord = rcoord.rename_dims({'nxp': 'locations'}) - elif self.orientation == 'west': - rcoord = xr.Dataset({ - 'lon': self.hgrid['x'].isel(nxp=0), - 'lat': self.hgrid['y'].isel(nxp=0), - 'angle': self.hgrid['angle_dx'].isel(nxp=0) - }) - rcoord = rcoord.rename_dims({'nyp': 'locations'}) - elif self.orientation == 'east': - rcoord = xr.Dataset({ - 'lon': self.hgrid['x'].isel(nxp=-1), - 'lat': self.hgrid['y'].isel(nxp=-1), - 'angle': self.hgrid['angle_dx'].isel(nxp=-1) - }) - rcoord = rcoord.rename_dims({'nyp': 'locations'}) + if self.orientation == "south": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nyp=0), + "lat": self.hgrid["y"].isel(nyp=0), + "angle": self.hgrid["angle_dx"].isel(nyp=0), + } + ) + rcoord = rcoord.rename_dims({"nxp": "locations"}) + elif self.orientation == "north": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nyp=-1), + "lat": self.hgrid["y"].isel(nyp=-1), + "angle": self.hgrid["angle_dx"].isel(nyp=-1), + } + ) + rcoord = rcoord.rename_dims({"nxp": "locations"}) + elif self.orientation == "west": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nxp=0), + "lat": self.hgrid["y"].isel(nxp=0), + "angle": self.hgrid["angle_dx"].isel(nxp=0), + } + ) + rcoord = rcoord.rename_dims({"nyp": "locations"}) + elif self.orientation == "east": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nxp=-1), + "lat": self.hgrid["y"].isel(nxp=-1), + "angle": self.hgrid["angle_dx"].isel(nxp=-1), + } + ) + rcoord = rcoord.rename_dims({"nyp": "locations"}) # Make lat and lon coordinates - rcoord = rcoord.assign_coords( - lat=rcoord['lat'], - lon=rcoord['lon'] - ) + rcoord = rcoord.assign_coords(lat=rcoord["lat"], lon=rcoord["lon"]) return rcoord - + def rectangular_brushcut(self): """ Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary @@ -2554,8 +2648,9 @@ def rectangular_brushcut(self): return segment_out, encoding_dict - def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, - method='nearest_s2d', periodic=False): + def regrid_tides( + self, tpxo_v, tpxo_u, tpxo_h, times, method="nearest_s2d", periodic=False + ): """ This function: Regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. @@ -2571,12 +2666,12 @@ def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, Returns: *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' - General Description: + General Description: This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: - Converted code for RM6 segment class - Implemented Horizontal Subsetting - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) - + Original Code was sourced from: Author(s): GFDL, James Simkins, Rob Cermak, etc.. @@ -2584,70 +2679,71 @@ def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" Version: N/A Type: Python Functions, Source Code - Web Address: https://github.com/jsimkins2/nwa25 + Web Address: https://github.com/jsimkins2/nwa25 """ ########## Tidal Elevation: Horizontally interpolate elevation components ############ regrid = xe.Regridder( - tpxo_h[['lon', 'lat', 'hRe']], + tpxo_h[["lon", "lat", "hRe"]], self.coords, - method='nearest_s2d', + method="nearest_s2d", locstream_out=True, periodic=False, - filename=os.path.join(self.outfolder,"forcing", f'regrid_{self.segment_name}_tidal_elev.nc'), - reuse_weights=False + filename=os.path.join( + self.outfolder, "forcing", f"regrid_{self.segment_name}_tidal_elev.nc" + ), + reuse_weights=False, ) - redest = regrid(tpxo_h[['lon', 'lat', 'hRe']]) - imdest = regrid(tpxo_h[['lon', 'lat', 'hIm']]) + redest = regrid(tpxo_h[["lon", "lat", "hRe"]]) + imdest = regrid(tpxo_h[["lon", "lat", "hIm"]]) # Fill missing data. # Need to do this first because complex would get converted to real - redest = redest.ffill(dim="locations", limit=None)['hRe'] - imdest = imdest.ffill(dim="locations", limit=None)['hIm'] + redest = redest.ffill(dim="locations", limit=None)["hRe"] + imdest = imdest.ffill(dim="locations", limit=None)["hIm"] # Convert complex cplex = redest + 1j * imdest # Convert to real amplitude and phase. - ds_ap = xr.Dataset({ - f'zamp_{self.segment_name}': np.abs(cplex) - }) + ds_ap = xr.Dataset({f"zamp_{self.segment_name}": np.abs(cplex)}) # np.angle doesn't return dataarray - ds_ap[f'zphase_{self.segment_name}'] = (('constituent', 'locations'), -1 * np.angle(cplex)) # radians + ds_ap[f"zphase_{self.segment_name}"] = ( + ("constituent", "locations"), + -1 * np.angle(cplex), + ) # radians # Add time coordinate and transpose so that time is first, # so that it can be the unlimited dimension ds_ap, _ = xr.broadcast(ds_ap, times) - ds_ap = ds_ap.transpose('time', 'constituent', 'locations') - + ds_ap = ds_ap.transpose("time", "constituent", "locations") + self.encode_tidal_files_and_output(ds_ap, "tz") - self.encode_tidal_files_and_output(ds_ap, 'tz') - ########### Regrid Tidal Velocity ###################### regrid_u = xe.Regridder( - tpxo_u[['lon', 'lat', 'uRe']], + tpxo_u[["lon", "lat", "uRe"]], self.coords, method=method, locstream_out=True, periodic=periodic, - reuse_weights=False + reuse_weights=False, ) regrid_v = xe.Regridder( - tpxo_v[['lon', 'lat', 'vRe']], + tpxo_v[["lon", "lat", "vRe"]], self.coords, method=method, locstream_out=True, periodic=periodic, - reuse_weights=False + reuse_weights=False, ) # Interpolate each real and imaginary parts to segment. - uredest = regrid_u(tpxo_u[['lon', 'lat', 'uRe']])['uRe'] - uimdest = regrid_u(tpxo_u[['lon', 'lat', 'uIm']])['uIm'] - vredest = regrid_v(tpxo_v[['lon', 'lat', 'vRe']])['vRe'] - vimdest = regrid_v(tpxo_v[['lon', 'lat', 'vIm']])['vIm'] + uredest = regrid_u(tpxo_u[["lon", "lat", "uRe"]])["uRe"] + uimdest = regrid_u(tpxo_u[["lon", "lat", "uIm"]])["uIm"] + vredest = regrid_v(tpxo_v[["lon", "lat", "vRe"]])["vRe"] + vimdest = regrid_v(tpxo_v[["lon", "lat", "vIm"]])["vIm"] # Fill missing data. # Need to do this first because complex would get converted to real @@ -2665,35 +2761,37 @@ def regrid_tides(self, tpxo_v, tpxo_u, tpxo_h, times, # and convert ellipse back to amplitude and phase. SEMA, ECC, INC, PHA = ap2ep(ucplex, vcplex) - - # Rotate to the model grid by adjusting the inclination. # Requries that angle is in radians. ua, va, up, vp = ep2ap(SEMA, ECC, INC, PHA) - ds_ap = xr.Dataset({ - f'uamp_{self.segment_name}': ua, - f'vamp_{self.segment_name}': va - }) + ds_ap = xr.Dataset( + {f"uamp_{self.segment_name}": ua, f"vamp_{self.segment_name}": va} + ) # up, vp aren't dataarrays - ds_ap[f'uphase_{self.segment_name}'] = (('constituent', 'locations'), up) # radians - ds_ap[f'vphase_{self.segment_name}'] = (('constituent', 'locations'), vp) # radians + ds_ap[f"uphase_{self.segment_name}"] = ( + ("constituent", "locations"), + up, + ) # radians + ds_ap[f"vphase_{self.segment_name}"] = ( + ("constituent", "locations"), + vp, + ) # radians ds_ap, _ = xr.broadcast(ds_ap, times) # Need to transpose so that time is first, # so that it can be the unlimited dimension - ds_ap = ds_ap.transpose('time', 'constituent', 'locations') + ds_ap = ds_ap.transpose("time", "constituent", "locations") # Some things may have become missing during the transformation ds_ap = ds_ap.ffill(dim="locations", limit=None) - self.encode_tidal_files_and_output(ds_ap, 'tu') - + self.encode_tidal_files_and_output(ds_ap, "tu") return - + def encode_tidal_files_and_output(self, ds, filename): """ This function: @@ -2709,12 +2807,12 @@ def encode_tidal_files_and_output(self, ds, filename): Returns: *.nc files: Regridded [FILENAME] files in 'self.outfolder/forcing/[filename]_[segmentname].nc' - General Description: + General Description: This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: - Converted code for RM6 segment class - Implemented Horizontal Subsetting - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) - + Original Code was sourced from: Author(s): GFDL, James Simkins, Rob Cermak, etc.. @@ -2722,57 +2820,54 @@ def encode_tidal_files_and_output(self, ds, filename): Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" Version: N/A Type: Python Functions, Source Code - Web Address: https://github.com/jsimkins2/nwa25 + Web Address: https://github.com/jsimkins2/nwa25 """ ## Expand Tidal Dimensions ## - if 'z' in ds.coords or 'constituent' in ds.dims: + if "z" in ds.coords or "constituent" in ds.dims: offset = 0 else: offset = 1 - if self.orientation in ['south', 'north']: - ds = ds.expand_dims(f'ny_{self.segment_name}', 2-offset) - elif self.orientation in ['west', 'east']: - ds = ds.expand_dims(f'nx_{self.segment_name}', 3-offset) + if self.orientation in ["south", "north"]: + ds = ds.expand_dims(f"ny_{self.segment_name}", 2 - offset) + elif self.orientation in ["west", "east"]: + ds = ds.expand_dims(f"nx_{self.segment_name}", 3 - offset) ## Rename Tidal Dimensions ## - ds = ds.rename({ - 'lon': f'lon_{self.segment_name}', - 'lat': f'lat_{self.segment_name}' - }) - if 'z' in ds.coords: - ds = ds.rename({ - 'z': f'nz_{self.segment_name}' - }) - if self.orientation in ['south', 'north']: - ds = ds.rename({'locations': f'nx_{self.segment_name}'}) - elif self.orientation in ['west', 'east']: - ds = ds.rename({'locations': f'ny_{self.segment_name}'}) - + ds = ds.rename( + {"lon": f"lon_{self.segment_name}", "lat": f"lat_{self.segment_name}"} + ) + if "z" in ds.coords: + ds = ds.rename({"z": f"nz_{self.segment_name}"}) + if self.orientation in ["south", "north"]: + ds = ds.rename({"locations": f"nx_{self.segment_name}"}) + elif self.orientation in ["west", "east"]: + ds = ds.rename({"locations": f"ny_{self.segment_name}"}) + ## Perform Encoding ## for v in ds: - ds[v].encoding['_FillValue']= 1.0e20 - fname = f'{filename}_{self.segment_name}.nc' + ds[v].encoding["_FillValue"] = 1.0e20 + fname = f"{filename}_{self.segment_name}.nc" # Set format and attributes for coordinates, including time if it does not already have calendar attribute # (may change this to detect whether time is a time type or a float). # Need to include the fillvalue or it will be back to nan encoding = { - 'time': dict(_FillValue=1.0e20), - f'lon_{self.segment_name}': dict(dtype='float64', _FillValue=1.0e20), - f'lat_{self.segment_name}': dict(dtype='float64', _FillValue=1.0e20) + "time": dict(_FillValue=1.0e20), + f"lon_{self.segment_name}": dict(dtype="float64", _FillValue=1.0e20), + f"lat_{self.segment_name}": dict(dtype="float64", _FillValue=1.0e20), } - if 'calendar' not in ds['time'].attrs and 'modulo' not in ds['time'].attrs: - encoding.update({'time': dict(dtype='float64', calendar='gregorian', _FillValue=1.0e20)}) + if "calendar" not in ds["time"].attrs and "modulo" not in ds["time"].attrs: + encoding.update( + {"time": dict(dtype="float64", calendar="gregorian", _FillValue=1.0e20)} + ) ## Export Files ## ds.to_netcdf( - os.path.join(self.outfolder,"forcing", fname), - engine='netcdf4', + os.path.join(self.outfolder, "forcing", fname), + engine="netcdf4", encoding=encoding, - unlimited_dims='time' + unlimited_dims="time", ) return - - \ No newline at end of file diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index 7bb0caa7..ed70266d 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -178,128 +178,134 @@ def quadrilateral_areas(lat, lon, R=1): coords[:-1, :-1, :], coords[:-1, 1:, :], coords[1:, 1:, :], coords[1:, :-1, :] ) + def ap2ep(uc, vc): - """Convert complex tidal u and v to tidal ellipse. - Adapted from ap2ep.m for matlab - Original copyright notice: - %Authorship Copyright: - % - % The author retains the copyright of this program, while you are welcome - % to use and distribute it as long as you credit the author properly and respect - % the program name itself. Particularly, you are expected to retain the original - % author's name in this original version or any of its modified version that - % you might make. You are also expected not to essentially change the name of - % the programs except for adding possible extension for your own version you - % might create, e.g. ap2ep_xx is acceptable. Any suggestions are welcome and - % enjoy my program(s)! - % - % - %Author Info: - %_______________________________________________________________________ - % Zhigang Xu, Ph.D. - % (pronounced as Tsi Gahng Hsu) - % Research Scientist - % Coastal Circulation - % Bedford Institute of Oceanography - % 1 Challenge Dr. - % P.O. Box 1006 Phone (902) 426-2307 (o) - % Dartmouth, Nova Scotia Fax (902) 426-7827 - % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca - %_______________________________________________________________________ - % - % Release Date: Nov. 2000, Revised on May. 2002 to adopt Foreman's northern semi - % major axis convention. - - Args: - uc: complex tidal u velocity - vc: complex tidal v velocity - - Returns: - (semi-major axis, eccentricity, inclination [radians], phase [radians]) - """ - wp = (uc + 1j * vc) / 2.0 - wm = np.conj(uc - 1j * vc) / 2.0 - - Wp = np.abs(wp) - Wm = np.abs(wm) - THETAp = np.angle(wp) - THETAm = np.angle(wm) - - SEMA = Wp + Wm - SEMI = Wp - Wm - ECC = SEMI / SEMA - PHA = (THETAm - THETAp) / 2.0 - INC = (THETAm + THETAp) / 2.0 - - return SEMA, ECC, INC, PHA + """Convert complex tidal u and v to tidal ellipse. + Adapted from ap2ep.m for matlab + Original copyright notice: + %Authorship Copyright: + % + % The author retains the copyright of this program, while you are welcome + % to use and distribute it as long as you credit the author properly and respect + % the program name itself. Particularly, you are expected to retain the original + % author's name in this original version or any of its modified version that + % you might make. You are also expected not to essentially change the name of + % the programs except for adding possible extension for your own version you + % might create, e.g. ap2ep_xx is acceptable. Any suggestions are welcome and + % enjoy my program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + % Release Date: Nov. 2000, Revised on May. 2002 to adopt Foreman's northern semi + % major axis convention. + + Args: + uc: complex tidal u velocity + vc: complex tidal v velocity + + Returns: + (semi-major axis, eccentricity, inclination [radians], phase [radians]) + """ + wp = (uc + 1j * vc) / 2.0 + wm = np.conj(uc - 1j * vc) / 2.0 + + Wp = np.abs(wp) + Wm = np.abs(wm) + THETAp = np.angle(wp) + THETAm = np.angle(wm) + + SEMA = Wp + Wm + SEMI = Wp - Wm + ECC = SEMI / SEMA + PHA = (THETAm - THETAp) / 2.0 + INC = (THETAm + THETAp) / 2.0 + + return SEMA, ECC, INC, PHA + def ep2ap(SEMA, ECC, INC, PHA): - """Convert tidal ellipse to real u and v amplitude and phase. - Adapted from ep2ap.m for matlab. - Original copyright notice: - %Authorship Copyright: - % - % The author of this program retains the copyright of this program, while - % you are welcome to use and distribute this program as long as you credit - % the author properly and respect the program name itself. Particularly, - % you are expected to retain the original author's name in this original - % version of the program or any of its modified version that you might make. - % You are also expected not to essentially change the name of the programs - % except for adding possible extension for your own version you might create, - % e.g. app2ep_xx is acceptable. Any suggestions are welcome and enjoy my - % program(s)! - % - % - %Author Info: - %_______________________________________________________________________ - % Zhigang Xu, Ph.D. - % (pronounced as Tsi Gahng Hsu) - % Research Scientist - % Coastal Circulation - % Bedford Institute of Oceanography - % 1 Challenge Dr. - % P.O. Box 1006 Phone (902) 426-2307 (o) - % Dartmouth, Nova Scotia Fax (902) 426-7827 - % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca - %_______________________________________________________________________ - % - %Release Date: Nov. 2000 - - Args: - SEMA: semi-major axis - ECC: eccentricity - INC: inclination [radians] - PHA: phase [radians] - - Returns: - (u amplitude, u phase [radians], v amplitude, v phase [radians]) - - """ - Wp = (1 + ECC) / 2. * SEMA - Wm = (1 - ECC) / 2. * SEMA - THETAp = INC - PHA - THETAm = INC + PHA - - wp = Wp * np.exp(1j * THETAp) - wm = Wm * np.exp(1j * THETAm) - - cu = wp + np.conj(wm) - cv = -1j * (wp - np.conj(wm)) - - ua = np.abs(cu) - va = np.abs(cv) - up = -np.angle(cu) - vp = -np.angle(cv) - - return ua, va, up, vp + """Convert tidal ellipse to real u and v amplitude and phase. + Adapted from ep2ap.m for matlab. + Original copyright notice: + %Authorship Copyright: + % + % The author of this program retains the copyright of this program, while + % you are welcome to use and distribute this program as long as you credit + % the author properly and respect the program name itself. Particularly, + % you are expected to retain the original author's name in this original + % version of the program or any of its modified version that you might make. + % You are also expected not to essentially change the name of the programs + % except for adding possible extension for your own version you might create, + % e.g. app2ep_xx is acceptable. Any suggestions are welcome and enjoy my + % program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + %Release Date: Nov. 2000 + + Args: + SEMA: semi-major axis + ECC: eccentricity + INC: inclination [radians] + PHA: phase [radians] + + Returns: + (u amplitude, u phase [radians], v amplitude, v phase [radians]) + + """ + Wp = (1 + ECC) / 2.0 * SEMA + Wm = (1 - ECC) / 2.0 * SEMA + THETAp = INC - PHA + THETAm = INC + PHA + + wp = Wp * np.exp(1j * THETAp) + wm = Wm * np.exp(1j * THETAm) + + cu = wp + np.conj(wm) + cv = -1j * (wp - np.conj(wm)) + + ua = np.abs(cu) + va = np.abs(cv) + up = -np.angle(cu) + vp = -np.angle(cv) + + return ua, va, up, vp + def find_roughly_nearest_ny_nx(lat, lon, ds): """ - Accepts a lat lon and returns a ROUGH closest ny,nx. in ds + Accepts a lat lon and returns a ROUGH closest ny,nx. in ds """ - ny = (np.abs(ds.lat.values - lat)).argmin(axis=1)[0] # We're looking for an nx, I know it's not exact, but this works - nx = (np.abs(ds.lon.values - lon)).argmin(axis=0)[0] - return ny,nx + ny = (np.abs(ds.lat.values - lat)).argmin(axis=1)[ + 0 + ] # We're looking for an nx, I know it's not exact, but this works + nx = (np.abs(ds.lon.values - lon)).argmin(axis=0)[0] + return ny, nx + def convert_lon_180_to_360(lon): """ From eabbc8c9fbd9162079e052b641d55c9a4a76df45 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Wed, 25 Sep 2024 15:50:02 -0600 Subject: [PATCH 28/81] Officially change boundary function names to verb names --- demos/reanalysis-forced.ipynb | 2 +- regional_mom6/regional_mom6.py | 31 ------------------------------- tests/test_expt_class.py | 2 +- 3 files changed, 2 insertions(+), 33 deletions(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index 9f1dcf2d..df57fe3d 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -301,7 +301,7 @@ " ) \n", "\n", "# Set up the four boundary conditions. Remember that in the glorys_path, we have four boundary files names north_unprocessed.nc etc. \n", - "expt.rectangular_boundaries(\n", + "expt.setup_ocean_state_rectangular_boundaries(\n", " glorys_path,\n", " ocean_varnames,\n", " boundaries = [\"south\", \"north\", \"west\", \"east\"],\n", diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d5efc7a4..d5173b18 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1053,25 +1053,6 @@ def get_glorys_rectangular( ) return - def rectangular_boundaries( - self, - raw_boundaries_path, - varnames, - boundaries=["south", "north", "west", "east"], - arakawa_grid="A", - ): - warnings.filterwarnings("default") # Set warnings back to on - warnings.warn( - 'The rectangular_boundaries function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with "setup_ocean_state_rectangular_boundaries"' - ) - warnings.filterwarnings("ignore") # Set warnings back off - return self.setup_ocean_state_rectangular_boundaries( - raw_boundaries_path, - varnames, - boundaries=boundaries, - arakawa_grid=arakawa_grid, - ) - def setup_ocean_state_rectangular_boundaries( self, raw_boundaries_path, @@ -1124,18 +1105,6 @@ def setup_ocean_state_rectangular_boundaries( arakawa_grid=arakawa_grid, ) - def simple_boundary( - self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" - ): - warnings.filterwarnings("default") # Set warnings back to on - warnings.warn( - 'The simple_boundary function has been changed in favor of a verb format, more description, and to accomodate tides. Drop-in replace with "setup_ocean_state_simple_boundary"' - ) - warnings.filterwarnings("ignore") # Turn warnings off - return self.setup_ocean_state_simple_boundary( - path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" - ) - def setup_ocean_state_simple_boundary( self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" ): diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index a601102f..292448e6 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -470,4 +470,4 @@ def test_rectangular_boundaries( "tracers": {"temp": "temp", "salt": "salt"}, } - expt.rectangular_boundaries(tmp_path, varnames, ["east"]) + expt.setup_ocean_state_rectangular_boundaries(tmp_path, varnames, ["east"]) From 5b78b7cba259266a7dbb892f38503a8009fe9901 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 11:09:59 -0600 Subject: [PATCH 29/81] Minor debugging --- regional_mom6/regional_mom6.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d5173b18..87f6b859 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -497,6 +497,7 @@ def __init__( repeat_year_forcing=False, read_existing_grids=False, minimum_depth=4, + tidal_constituents = [], ): ## in case list was given, convert to tuples self.longitude_extent = tuple(longitude_extent) @@ -525,6 +526,7 @@ def __init__( self.min_depth = ( minimum_depth # Minimum depth. Shallower water will be masked out. ) + self.tidal_constituents = tidal_constituents if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -1152,7 +1154,7 @@ def setup_ocean_state_simple_boundary( return def setup_tides_rectangle_boundaries( - self, path_to_td, tidal_filename, tidal_constituents=[0] + self, path_to_td, tidal_filename, tidal_constituents="read_from_expt_init" ): """ This function: @@ -1196,15 +1198,16 @@ def setup_tides_rectangle_boundaries( ### Find Rough Horizontal Subset (with 0.5 Buffer)### - self.tidal_constituents = tidal_constituents + if tidal_constituents != "`read_from_expt_init`": + self.tidal_constituents = tidal_constituents tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) .isel(constituent=tidal_constituents) ) tidal_360_lon = [ - convert_lon_180_to_360(self.longitude_extent[0]), - convert_lon_180_to_360(self.longitude_extent[1]), + self.longitude_extent[0], + self.longitude_extent[1], ] ny0, nx0 = find_roughly_nearest_ny_nx( self.latitude_extent[0] - 0.5, tidal_360_lon[0] - 0.5, tpxo_h @@ -1722,7 +1725,7 @@ def cpu_layout(self, layout): return def setup_run_directory( - self, surface_forcing=None, using_payu=False, overwrite=False, with_tides=False + self, surface_forcing=None, using_payu=False, overwrite=False, with_tides_rectangular=False ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify @@ -1789,7 +1792,7 @@ def setup_run_directory( overwrite_run_dir = False # Check if we can implement tides - if with_tides: + if with_tides_rectangular: tidal_files_exist = any( "tidal" in filename for filename in os.listdir(os.path.join(self.mom_input_dir, "forcing")) @@ -1897,7 +1900,7 @@ def setup_run_directory( MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") MOM_input_dict["MINIMUM_DEPTH"] = float(self.min_depth) MOM_input_dict["NK"] = len(self.vgrid.zl.values) - if with_tides: + if with_tides_rectangular: MOM_input_dict["TIDES"] = "True" MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = len(self.tidal_constituents) MOM_input_dict["OBC_SEGMENT_001_DATA"] = ( From 5e44fba0feac13d49282ec1bb3cc6b47494197cf Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 11:11:31 -0600 Subject: [PATCH 30/81] Black formatting --- regional_mom6/regional_mom6.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 87f6b859..1c5e0681 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -497,7 +497,7 @@ def __init__( repeat_year_forcing=False, read_existing_grids=False, minimum_depth=4, - tidal_constituents = [], + tidal_constituents=[], ): ## in case list was given, convert to tuples self.longitude_extent = tuple(longitude_extent) @@ -1725,7 +1725,11 @@ def cpu_layout(self, layout): return def setup_run_directory( - self, surface_forcing=None, using_payu=False, overwrite=False, with_tides_rectangular=False + self, + surface_forcing=None, + using_payu=False, + overwrite=False, + with_tides_rectangular=False, ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify From f3cd80ce5ee48beb1fd6806e18ceea25d78884f5 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 11:15:59 -0600 Subject: [PATCH 31/81] Change function name for rect orientation --- regional_mom6/regional_mom6.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 1c5e0681..c3bf6326 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -152,7 +152,7 @@ def longitude_slicer(data, longitude_extent, longitude_coords): return data -def find_MOM6_orientation(input): +def find_MOM6_rectangular_orientation(input): """ Convert between MOM6 boundary and the specific segment number needed, or the inverse """ @@ -1101,7 +1101,7 @@ def setup_ocean_state_rectangular_boundaries( ), varnames, orientation, # The cardinal direction of the boundary - find_MOM6_orientation( + find_MOM6_rectangular_orientation( orientation ), # A number to identify the boundary; indexes from 1 arakawa_grid=arakawa_grid, @@ -1259,7 +1259,9 @@ def setup_tides_rectangle_boundaries( infile=None, # location of raw boundary outfolder=self.mom_input_dir, varnames=None, - segment_name="segment_{:03d}".format(find_MOM6_orientation(b)), + segment_name="segment_{:03d}".format( + find_MOM6_rectangular_orientation(b) + ), orientation=b, # orienataion startdate=self.date_range[0], repeat_year_forcing=self.repeat_year_forcing, From b38508dd6be4de4fe03317ea34276a111588630b Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 11:57:56 -0600 Subject: [PATCH 32/81] Remove Greek letters --- regional_mom6/regional_mom6.py | 52 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index c3bf6326..e26346de 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -84,11 +84,11 @@ def longitude_slicer(data, longitude_extent, longitude_coords): ## Find a corresponding value for the intended domain midpoint in our data. ## It's assumed that data has equally-spaced longitude values. - λ = data[lon].data - dλ = λ[1] - λ[0] + lons = data[lon].data + dlons = lons[1] - lons[0] assert np.allclose( - np.diff(λ), dλ * np.ones(np.size(λ) - 1) + np.diff(lons), dlons * np.ones(np.size(lons) - 1) ), "provided longitude coordinate must be uniformly spaced" for i in range(-1, 2, 1): @@ -343,10 +343,10 @@ def hyperbolictan_thickness_profile(nlayers, ratio, total_depth): return layer_thicknesses -def rectangular_hgrid(λ, φ): +def rectangular_hgrid(lons, lats): """ Construct a horizontal grid with all the metadata required by MOM6, based on - arrays of longitudes (``λ``) and latitudes (``φ``) on the supergrid. + arrays of longitudes (``lons``) and latitudes (``lats``) on the supergrid. Here, 'supergrid' refers to both cell edges and centres, meaning that there are twice as many points along each axis than for any individual field. @@ -356,40 +356,40 @@ def rectangular_hgrid(λ, φ): It is also assumed here that the longitude array values are uniformly spaced. - Ensure both ``λ`` and ``φ`` are monotonically increasing. + Ensure both ``lons`` and ``lats`` are monotonically increasing. Args: - λ (numpy.array): All longitude points on the supergrid. Must be uniformly spaced. - φ (numpy.array): All latitude points on the supergrid. + lons (numpy.array): All longitude points on the supergrid. Must be uniformly spaced. + lats (numpy.array): All latitude points on the supergrid. Returns: xarray.Dataset: An FMS-compatible horizontal grid (``hgrid``) that includes all required attributes. """ - assert np.all(np.diff(λ) > 0), "longitudes array λ must be monotonically increasing" - assert np.all(np.diff(φ) > 0), "latitudes array φ must be monotonically increasing" + assert np.all(np.diff(lons) > 0), "longitudes array lons must be monotonically increasing" + assert np.all(np.diff(lats) > 0), "latitudes array lats must be monotonically increasing" R = 6371e3 # mean radius of the Earth; https://en.wikipedia.org/wiki/Earth_radius # compute longitude spacing and ensure that longitudes are uniformly spaced - dλ = λ[1] - λ[0] + dlons = lons[1] - lons[0] assert np.allclose( - np.diff(λ), dλ * np.ones(np.size(λ) - 1) + np.diff(lons), dlons * np.ones(np.size(lons) - 1) ), "provided array of longitudes must be uniformly spaced" - # dx = R * cos(np.deg2rad(φ)) * np.deg2rad(dλ) / 2 + # dx = R * cos(np.deg2rad(lats)) * np.deg2rad(dlons) / 2 # Note: division by 2 because we're on the supergrid dx = np.broadcast_to( - R * np.cos(np.deg2rad(φ)) * np.deg2rad(dλ) / 2, - (λ.shape[0] - 1, φ.shape[0]), + R * np.cos(np.deg2rad(lats)) * np.deg2rad(dlons) / 2, + (lons.shape[0] - 1, lats.shape[0]), ).T - # dy = R * np.deg2rad(dφ) / 2 + # dy = R * np.deg2rad(dlats) / 2 # Note: division by 2 because we're on the supergrid - dy = np.broadcast_to(R * np.deg2rad(np.diff(φ)) / 2, (λ.shape[0], φ.shape[0] - 1)).T + dy = np.broadcast_to(R * np.deg2rad(np.diff(lats)) / 2, (lons.shape[0], lats.shape[0] - 1)).T - lon, lat = np.meshgrid(λ, φ) + lon, lat = np.meshgrid(lons, lats) area = quadrilateral_areas(lat, lon, R) @@ -572,14 +572,14 @@ def _make_hgrid(self): and in latitude. The latitudinal resolution is scaled with the cosine of the central - latitude of the domain, i.e., ``Δφ = cos(φ_central) * Δλ``, where ``Δλ`` + latitude of the domain, i.e., ``Δlats = cos(lats_central) * Δlons``, where ``Δlons`` is the longitudinal spacing. This way, for a sufficiently small domain, the linear distances between grid points are nearly identical: - ``Δx = R * cos(φ) * Δλ`` and ``Δy = R * Δφ = R * cos(φ_central) * Δλ`` - (here ``R`` is Earth's radius and ``φ``, ``φ_central``, ``Δλ``, and ``Δφ`` + ``Δx = R * cos(lats) * Δlons`` and ``Δy = R * Δlats = R * cos(lats_central) * Δlons`` + (here ``R`` is Earth's radius and ``lats``, ``lats_central``, ``Δlons``, and ``Δlats`` are all expressed in radians). - That is, if the domain is small enough that so that ``cos(φ_North_Side)`` - is not much different from ``cos(φ_South_Side)``, then ``Δx`` and ``Δy`` + That is, if the domain is small enough that so that ``cos(lats_North_Side)`` + is not much different from ``cos(lats_South_Side)``, then ``Δx`` and ``Δy`` are similar. Note: @@ -605,7 +605,7 @@ def _make_hgrid(self): if nx % 2 != 1: nx += 1 - λ = np.linspace( + lons = np.linspace( self.longitude_extent[0], self.longitude_extent[1], nx ) # longitudes in degrees @@ -626,11 +626,11 @@ def _make_hgrid(self): if ny % 2 != 1: ny += 1 - φ = np.linspace( + lats = np.linspace( self.latitude_extent[0], self.latitude_extent[1], ny ) # latitudes in degrees - hgrid = rectangular_hgrid(λ, φ) + hgrid = rectangular_hgrid(lons, lats) hgrid.to_netcdf(self.mom_input_dir / "hgrid.nc") return hgrid From 38045313a27ab854896ee5aaa47ba9fb8a3f3c96 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 11:59:21 -0600 Subject: [PATCH 33/81] Black Formatting --- regional_mom6/regional_mom6.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index e26346de..a5e5afe6 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -366,8 +366,12 @@ def rectangular_hgrid(lons, lats): xarray.Dataset: An FMS-compatible horizontal grid (``hgrid``) that includes all required attributes. """ - assert np.all(np.diff(lons) > 0), "longitudes array lons must be monotonically increasing" - assert np.all(np.diff(lats) > 0), "latitudes array lats must be monotonically increasing" + assert np.all( + np.diff(lons) > 0 + ), "longitudes array lons must be monotonically increasing" + assert np.all( + np.diff(lats) > 0 + ), "latitudes array lats must be monotonically increasing" R = 6371e3 # mean radius of the Earth; https://en.wikipedia.org/wiki/Earth_radius @@ -387,7 +391,9 @@ def rectangular_hgrid(lons, lats): # dy = R * np.deg2rad(dlats) / 2 # Note: division by 2 because we're on the supergrid - dy = np.broadcast_to(R * np.deg2rad(np.diff(lats)) / 2, (lons.shape[0], lats.shape[0] - 1)).T + dy = np.broadcast_to( + R * np.deg2rad(np.diff(lats)) / 2, (lons.shape[0], lats.shape[0] - 1) + ).T lon, lat = np.meshgrid(lons, lats) From 66bb465ca69f415b140da7926f279299fdd62990 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 16:23:42 -0600 Subject: [PATCH 34/81] Replace rectangular brushcut coords with GFDL self.coords --- regional_mom6/regional_mom6.py | 154 ++++++++++++++++----------------- 1 file changed, 75 insertions(+), 79 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index a5e5afe6..618de52a 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -503,7 +503,7 @@ def __init__( repeat_year_forcing=False, read_existing_grids=False, minimum_depth=4, - tidal_constituents=[], + tidal_constituents=[0], ): ## in case list was given, convert to tuples self.longitude_extent = tuple(longitude_extent) @@ -1204,12 +1204,12 @@ def setup_tides_rectangle_boundaries( ### Find Rough Horizontal Subset (with 0.5 Buffer)### - if tidal_constituents != "`read_from_expt_init`": + if tidal_constituents != 'read_from_expt_init': self.tidal_constituents = tidal_constituents tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) - .isel(constituent=tidal_constituents) + .isel(constituent=self.tidal_constituents) ) tidal_360_lon = [ self.longitude_extent[0], @@ -1231,7 +1231,7 @@ def setup_tides_rectangle_boundaries( tpxo_u = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) - .isel(constituent=tidal_constituents, **horizontal_subset) + .isel(constituent=self.tidal_constituents, **horizontal_subset) ) tpxo_u["ua"] *= 0.01 # convert to m/s u = tpxo_u["ua"] * np.exp(-1j * np.radians(tpxo_u["up"])) @@ -1240,7 +1240,7 @@ def setup_tides_rectangle_boundaries( tpxo_v = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) - .isel(constituent=tidal_constituents, **horizontal_subset) + .isel(constituent=self.tidal_constituents, **horizontal_subset) ) tpxo_v["va"] *= 0.01 # convert to m/s v = tpxo_v["va"] * np.exp(-1j * np.radians(tpxo_v["vp"])) @@ -2281,7 +2281,11 @@ def coords(self): "angle": self.hgrid["angle_dx"].isel(nyp=0), } ) - rcoord = rcoord.rename_dims({"nxp": "locations"}) + rcoord = rcoord.rename_dims({"nxp": f"nx_{self.segment_name}"}) + rcoord.attrs["perpendicular"] = "ny" + rcoord.attrs["parallel"] = "nx" + rcoord.attrs["axis_to_expand"] = 2 ## Need to keep track of which axis the 'main' coordinate corresponds to for rectangular_brushcut on when re-adding the 'secondary' axis + rcoord.attrs["locations_name"] = f"nx_{self.segment_name}" # Legacy name of nx_... was locations. This provides a clear transform in regrid_tides elif self.orientation == "north": rcoord = xr.Dataset( { @@ -2290,7 +2294,11 @@ def coords(self): "angle": self.hgrid["angle_dx"].isel(nyp=-1), } ) - rcoord = rcoord.rename_dims({"nxp": "locations"}) + rcoord = rcoord.rename_dims({"nxp": f"nx_{self.segment_name}"}) + rcoord.attrs["perpendicular"] = "ny" + rcoord.attrs["parallel"] = "nx" + rcoord.attrs["axis_to_expand"] = 2 + rcoord.attrs["locations_name"] = f"nx_{self.segment_name}" elif self.orientation == "west": rcoord = xr.Dataset( { @@ -2299,7 +2307,11 @@ def coords(self): "angle": self.hgrid["angle_dx"].isel(nxp=0), } ) - rcoord = rcoord.rename_dims({"nyp": "locations"}) + rcoord = rcoord.rename_dims({"nyp": f"ny_{self.segment_name}"}) + rcoord.attrs["perpendicular"] = "nx" + rcoord.attrs["parallel"] = "ny" + rcoord.attrs["axis_to_expand"] = 3 + rcoord.attrs["locations_name"] = f"ny_{self.segment_name}" elif self.orientation == "east": rcoord = xr.Dataset( { @@ -2308,8 +2320,12 @@ def coords(self): "angle": self.hgrid["angle_dx"].isel(nxp=-1), } ) - rcoord = rcoord.rename_dims({"nyp": "locations"}) - + rcoord = rcoord.rename_dims({"nyp": f"ny_{self.segment_name}"}) + rcoord.attrs["perpendicular"] = "nx" + rcoord.attrs["parallel"] = "ny" + rcoord.attrs["axis_to_expand"] = 3 + rcoord.attrs["locations_name"] = f"ny_{self.segment_name}" + # Make lat and lon coordinates rcoord = rcoord.assign_coords(lat=rcoord["lat"], lon=rcoord["lon"]) @@ -2320,45 +2336,6 @@ def rectangular_brushcut(self): Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary is a simple Northern, Southern, Eastern, or Western boundary. """ - if self.orientation == "north": - self.hgrid_seg = self.hgrid.isel(nyp=[-1]) - self.perpendicular = "ny" - self.parallel = "nx" - - if self.orientation == "south": - self.hgrid_seg = self.hgrid.isel(nyp=[0]) - self.perpendicular = "ny" - self.parallel = "nx" - - if self.orientation == "east": - self.hgrid_seg = self.hgrid.isel(nxp=[-1]) - self.perpendicular = "nx" - self.parallel = "ny" - - if self.orientation == "west": - self.hgrid_seg = self.hgrid.isel(nxp=[0]) - self.perpendicular = "nx" - self.parallel = "ny" - - ## Need to keep track of which axis the 'main' coordinate corresponds to for later on when re-adding the 'secondary' axis - if self.perpendicular == "ny": - self.axis_to_expand = 2 - else: - self.axis_to_expand = 3 - - ## Grid for interpolating our fields - self.interp_grid = xr.Dataset( - { - "lat": ( - [f"{self.parallel}_{self.segment_name}"], - self.hgrid_seg.y.squeeze().data, - ), - "lon": ( - [f"{self.parallel}_{self.segment_name}"], - self.hgrid_seg.x.squeeze().data, - ), - } - ).set_coords(["lat", "lon"]) rawseg = xr.open_dataset(self.infile, decode_times=False, engine="netcdf4") @@ -2367,7 +2344,7 @@ def rectangular_brushcut(self): ## In this case velocities and tracers all on same points regridder = xe.Regridder( rawseg[self.u], - self.interp_grid, + self.coords, "bilinear", locstream_out=True, reuse_weights=False, @@ -2390,7 +2367,7 @@ def rectangular_brushcut(self): ## All tracers on one grid, all velocities on another regridder_velocity = xe.Regridder( rawseg[self.u].rename({self.xq: "lon", self.yq: "lat"}), - self.interp_grid, + self.coords, "bilinear", locstream_out=True, reuse_weights=False, @@ -2400,7 +2377,7 @@ def rectangular_brushcut(self): regridder_tracer = xe.Regridder( rawseg[self.tracers["salt"]].rename({self.xh: "lon", self.yh: "lat"}), - self.interp_grid, + self.coords, "bilinear", locstream_out=True, reuse_weights=False, @@ -2427,7 +2404,7 @@ def rectangular_brushcut(self): ## All tracers on one grid, all velocities on another regridder_uvelocity = xe.Regridder( rawseg[self.u].rename({self.xq: "lon", self.yh: "lat"}), - self.interp_grid, + self.coords, "bilinear", locstream_out=True, reuse_weights=False, @@ -2437,7 +2414,7 @@ def rectangular_brushcut(self): regridder_vvelocity = xe.Regridder( rawseg[self.v].rename({self.xh: "lon", self.yq: "lat"}), - self.interp_grid, + self.coords, "bilinear", locstream_out=True, reuse_weights=False, @@ -2447,7 +2424,7 @@ def rectangular_brushcut(self): regridder_tracer = xe.Regridder( rawseg[self.tracers["salt"]].rename({self.xh: "lon", self.yh: "lat"}), - self.interp_grid, + self.coords, "bilinear", locstream_out=True, reuse_weights=False, @@ -2481,9 +2458,9 @@ def rectangular_brushcut(self): # fill in NaNs segment_out = ( segment_out.ffill(self.z) - .interpolate_na(f"{self.parallel}_{self.segment_name}") - .ffill(f"{self.parallel}_{self.segment_name}") - .bfill(f"{self.parallel}_{self.segment_name}") + .interpolate_na(f"{self.coords.attrs["parallel"]}_{self.segment_name}") + .ffill(f"{self.coords.attrs["parallel"]}_{self.segment_name}") + .bfill(f"{self.coords.attrs["parallel"]}_{self.segment_name}") ) time = np.arange( @@ -2547,7 +2524,7 @@ def rectangular_brushcut(self): ## Re-add the secondary dimension (even though it represents one value..) segment_out[v] = segment_out[v].expand_dims( - f"{self.perpendicular}_{self.segment_name}", axis=self.axis_to_expand + f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", axis=self.coords.attrs["axis_to_expand"] ) ## Add the layer thicknesses @@ -2592,23 +2569,42 @@ def rectangular_brushcut(self): segment_out[f"eta_{self.segment_name}"] = segment_out[ f"eta_{self.segment_name}" ].expand_dims( - f"{self.perpendicular}_{self.segment_name}", axis=self.axis_to_expand - 1 + f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", axis=self.coords.attrs["axis_to_expand"] - 1 ) # Overwrite the actual lat/lon values in the dimensions, replace with incrementing integers - segment_out[f"{self.parallel}_{self.segment_name}"] = np.arange( - segment_out[f"{self.parallel}_{self.segment_name}"].size + segment_out[f"{self.coords.attrs["parallel"]}_{self.segment_name}"] = np.arange( + segment_out[f"{self.coords.attrs["parallel"]}_{self.segment_name}"].size ) - segment_out[f"{self.perpendicular}_{self.segment_name}"] = [0] + segment_out[f"{self.coords.attrs["perpendicular"]}_{self.segment_name}"] = [0] + if self.orientation == "north": + self.hgrid_seg = self.hgrid.isel(nyp=[-1]) + self.perpendicular = "ny" + self.parallel = "nx" + + if self.orientation == "south": + self.hgrid_seg = self.hgrid.isel(nyp=[0]) + self.perpendicular = "ny" + self.parallel = "nx" + + if self.orientation == "east": + self.hgrid_seg = self.hgrid.isel(nxp=[-1]) + self.perpendicular = "nx" + self.parallel = "ny" + + if self.orientation == "west": + self.hgrid_seg = self.hgrid.isel(nxp=[0]) + self.perpendicular = "nx" + self.parallel = "ny" # Store actual lat/lon values here as variables rather than coordinates segment_out[f"lon_{self.segment_name}"] = ( [f"ny_{self.segment_name}", f"nx_{self.segment_name}"], - self.hgrid_seg.x.data, + self.coords.lon.expand_dims(dim="blank",axis=self.coords.attrs["axis_to_expand"] - 2).data, ) segment_out[f"lat_{self.segment_name}"] = ( [f"ny_{self.segment_name}", f"nx_{self.segment_name}"], - self.hgrid_seg.y.data, + self.coords.lon.expand_dims(dim="blank",axis=self.coords.attrs["axis_to_expand"] - 2).data, ) # Add units to the lat / lon to keep the `categorize_axis_from_units` checker happy @@ -2683,8 +2679,8 @@ def regrid_tides( # Fill missing data. # Need to do this first because complex would get converted to real - redest = redest.ffill(dim="locations", limit=None)["hRe"] - imdest = imdest.ffill(dim="locations", limit=None)["hIm"] + redest = redest.ffill(dim=self.coords.attrs["locations_name"], limit=None)["hRe"] + imdest = imdest.ffill(dim=self.coords.attrs["locations_name"], limit=None)["hIm"] # Convert complex cplex = redest + 1j * imdest @@ -2693,14 +2689,14 @@ def regrid_tides( ds_ap = xr.Dataset({f"zamp_{self.segment_name}": np.abs(cplex)}) # np.angle doesn't return dataarray ds_ap[f"zphase_{self.segment_name}"] = ( - ("constituent", "locations"), + ("constituent", self.coords.attrs["locations_name"]), -1 * np.angle(cplex), ) # radians # Add time coordinate and transpose so that time is first, # so that it can be the unlimited dimension ds_ap, _ = xr.broadcast(ds_ap, times) - ds_ap = ds_ap.transpose("time", "constituent", "locations") + ds_ap = ds_ap.transpose("time", "constituent", self.coords.attrs["locations_name"]) self.encode_tidal_files_and_output(ds_ap, "tz") @@ -2731,10 +2727,10 @@ def regrid_tides( # Fill missing data. # Need to do this first because complex would get converted to real - uredest = uredest.ffill(dim="locations", limit=None) - uimdest = uimdest.ffill(dim="locations", limit=None) - vredest = vredest.ffill(dim="locations", limit=None) - vimdest = vimdest.ffill(dim="locations", limit=None) + uredest = uredest.ffill(dim=self.coords.attrs["locations_name"], limit=None) + uimdest = uimdest.ffill(dim=self.coords.attrs["locations_name"], limit=None) + vredest = vredest.ffill(dim=self.coords.attrs["locations_name"], limit=None) + vimdest = vimdest.ffill(dim=self.coords.attrs["locations_name"], limit=None) # Convert to complex, remaining separate for u and v. ucplex = uredest + 1j * uimdest @@ -2755,11 +2751,11 @@ def regrid_tides( ) # up, vp aren't dataarrays ds_ap[f"uphase_{self.segment_name}"] = ( - ("constituent", "locations"), + ("constituent", self.coords.attrs["locations_name"]), up, ) # radians ds_ap[f"vphase_{self.segment_name}"] = ( - ("constituent", "locations"), + ("constituent", self.coords.attrs["locations_name"]), vp, ) # radians @@ -2767,10 +2763,10 @@ def regrid_tides( # Need to transpose so that time is first, # so that it can be the unlimited dimension - ds_ap = ds_ap.transpose("time", "constituent", "locations") + ds_ap = ds_ap.transpose("time", "constituent", self.coords.attrs["locations_name"]) # Some things may have become missing during the transformation - ds_ap = ds_ap.ffill(dim="locations", limit=None) + ds_ap = ds_ap.ffill(dim=self.coords.attrs["locations_name"], limit=None) self.encode_tidal_files_and_output(ds_ap, "tu") @@ -2826,9 +2822,9 @@ def encode_tidal_files_and_output(self, ds, filename): if "z" in ds.coords: ds = ds.rename({"z": f"nz_{self.segment_name}"}) if self.orientation in ["south", "north"]: - ds = ds.rename({"locations": f"nx_{self.segment_name}"}) + ds = ds.rename({self.coords.attrs["locations_name"]: f"nx_{self.segment_name}"}) elif self.orientation in ["west", "east"]: - ds = ds.rename({"locations": f"ny_{self.segment_name}"}) + ds = ds.rename({self.coords.attrs["locations_name"]: f"ny_{self.segment_name}"}) ## Perform Encoding ## for v in ds: From d16fc4c13c3afb786ca281f21bee4ba802db0a85 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 16:25:36 -0600 Subject: [PATCH 35/81] Black formatting --- regional_mom6/regional_mom6.py | 50 ++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 618de52a..96e8987c 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1204,7 +1204,7 @@ def setup_tides_rectangle_boundaries( ### Find Rough Horizontal Subset (with 0.5 Buffer)### - if tidal_constituents != 'read_from_expt_init': + if tidal_constituents != "read_from_expt_init": self.tidal_constituents = tidal_constituents tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) @@ -2284,8 +2284,12 @@ def coords(self): rcoord = rcoord.rename_dims({"nxp": f"nx_{self.segment_name}"}) rcoord.attrs["perpendicular"] = "ny" rcoord.attrs["parallel"] = "nx" - rcoord.attrs["axis_to_expand"] = 2 ## Need to keep track of which axis the 'main' coordinate corresponds to for rectangular_brushcut on when re-adding the 'secondary' axis - rcoord.attrs["locations_name"] = f"nx_{self.segment_name}" # Legacy name of nx_... was locations. This provides a clear transform in regrid_tides + rcoord.attrs["axis_to_expand"] = ( + 2 ## Need to keep track of which axis the 'main' coordinate corresponds to for rectangular_brushcut on when re-adding the 'secondary' axis + ) + rcoord.attrs["locations_name"] = ( + f"nx_{self.segment_name}" # Legacy name of nx_... was locations. This provides a clear transform in regrid_tides + ) elif self.orientation == "north": rcoord = xr.Dataset( { @@ -2325,7 +2329,7 @@ def coords(self): rcoord.attrs["parallel"] = "ny" rcoord.attrs["axis_to_expand"] = 3 rcoord.attrs["locations_name"] = f"ny_{self.segment_name}" - + # Make lat and lon coordinates rcoord = rcoord.assign_coords(lat=rcoord["lat"], lon=rcoord["lon"]) @@ -2524,7 +2528,8 @@ def rectangular_brushcut(self): ## Re-add the secondary dimension (even though it represents one value..) segment_out[v] = segment_out[v].expand_dims( - f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", axis=self.coords.attrs["axis_to_expand"] + f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", + axis=self.coords.attrs["axis_to_expand"], ) ## Add the layer thicknesses @@ -2569,7 +2574,8 @@ def rectangular_brushcut(self): segment_out[f"eta_{self.segment_name}"] = segment_out[ f"eta_{self.segment_name}" ].expand_dims( - f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", axis=self.coords.attrs["axis_to_expand"] - 1 + f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", + axis=self.coords.attrs["axis_to_expand"] - 1, ) # Overwrite the actual lat/lon values in the dimensions, replace with incrementing integers @@ -2600,11 +2606,15 @@ def rectangular_brushcut(self): # Store actual lat/lon values here as variables rather than coordinates segment_out[f"lon_{self.segment_name}"] = ( [f"ny_{self.segment_name}", f"nx_{self.segment_name}"], - self.coords.lon.expand_dims(dim="blank",axis=self.coords.attrs["axis_to_expand"] - 2).data, + self.coords.lon.expand_dims( + dim="blank", axis=self.coords.attrs["axis_to_expand"] - 2 + ).data, ) segment_out[f"lat_{self.segment_name}"] = ( [f"ny_{self.segment_name}", f"nx_{self.segment_name}"], - self.coords.lon.expand_dims(dim="blank",axis=self.coords.attrs["axis_to_expand"] - 2).data, + self.coords.lon.expand_dims( + dim="blank", axis=self.coords.attrs["axis_to_expand"] - 2 + ).data, ) # Add units to the lat / lon to keep the `categorize_axis_from_units` checker happy @@ -2679,8 +2689,12 @@ def regrid_tides( # Fill missing data. # Need to do this first because complex would get converted to real - redest = redest.ffill(dim=self.coords.attrs["locations_name"], limit=None)["hRe"] - imdest = imdest.ffill(dim=self.coords.attrs["locations_name"], limit=None)["hIm"] + redest = redest.ffill(dim=self.coords.attrs["locations_name"], limit=None)[ + "hRe" + ] + imdest = imdest.ffill(dim=self.coords.attrs["locations_name"], limit=None)[ + "hIm" + ] # Convert complex cplex = redest + 1j * imdest @@ -2696,7 +2710,9 @@ def regrid_tides( # Add time coordinate and transpose so that time is first, # so that it can be the unlimited dimension ds_ap, _ = xr.broadcast(ds_ap, times) - ds_ap = ds_ap.transpose("time", "constituent", self.coords.attrs["locations_name"]) + ds_ap = ds_ap.transpose( + "time", "constituent", self.coords.attrs["locations_name"] + ) self.encode_tidal_files_and_output(ds_ap, "tz") @@ -2763,7 +2779,9 @@ def regrid_tides( # Need to transpose so that time is first, # so that it can be the unlimited dimension - ds_ap = ds_ap.transpose("time", "constituent", self.coords.attrs["locations_name"]) + ds_ap = ds_ap.transpose( + "time", "constituent", self.coords.attrs["locations_name"] + ) # Some things may have become missing during the transformation ds_ap = ds_ap.ffill(dim=self.coords.attrs["locations_name"], limit=None) @@ -2822,9 +2840,13 @@ def encode_tidal_files_and_output(self, ds, filename): if "z" in ds.coords: ds = ds.rename({"z": f"nz_{self.segment_name}"}) if self.orientation in ["south", "north"]: - ds = ds.rename({self.coords.attrs["locations_name"]: f"nx_{self.segment_name}"}) + ds = ds.rename( + {self.coords.attrs["locations_name"]: f"nx_{self.segment_name}"} + ) elif self.orientation in ["west", "east"]: - ds = ds.rename({self.coords.attrs["locations_name"]: f"ny_{self.segment_name}"}) + ds = ds.rename( + {self.coords.attrs["locations_name"]: f"ny_{self.segment_name}"} + ) ## Perform Encoding ## for v in ds: From 127c7ec3910fe460f405fe6e7faca7777914d620 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Thu, 26 Sep 2024 16:33:32 -0600 Subject: [PATCH 36/81] Fix F-strings --- regional_mom6/regional_mom6.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 96e8987c..0b1d4c15 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -2462,9 +2462,9 @@ def rectangular_brushcut(self): # fill in NaNs segment_out = ( segment_out.ffill(self.z) - .interpolate_na(f"{self.coords.attrs["parallel"]}_{self.segment_name}") - .ffill(f"{self.coords.attrs["parallel"]}_{self.segment_name}") - .bfill(f"{self.coords.attrs["parallel"]}_{self.segment_name}") + .interpolate_na(f"{self.coords.attrs['parallel']}_{self.segment_name}") + .ffill(f"{self.coords.attrs['parallel']}_{self.segment_name}") + .bfill(f"{self.coords.attrs['parallel']}_{self.segment_name}") ) time = np.arange( @@ -2528,7 +2528,7 @@ def rectangular_brushcut(self): ## Re-add the secondary dimension (even though it represents one value..) segment_out[v] = segment_out[v].expand_dims( - f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", + f"{self.coords.attrs['perpendicular']}_{self.segment_name}", axis=self.coords.attrs["axis_to_expand"], ) @@ -2574,15 +2574,15 @@ def rectangular_brushcut(self): segment_out[f"eta_{self.segment_name}"] = segment_out[ f"eta_{self.segment_name}" ].expand_dims( - f"{self.coords.attrs["perpendicular"]}_{self.segment_name}", + f"{self.coords.attrs['perpendicular']}_{self.segment_name}", axis=self.coords.attrs["axis_to_expand"] - 1, ) # Overwrite the actual lat/lon values in the dimensions, replace with incrementing integers - segment_out[f"{self.coords.attrs["parallel"]}_{self.segment_name}"] = np.arange( - segment_out[f"{self.coords.attrs["parallel"]}_{self.segment_name}"].size + segment_out[f"{self.coords.attrs['parallel']}_{self.segment_name}"] = np.arange( + segment_out[f"{self.coords.attrs['parallel']}_{self.segment_name}"].size ) - segment_out[f"{self.coords.attrs["perpendicular"]}_{self.segment_name}"] = [0] + segment_out[f"{self.coords.attrs['perpendicular']}_{self.segment_name}"] = [0] if self.orientation == "north": self.hgrid_seg = self.hgrid.isel(nyp=[-1]) self.perpendicular = "ny" From 34be7e2971e1f3331eef8652143f357571aa3d5d Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 11:21:19 -0600 Subject: [PATCH 37/81] Add angled_grids testing for debugging --- tests/__init__.py | 0 tests/test_angled_grids.py | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_angled_grids.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_angled_grids.py b/tests/test_angled_grids.py new file mode 100644 index 00000000..a5564519 --- /dev/null +++ b/tests/test_angled_grids.py @@ -0,0 +1,49 @@ +import regional_mom6 as rmom6 +import os +from pathlib import Path + + + + +def test_angled_grids(): + """ + Test that the angled grid is correctly read in. + """ + expt_name = "nwa12_read_grids" + + latitude_extent = [16., 27] + longitude_extent = [192, 209] + + date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] + + ## Place where all your input files go + input_dir = Path(os.path.join("/","glade","u","home","manishrv","documents","nwa12_0.1","regional_mom_workflows","rm6",expt_name, "inputs")) + + ## Directory where you'll run the experiment from + run_dir = Path(os.path.join("/","glade","u","home","manishrv","documents","nwa12_0.1","regional_mom_workflows","rm6",expt_name, "run_files")) + for path in (run_dir, input_dir): + os.makedirs(str(path), exist_ok=True) + + ## User-1st, test if we can even read the angled nc files. + expt = rmom6.experiment( + longitude_extent = longitude_extent, + latitude_extent = latitude_extent, + date_range = date_range, + resolution = 0.05, + number_vertical_layers = 75, + layer_thickness_ratio = 10, + depth = 4500, + minimum_depth = 5, + mom_run_dir = run_dir, + mom_input_dir = input_dir, + toolpath_dir = "", + read_existing_grids = True) + + ## Dev-2nd, test if the segment.coords function can properly give us the angles from this grid, which is at least called by rectangular_boundaries. + + ## User-2nd, test our ocean state boundary conditions + + ## User-3rd, test our tides boundary conditions + + + \ No newline at end of file From 5efb3b0cdd471e31e1cf7898f89dd0cc76ddb2b1 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 11:28:30 -0600 Subject: [PATCH 38/81] black formatting --- tests/test_angled_grids.py | 68 ++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/tests/test_angled_grids.py b/tests/test_angled_grids.py index a5564519..7b43bd60 100644 --- a/tests/test_angled_grids.py +++ b/tests/test_angled_grids.py @@ -3,47 +3,71 @@ from pathlib import Path - - def test_angled_grids(): """ Test that the angled grid is correctly read in. """ expt_name = "nwa12_read_grids" - latitude_extent = [16., 27] + latitude_extent = [16.0, 27] longitude_extent = [192, 209] date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] - ## Place where all your input files go - input_dir = Path(os.path.join("/","glade","u","home","manishrv","documents","nwa12_0.1","regional_mom_workflows","rm6",expt_name, "inputs")) + ## Place where all your input files go + input_dir = Path( + os.path.join( + "/", + "glade", + "u", + "home", + "manishrv", + "documents", + "nwa12_0.1", + "regional_mom_workflows", + "rm6", + expt_name, + "inputs", + ) + ) ## Directory where you'll run the experiment from - run_dir = Path(os.path.join("/","glade","u","home","manishrv","documents","nwa12_0.1","regional_mom_workflows","rm6",expt_name, "run_files")) + run_dir = Path( + os.path.join( + "/", + "glade", + "u", + "home", + "manishrv", + "documents", + "nwa12_0.1", + "regional_mom_workflows", + "rm6", + expt_name, + "run_files", + ) + ) for path in (run_dir, input_dir): os.makedirs(str(path), exist_ok=True) ## User-1st, test if we can even read the angled nc files. expt = rmom6.experiment( - longitude_extent = longitude_extent, - latitude_extent = latitude_extent, - date_range = date_range, - resolution = 0.05, - number_vertical_layers = 75, - layer_thickness_ratio = 10, - depth = 4500, - minimum_depth = 5, - mom_run_dir = run_dir, - mom_input_dir = input_dir, - toolpath_dir = "", - read_existing_grids = True) + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=0.05, + number_vertical_layers=75, + layer_thickness_ratio=10, + depth=4500, + minimum_depth=5, + mom_run_dir=run_dir, + mom_input_dir=input_dir, + toolpath_dir="", + read_existing_grids=True, + ) ## Dev-2nd, test if the segment.coords function can properly give us the angles from this grid, which is at least called by rectangular_boundaries. - + ## User-2nd, test our ocean state boundary conditions ## User-3rd, test our tides boundary conditions - - - \ No newline at end of file From 52347df6fbbd58ee91e46f955e04de53f294850e Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 11:34:00 -0600 Subject: [PATCH 39/81] Skip this test in git workflow for now --- tests/test_angled_grids.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_angled_grids.py b/tests/test_angled_grids.py index 7b43bd60..820ac181 100644 --- a/tests/test_angled_grids.py +++ b/tests/test_angled_grids.py @@ -1,8 +1,12 @@ import regional_mom6 as rmom6 import os from pathlib import Path +import pytest +IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" + +@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") def test_angled_grids(): """ Test that the angled grid is correctly read in. From c52b49be4d84c70191411e33d74dc59f01bc3903 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 12:13:20 -0600 Subject: [PATCH 40/81] Wrap up tides adjustments, includinincluding string to tpxo number conversion --- regional_mom6/regional_mom6.py | 59 ++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index a5e5afe6..3db0e638 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -34,6 +34,37 @@ "segment", ] +tidal_constituents_tpxo_dict = { + "M2": 0, + "S2": 1, + "N2": 2, + "K2": 3, + "K1": 4, + "O1": 5, + "P1": 6, + "Q1": 7, + "MM": 8, + "MF": 9, + "M4": 10, + "MN4":11, + "MS4":12, + "2N2":13, + "S1":14 + # Add other constituents as needed +} + +def convert_to_tpxo_tidal_constituents(tidal_constituents): + """ + Convert tidal constituents from strings to integers using a dictionary. + + Parameters: + tidal_constituents (list of str): List of tidal constituent names as strings. + + Returns: + list of int: List of tidal constituent indices as integers. + """ + return [tidal_constituents_tpxo_dict[tc] for tc in tidal_constituents] + ## Auxiliary functions @@ -503,7 +534,7 @@ def __init__( repeat_year_forcing=False, read_existing_grids=False, minimum_depth=4, - tidal_constituents=[], + tidal_constituents=["M2"], ): ## in case list was given, convert to tuples self.longitude_extent = tuple(longitude_extent) @@ -1189,27 +1220,12 @@ def setup_tides_rectangle_boundaries( Web Address: https://github.com/jsimkins2/nwa25 """ - if ( - not os.path.exists(path_to_td) - or not os.path.exists(os.path.join(path_to_td, "h_" + tidal_filename)) - or not os.path.exists(os.path.join(path_to_td, "u_" + tidal_filename)) - ): - raise ValueError( - "Tidal Files don't exist at " - + path_to_td - + "/[h.or.u]_" - + tidal_filename - + ".nc" - ) - - ### Find Rough Horizontal Subset (with 0.5 Buffer)### - - if tidal_constituents != "`read_from_expt_init`": + if tidal_constituents != "read_from_expt_init": self.tidal_constituents = tidal_constituents tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) - .isel(constituent=tidal_constituents) + .isel(constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents)) ) tidal_360_lon = [ self.longitude_extent[0], @@ -1231,7 +1247,7 @@ def setup_tides_rectangle_boundaries( tpxo_u = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) - .isel(constituent=tidal_constituents, **horizontal_subset) + .isel(constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), **horizontal_subset) ) tpxo_u["ua"] *= 0.01 # convert to m/s u = tpxo_u["ua"] * np.exp(-1j * np.radians(tpxo_u["up"])) @@ -1240,7 +1256,7 @@ def setup_tides_rectangle_boundaries( tpxo_v = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) - .isel(constituent=tidal_constituents, **horizontal_subset) + .isel(constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), **horizontal_subset) ) tpxo_v["va"] *= 0.01 # convert to m/s v = tpxo_v["va"] * np.exp(-1j * np.radians(tpxo_v["vp"])) @@ -1810,7 +1826,7 @@ def setup_run_directory( for filename in os.listdir(os.path.join(self.mom_input_dir, "forcing")) ) if not tidal_files_exist: - raise ValueError( + raise ( "No files with 'tidal' in their names found in the forcing directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files." ) @@ -1915,6 +1931,7 @@ def setup_run_directory( if with_tides_rectangular: MOM_input_dict["TIDES"] = "True" MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = len(self.tidal_constituents) + MOM_input_dict["OBC_TIDE_CONSTITUENTS"] = "\"" + ", ".join(self.tidal_constituents) + "\"" MOM_input_dict["OBC_SEGMENT_001_DATA"] = ( '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)"' ) From 6fb8c13d65708786ce24c8331406bde5d15b5f01 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 12:14:01 -0600 Subject: [PATCH 41/81] mend --- regional_mom6/regional_mom6.py | 39 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 3db0e638..459423d7 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -43,28 +43,29 @@ "O1": 5, "P1": 6, "Q1": 7, - "MM": 8, - "MF": 9, - "M4": 10, - "MN4":11, - "MS4":12, - "2N2":13, - "S1":14 + "MM": 8, + "MF": 9, + "M4": 10, + "MN4": 11, + "MS4": 12, + "2N2": 13, + "S1": 14, # Add other constituents as needed } + def convert_to_tpxo_tidal_constituents(tidal_constituents): """ Convert tidal constituents from strings to integers using a dictionary. - + Parameters: tidal_constituents (list of str): List of tidal constituent names as strings. - + Returns: list of int: List of tidal constituent indices as integers. """ return [tidal_constituents_tpxo_dict[tc] for tc in tidal_constituents] - + ## Auxiliary functions @@ -1225,7 +1226,9 @@ def setup_tides_rectangle_boundaries( tpxo_h = ( xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) - .isel(constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents)) + .isel( + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) + ) ) tidal_360_lon = [ self.longitude_extent[0], @@ -1247,7 +1250,10 @@ def setup_tides_rectangle_boundaries( tpxo_u = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) - .isel(constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), **horizontal_subset) + .isel( + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), + **horizontal_subset, + ) ) tpxo_u["ua"] *= 0.01 # convert to m/s u = tpxo_u["ua"] * np.exp(-1j * np.radians(tpxo_u["up"])) @@ -1256,7 +1262,10 @@ def setup_tides_rectangle_boundaries( tpxo_v = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) - .isel(constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), **horizontal_subset) + .isel( + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), + **horizontal_subset, + ) ) tpxo_v["va"] *= 0.01 # convert to m/s v = tpxo_v["va"] * np.exp(-1j * np.radians(tpxo_v["vp"])) @@ -1931,7 +1940,9 @@ def setup_run_directory( if with_tides_rectangular: MOM_input_dict["TIDES"] = "True" MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = len(self.tidal_constituents) - MOM_input_dict["OBC_TIDE_CONSTITUENTS"] = "\"" + ", ".join(self.tidal_constituents) + "\"" + MOM_input_dict["OBC_TIDE_CONSTITUENTS"] = ( + '"' + ", ".join(self.tidal_constituents) + '"' + ) MOM_input_dict["OBC_SEGMENT_001_DATA"] = ( '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)"' ) From 51bf014eabeb870794dfe77e5d241d080faf3c1e Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 14:34:35 -0600 Subject: [PATCH 42/81] Change functions to verb start --- regional_mom6/regional_mom6.py | 67 +++++++++++++++---- tests/__init__.py | 0 tests/test_expt_class.py | 4 +- tests/test_tides_functions_config.py | 98 ++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_tides_functions_config.py diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 459423d7..22543a56 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -699,7 +699,25 @@ def _make_vgrid(self): return vcoord - def initial_condition( + @property + def initial_condition(self): + """ + Read the ic's from disk, and print 'em + """ + + try: + ic_tracers = xr.open_dataset(self.mom_input_dir / "forcing/init_tracers.nc") + ic_vel = xr.open_dataset(self.mom_input_dir / "forcing/init_vel.nc") + ic_eta = xr.open_dataset(self.mom_input_dir / "forcing/init_eta.nc") + return ic_tracers, ic_vel, ic_eta + except: + return "No initial condition set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( + self.mom_input_dir / "forcing" + ) + + return + + def setup_initial_condition( self, raw_ic_path, varnames, @@ -1093,12 +1111,13 @@ def get_glorys_rectangular( ) return - def setup_ocean_state_rectangular_boundaries( + def setup_ocean_state_boundaries( self, raw_boundaries_path, varnames, boundaries=["south", "north", "west", "east"], arakawa_grid="A", + boundary_type="rectangular", ): """ This function is a wrapper for `simple_boundary`. Given a list of up to four cardinal directions, @@ -1113,6 +1132,7 @@ def setup_ocean_state_rectangular_boundaries( Default is `["south", "north", "west", "east"]`. arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. Either ``'A'`` (default), ``'B'``, or ``'C'``. + boundary_type (Optional[str]): Type of box around region. Currently, only ``'rectangular'`` is supported. """ for i in boundaries: if i not in ["south", "north", "west", "east"]: @@ -1129,9 +1149,13 @@ def setup_ocean_state_rectangular_boundaries( raise ValueError( "This method only supports up to four boundaries. To set up more complex boundary shapes you can manually call the 'simple_boundary' method for each boundary." ) + if boundary_type != "rectangular": + raise ValueError( + "Only rectangular boundaries are supported by this method. To set up more complex boundary shapes you can manually call the 'simple_boundary' method for each boundary." + ) # Now iterate through our four boundaries for orientation in boundaries: - self.setup_ocean_state_simple_boundary( + self.setup_ocean_state_boundary( Path( os.path.join( (raw_boundaries_path), (orientation + "_unprocessed.nc") @@ -1145,8 +1169,14 @@ def setup_ocean_state_rectangular_boundaries( arakawa_grid=arakawa_grid, ) - def setup_ocean_state_simple_boundary( - self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" + def setup_ocean_state_boundary( + self, + path_to_bc, + varnames, + orientation, + segment_number, + arakawa_grid="A", + boundary_type="simple", ): """ Here 'simple' refers to boundaries that are parallel to lines of constant longitude or latitude. @@ -1165,6 +1195,7 @@ def setup_ocean_state_simple_boundary( the ``MOM_input``. arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. Either ``'A'`` (default), ``'B'``, or ``'C'``. + boundary_type (Optional[str]): Type of boundary. Currently, only ``'simple'`` is supported. Here 'simple' refers to boundaries that are parallel to lines of constant longitude or latitude. """ print("Processing {} boundary...".format(orientation), end="") @@ -1172,6 +1203,8 @@ def setup_ocean_state_simple_boundary( raise FileNotFoundError( f"Boundary file not found at {path_to_bc}. Please ensure that the files are named in the format `east_unprocessed.nc`." ) + if boundary_type != "simple": + raise ValueError("Only simple boundaries are supported by this method.") seg = segment( hgrid=self.hgrid, infile=path_to_bc, # location of raw boundary @@ -1184,15 +1217,19 @@ def setup_ocean_state_simple_boundary( repeat_year_forcing=self.repeat_year_forcing, ) - seg.rectangular_brushcut() + seg.regrid_rectangle_tracers() # Save Segment to Experiment self.segments[orientation] = seg print("Done.") return - def setup_tides_rectangle_boundaries( - self, path_to_td, tidal_filename, tidal_constituents="read_from_expt_init" + def setup_tides_boundaries( + self, + path_to_td, + tidal_filename, + tidal_constituents="read_from_expt_init", + boundary_type="rectangle", ): """ This function: @@ -1202,6 +1239,7 @@ def setup_tides_rectangle_boundaries( path_to_td (str): Path to boundary tidal file. tidal_filename: Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} tidal_constiuents: List of tidal constituents to include in the regridding. Default is [0] which is the M2 constituent. + boundary_type (Optional[str]): Type of boundary. Currently, only ``'rectangle'`` is supported. Here 'rectangle' refers to boundaries that are parallel to lines of constant longitude or latitude. Returns: *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' @@ -1220,7 +1258,10 @@ def setup_tides_rectangle_boundaries( Type: Python Functions, Source Code Web Address: https://github.com/jsimkins2/nwa25 """ - + if boundary_type != "rectangle": + raise ValueError( + "Only rectangular boundaries are supported by this method." + ) if tidal_constituents != "read_from_expt_init": self.tidal_constituents = tidal_constituents tpxo_h = ( @@ -1699,7 +1740,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): print("done.") self.bathymetry = bathymetry - def FRE_tools(self, layout=None): + def run_FRE_tools(self, layout=None): """A wrapper for FRE Tools ``check_mask``, ``make_solo_mosaic``, and ``make_quick_mosaic``. User provides processor ``layout`` tuple of processing units. """ @@ -1737,9 +1778,9 @@ def FRE_tools(self, layout=None): ) if layout != None: - self.cpu_layout(layout) + self.configure_cpu_layout(layout) - def cpu_layout(self, layout): + def configure_cpu_layout(self, layout): """ Wrapper for the ``check_mask`` function of GFDL's FRE Tools. User provides processor ``layout`` tuple of processing units. @@ -2343,7 +2384,7 @@ def coords(self): return rcoord - def rectangular_brushcut(self): + def regrid_rectangle_tracers(self): """ Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary is a simple Northern, Southern, Eastern, or Western boundary. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index 292448e6..d03c3bf5 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -311,7 +311,7 @@ def test_ocean_forcing( "tracers": {"temp": "temp", "salt": "salt"}, } - expt.initial_condition( + expt.setup_initial_condition( tmp_path / "ic_unprocessed", varnames, arakawa_grid="A", @@ -470,4 +470,4 @@ def test_rectangular_boundaries( "tracers": {"temp": "temp", "salt": "salt"}, } - expt.setup_ocean_state_rectangular_boundaries(tmp_path, varnames, ["east"]) + expt.setup_ocean_state_boundaries(tmp_path, varnames, ["east"]) diff --git a/tests/test_tides_functions_config.py b/tests/test_tides_functions_config.py new file mode 100644 index 00000000..fd62a3f9 --- /dev/null +++ b/tests/test_tides_functions_config.py @@ -0,0 +1,98 @@ +import regional_mom6 as rmom6 +import os +import pytest +import logging +from pathlib import Path + + +class TestAll: + @classmethod + def setup_class(self): + expt_name = "testing" + + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + + date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] + + ## Place where all your input files go + input_dir = Path( + os.path.join( + "/", + "glade", + "u", + "home", + "manishrv", + "documents", + "nwa12_0.1", + "regional_mom_workflows", + "rm6", + expt_name, + "inputs", + ) + ) + + ## Directory where you'll run the experiment from + run_dir = Path( + os.path.join( + "/", + "glade", + "u", + "home", + "manishrv", + "documents", + "nwa12_0.1", + "regional_mom_workflows", + "rm6", + expt_name, + "run_files", + ) + ) + for path in (run_dir, input_dir): + os.makedirs(str(path), exist_ok=True) + self.glorys_path = os.path.join( + "/", + "glade", + "derecho", + "scratch", + "manishrv", + "inputs_rm6_hawaii", + "glorys", + ) + ## User-1st, test if we can even read the angled nc files. + self.expt = rmom6.experiment( + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=0.05, + number_vertical_layers=75, + layer_thickness_ratio=10, + depth=4500, + minimum_depth=5, + mom_run_dir=run_dir, + mom_input_dir=input_dir, + toolpath_dir="", + ) + + def test_initial_condition(self): + ocean_varnames = { + "time": "time", + "yh": "latitude", + "xh": "longitude", + "zl": "depth", + "eta": "zos", + "u": "uo", + "v": "vo", + "tracers": {"salt": "so", "temp": "thetao"}, + } + + # Set up the initial condition + self.expt.setup_initial_condition( + Path( + os.path.join(self.glorys_path, "ic_unprocessed.nc") + ), # directory where the unprocessed initial condition is stored, as defined earlier + ocean_varnames, + arakawa_grid="A", + ) + d1, d2, d3 = self.expt.initial_condition + print(d1, d2, d3) From 08288ee64e0506b382f7c37bf6948bb39574e34c Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 14:38:45 -0600 Subject: [PATCH 43/81] Remove test from github workflow --- tests/test_tides_functions_config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_tides_functions_config.py b/tests/test_tides_functions_config.py index fd62a3f9..653295a0 100644 --- a/tests/test_tides_functions_config.py +++ b/tests/test_tides_functions_config.py @@ -3,6 +3,7 @@ import pytest import logging from pathlib import Path +IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" class TestAll: @@ -74,6 +75,8 @@ def setup_class(self): toolpath_dir="", ) + + @pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") def test_initial_condition(self): ocean_varnames = { "time": "time", From bfefbe8285a5e998803d8bff4e44dc9a179a5910 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 14:40:25 -0600 Subject: [PATCH 44/81] Formatting --- tests/test_tides_functions_config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_tides_functions_config.py b/tests/test_tides_functions_config.py index 653295a0..d7ec55f3 100644 --- a/tests/test_tides_functions_config.py +++ b/tests/test_tides_functions_config.py @@ -3,6 +3,7 @@ import pytest import logging from pathlib import Path + IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" @@ -75,8 +76,9 @@ def setup_class(self): toolpath_dir="", ) - - @pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") + @pytest.mark.skipif( + IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." + ) def test_initial_condition(self): ocean_varnames = { "time": "time", From ea5a9c34a601b8698e99cf67a769285e5c1da921 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 15:41:02 -0600 Subject: [PATCH 45/81] Add properties and change function names --- demos/reanalysis-forced.ipynb | 2 +- regional_mom6/regional_mom6.py | 95 +++++++++++++++++++++++++--- tests/test_grid_generation.py | 4 +- tests/test_tides_functions_config.py | 18 +++++- 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index df57fe3d..70f515c2 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -324,7 +324,7 @@ "metadata": {}, "outputs": [], "source": [ - "expt.FRE_tools(layout=(10, 10)) ## Here the tuple defines the processor layout" + "expt.run_FRE_tools(layout=(10, 10)) ## Here the tuple defines the processor layout" ] }, { diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 22543a56..772abc27 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -23,13 +23,15 @@ ) import pandas as pd import re +from pathlib import Path +import glob warnings.filterwarnings("ignore") __all__ = [ "longitude_slicer", "hyperbolictan_thickness_profile", - "rectangular_hgrid", + "calculate_rectangular_hgrid", "experiment", "segment", ] @@ -212,9 +214,6 @@ def find_MOM6_rectangular_orientation(input): raise ValueError("Invalid type of Input, can only be string or int.") -from pathlib import Path - - def get_glorys_data( longitude_extent, latitude_extent, @@ -375,7 +374,7 @@ def hyperbolictan_thickness_profile(nlayers, ratio, total_depth): return layer_thicknesses -def rectangular_hgrid(lons, lats): +def calculate_rectangular_hgrid(lons, lats): """ Construct a horizontal grid with all the metadata required by MOM6, based on arrays of longitudes (``lons``) and latitudes (``lats``) on the supergrid. @@ -668,7 +667,7 @@ def _make_hgrid(self): self.latitude_extent[0], self.latitude_extent[1], ny ) # latitudes in degrees - hgrid = rectangular_hgrid(lons, lats) + hgrid = calculate_rectangular_hgrid(lons, lats) hgrid.to_netcdf(self.mom_input_dir / "hgrid.nc") return hgrid @@ -699,6 +698,74 @@ def _make_vgrid(self): return vcoord + @property + def ocean_state_boundaries(self): + """ + Read the ocean state files from disk, and print 'em + """ + ocean_state_path = self.mom_input_dir / "forcing" + try: + # Use glob to find all tides files + patterns = ["forcing_*", "weights/bi*"] + all_files = [] + for pattern in patterns: + all_files.extend(glob.glob(os.path.join(ocean_state_path, pattern))) + + if len(all_files) == 0: + return "No ocean state files set up yet (or files misplaced from {}). Call `setup_ocean_state_boundaries` method to set up ocean state.".format( + ocean_state_path + ) + + # Open the files as xarray datasets + datasets = [xr.open_dataset(file) for file in all_files] + return datasets + except: + return "Error retrieving ocean state files" + + @property + def tides_boundaries(self): + """ + Read the tides from disk, and print 'em + """ + tides_path = self.mom_input_dir / "forcing" + try: + # Use glob to find all tides files + patterns = ["regrid*", "tu_*", "tz_*"] + all_files = [] + for pattern in patterns: + all_files.extend(glob.glob(os.path.join(tides_path, pattern))) + + if len(all_files) == 0: + return "No tides files set up yet (or files misplaced from {}). Call `setup_tides_boundaries` method to set up tides.".format( + tides_path + ) + + # Open the files as xarray datasets + datasets = [xr.open_dataset(file) for file in all_files] + return datasets + except: + return "Error retrieving tides files" + + @property + def era5(self): + """ + Read the era5's from disk, and print 'em + """ + era5_path = self.mom_input_dir / "forcing" + try: + # Use glob to find all *_ERA5.nc files + all_files = glob.glob(os.path.join(era5_path, "*_ERA5.nc")) + if len(all_files) == 0: + return "No era5 files set up yet (or files misplaced from {}). Call `setup_era5` method to set up era5.".format( + era5_path + ) + + # Open the files as xarray datasets + datasets = [xr.open_dataset(file) for file in all_files] + return datasets + except: + return "Error retrieving ERA5 files" + @property def initial_condition(self): """ @@ -709,13 +776,25 @@ def initial_condition(self): ic_tracers = xr.open_dataset(self.mom_input_dir / "forcing/init_tracers.nc") ic_vel = xr.open_dataset(self.mom_input_dir / "forcing/init_vel.nc") ic_eta = xr.open_dataset(self.mom_input_dir / "forcing/init_eta.nc") - return ic_tracers, ic_vel, ic_eta + return [ic_tracers, ic_vel, ic_eta] except: return "No initial condition set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( self.mom_input_dir / "forcing" ) - return + @property + def bathymetry_property(self): + """ + Read the bathymetry from disk, and print 'em + """ + + try: + bathy = xr.open_dataset(self.mom_input_dir / "bathymetry.nc") + return [bathy] + except: + return "No bathymetry set up yet (or files misplaced from {}). Call `setup_bathymetry` method to set up bathymetry.".format( + self.mom_input_dir + ) def setup_initial_condition( self, diff --git a/tests/test_grid_generation.py b/tests/test_grid_generation.py index d5158420..538cf2ad 100644 --- a/tests/test_grid_generation.py +++ b/tests/test_grid_generation.py @@ -2,7 +2,7 @@ import pytest from regional_mom6 import hyperbolictan_thickness_profile -from regional_mom6 import rectangular_hgrid +from regional_mom6 import calculate_rectangular_hgrid from regional_mom6 import longitude_slicer from regional_mom6.utils import angle_between @@ -129,7 +129,7 @@ def test_quadrilateral_areas(lat, lon, true_area): ], ) def test_rectangular_hgrid(lat, lon): - assert isinstance(rectangular_hgrid(lat, lon), xr.Dataset) + assert isinstance(calculate_rectangular_hgrid(lat, lon), xr.Dataset) def test_longitude_slicer(): diff --git a/tests/test_tides_functions_config.py b/tests/test_tides_functions_config.py index d7ec55f3..9bb9b37c 100644 --- a/tests/test_tides_functions_config.py +++ b/tests/test_tides_functions_config.py @@ -99,5 +99,19 @@ def test_initial_condition(self): ocean_varnames, arakawa_grid="A", ) - d1, d2, d3 = self.expt.initial_condition - print(d1, d2, d3) + dss = self.expt.initial_condition + print(dss) + + @pytest.mark.skipif( + IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." + ) + def test_properties(self): + """ + Test the properties + """ + dss = self.expt.era5 + dss_2 = self.expt.tides_boundaries + dss_3 = self.expt.ocean_state_boundaries + dss_4 = self.expt.initial_condition + dss_5 = self.expt.bathymetry_property + print(dss_2, dss_3, dss_4, dss_5) From b42e44d2310f8eba6eb1fc225aa7bac6daa05c27 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 16:44:56 -0600 Subject: [PATCH 46/81] Shifting MOM_input to MOM_override Part 1 --- demos/reanalysis-forced.ipynb | 2 +- regional_mom6/regional_mom6.py | 122 ++++++++++++++++++++++++--------- 2 files changed, 89 insertions(+), 35 deletions(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index 70f515c2..4865b67d 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -294,7 +294,7 @@ " }\n", "\n", "# Set up the initial condition\n", - "expt.initial_condition(\n", + "expt.setup_initial_condition(\n", " glorys_path / \"ic_unprocessed.nc\", # directory where the unprocessed initial condition is stored, as defined earlier\n", " ocean_varnames,\n", " arakawa_grid=\"A\"\n", diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 772abc27..63adc446 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -25,6 +25,7 @@ import re from pathlib import Path import glob +from collections import defaultdict warnings.filterwarnings("ignore") @@ -1952,11 +1953,14 @@ def setup_run_directory( if with_tides_rectangular: tidal_files_exist = any( "tidal" in filename - for filename in os.listdir(os.path.join(self.mom_input_dir, "forcing")) + for filename in ( + os.listdir(os.path.join(self.mom_input_dir, "forcing")) + + os.listdir(os.path.join(self.mom_input_dir)) + ) ) if not tidal_files_exist: raise ( - "No files with 'tidal' in their names found in the forcing directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files." + "No files with 'tidal' in their names found in the forcing or input directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files." ) # 3 different cases to handle: @@ -2039,44 +2043,64 @@ def setup_run_directory( MOM_layout_dict = self.read_MOM_file_as_dict("MOM_layout") if "MASKTABLE" in MOM_layout_dict.keys(): if mask_table != None: - MOM_layout_dict["MASKTABLE"] = mask_table + MOM_layout_dict["MASKTABLE"]["value"] = mask_table else: - MOM_layout_dict["MASKTABLE"] = "# MASKTABLE = no mask table" + MOM_layout_dict["MASKTABLE"]["value"] = "# MASKTABLE = no mask table" if ( "LAYOUT" in MOM_layout_dict.keys() and "IO" not in MOM_layout_dict.keys() and layout != None ): - MOM_layout_dict["LAYOUT"] = str(layout[1]) + "," + str(layout[0]) + MOM_layout_dict["LAYOUT"]["value"] = str(layout[1]) + "," + str(layout[0]) if "NIGLOBAL" in MOM_layout_dict.keys(): - MOM_layout_dict["NIGLOBAL"] = self.hgrid.nx.shape[0] // 2 + MOM_layout_dict["NIGLOBAL"]["value"] = self.hgrid.nx.shape[0] // 2 if "NJGLOBAL" in MOM_layout_dict.keys(): - MOM_layout_dict["NJGLOBAL"] = self.hgrid.ny.shape[0] // 2 + MOM_layout_dict["NJGLOBAL"]["value"] = self.hgrid.ny.shape[0] // 2 self.write_MOM_file(MOM_layout_dict) MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") - MOM_input_dict["MINIMUM_DEPTH"] = float(self.min_depth) - MOM_input_dict["NK"] = len(self.vgrid.zl.values) + MOM_override_dict = self.read_MOM_file_as_dict("MOM_override") + MOM_override_dict["MINIMUM_DEPTH"]["value"] = float(self.min_depth) + MOM_override_dict["NK"]["value"] = len(self.vgrid.zl.values) if with_tides_rectangular: - MOM_input_dict["TIDES"] = "True" - MOM_input_dict["OBC_TIDE_N_CONSTITUENTS"] = len(self.tidal_constituents) - MOM_input_dict["OBC_TIDE_CONSTITUENTS"] = ( - '"' + ", ".join(self.tidal_constituents) + '"' - ) - MOM_input_dict["OBC_SEGMENT_001_DATA"] = ( - '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)"' - ) - MOM_input_dict["OBC_SEGMENT_002_DATA"] = ( - '"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)"' - ) - MOM_input_dict["OBC_SEGMENT_003_DATA"] = ( - '"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)"' + MOM_override_dict["TIDES"]["value"] = "True" + MOM_override_dict["OBC_TIDE_N_CONSTITUENTS"]["value"] = len( + self.tidal_constituents ) - MOM_input_dict["OBC_SEGMENT_004_DATA"] = ( - '"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)"' + MOM_override_dict["OBC_TIDE_CONSTITUENTS"]["value"] = ( + '"' + ", ".join(self.tidal_constituents) + '"' ) - + MOM_override_dict["OBC_SEGMENT_001_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)"' + MOM_override_dict["OBC_SEGMENT_002_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)"' + MOM_override_dict["OBC_SEGMENT_003_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)"' + MOM_override_dict["OBC_SEGMENT_004_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)"' + else: + MOM_override_dict["OBC_SEGMENT_001_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt)"' + MOM_override_dict["OBC_SEGMENT_002_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt)"' + MOM_override_dict["OBC_SEGMENT_003_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt)"' + MOM_override_dict["OBC_SEGMENT_004_DATA"][ + "value" + ] = '"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt)"' + + for key in MOM_override_dict.keys(): + if type(MOM_override_dict[key]) == dict: + MOM_override_dict[key]["override"] = True self.write_MOM_file(MOM_input_dict) + self.write_MOM_file(MOM_override_dict) ## If using payu to run the model, create a payu configuration file if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): @@ -2121,16 +2145,35 @@ def read_MOM_file_as_dict(self, filename): """ Read the MOM_input file and return a dictionary of the variables and their values. """ + + # Default information for each parameter + default_layout = {"value": None, "override": False, "comment": None} with open(os.path.join(self.mom_run_dir, filename), "r") as file: lines = file.readlines() - MOM_file_dict = {"filename": filename} + + # Set the default initialization for a new key + MOM_file_dict = defaultdict(lambda: default_layout.copy()) + MOM_file_dict["filename"] = filename + dlc = default_layout.copy() for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: split = lines[jj].split("=", 1) var = split[0] value = split[1] - value = value.split("!")[0].strip() # Remove Comments - MOM_file_dict[var.strip()] = value.strip() + if "#override" in var: + var = var.split("#override")[1].strip() + dlc["override"] = True + else: + dlc["override"] = False + if "!" in value: + dlc["comment"] = value.split("!")[1] + value = value.split("!")[0].strip() # Remove Comments + dlc["value"] = str(value) + else: + dlc["value"] = str(value.strip()) + dlc["comment"] = None + + MOM_file_dict[var.strip()] = dlc.copy() # Save a copy of the original dictionary MOM_file_dict["original"] = MOM_file_dict.copy() @@ -2149,12 +2192,16 @@ def write_MOM_file(self, MOM_file_dict): for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: var = lines[jj].split("=", 1)[0].strip() - if ( - var in MOM_file_dict.keys() - and (str(MOM_file_dict[var])) != original_MOM_file_dict[var] - ): + if var in MOM_file_dict.keys() and ( + str(MOM_file_dict[var]["value"]) + ) != str(original_MOM_file_dict[var]["value"]): lines[jj] = lines[jj].replace( - original_MOM_file_dict[var], str(MOM_file_dict[var]) + str(original_MOM_file_dict[var]["value"]), + str(MOM_file_dict[var]["value"]), + ) + lines[jj] = lines[jj].replace( + original_MOM_file_dict[var]["comment"], + str(MOM_file_dict[var]["comment"]), ) print( "Changed", @@ -2170,7 +2217,14 @@ def write_MOM_file(self, MOM_file_dict): lines.append("! === Added with RM6 ===\n") for key in MOM_file_dict.keys(): if key not in original_MOM_file_dict.keys(): - lines.append(f"{key} = {MOM_file_dict[key]}\n") + if MOM_file_dict[key]["override"]: + lines.append( + f"#override {key} = {MOM_file_dict[key]['value']} !{MOM_file_dict[key]["comment"]}\n" + ) + else: + lines.append( + f"{key} = {MOM_file_dict[key]} !{MOM_file_dict[key]["comment"]}\n" + ) print( "Added", key, From 2ba990b45421ac23acc5afe244b58acef0e6e122 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 27 Sep 2024 16:49:34 -0600 Subject: [PATCH 47/81] Minor Changes --- regional_mom6/regional_mom6.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 63adc446..10f8c8f1 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -2219,11 +2219,11 @@ def write_MOM_file(self, MOM_file_dict): if key not in original_MOM_file_dict.keys(): if MOM_file_dict[key]["override"]: lines.append( - f"#override {key} = {MOM_file_dict[key]['value']} !{MOM_file_dict[key]["comment"]}\n" + f"#override {key} = {MOM_file_dict[key]['value']} !{MOM_file_dict[key]['comment']}\n" ) else: lines.append( - f"{key} = {MOM_file_dict[key]} !{MOM_file_dict[key]["comment"]}\n" + f"{key} = {MOM_file_dict[key]['value']} !{MOM_file_dict[key]['comment']}\n" ) print( "Added", From fefcc413e3842ee44963c2480774bcc2518153c3 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Mon, 30 Sep 2024 10:59:21 -0600 Subject: [PATCH 48/81] Add flexible OBC to help fix Issue #8, Move OBC params to MOM Override, Add a change_MOM_parameter function as suggested to be fredc --- demos/reanalysis-forced.ipynb | 2 +- regional_mom6/regional_mom6.py | 273 ++++++++++++++++++++++++--------- 2 files changed, 200 insertions(+), 75 deletions(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index 4865b67d..d449d55f 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -301,7 +301,7 @@ " ) \n", "\n", "# Set up the four boundary conditions. Remember that in the glorys_path, we have four boundary files names north_unprocessed.nc etc. \n", - "expt.setup_ocean_state_rectangular_boundaries(\n", + "expt.setup_ocean_state(\n", " glorys_path,\n", " ocean_varnames,\n", " boundaries = [\"south\", \"north\", \"west\", \"east\"],\n", diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 10f8c8f1..247ca7e2 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -37,24 +37,8 @@ "segment", ] -tidal_constituents_tpxo_dict = { - "M2": 0, - "S2": 1, - "N2": 2, - "K2": 3, - "K1": 4, - "O1": 5, - "P1": 6, - "Q1": 7, - "MM": 8, - "MF": 9, - "M4": 10, - "MN4": 11, - "MS4": 12, - "2N2": 13, - "S1": 14, - # Add other constituents as needed -} + +## Mapping Functions def convert_to_tpxo_tidal_constituents(tidal_constituents): @@ -67,7 +51,58 @@ def convert_to_tpxo_tidal_constituents(tidal_constituents): Returns: list of int: List of tidal constituent indices as integers. """ - return [tidal_constituents_tpxo_dict[tc] for tc in tidal_constituents] + tidal_constituents_tpxo_dict = { + "M2": 0, + "S2": 1, + "N2": 2, + "K2": 3, + "K1": 4, + "O1": 5, + "P1": 6, + "Q1": 7, + "MM": 8, + "MF": 9, + # Only supported tidal bc's + } + + list_of_ints = [] + for tc in tidal_constituents: + try: + list_of_ints.append(tidal_constituents_tpxo_dict[tc]) + except: + raise ValueError( + "Invalid Input. Tidal constituent {} is not supported.".format(tc) + ) + + return list_of_ints + + +def find_MOM6_rectangular_orientation(input): + """ + Convert between MOM6 boundary and the specific segment number needed, or the inverse + """ + direction_dir = { + "south": 1, + "north": 2, + "west": 3, + "east": 4, + } + direction_dir_inv = {v: k for k, v in direction_dir.items()} + + if type(input) == str: + try: + return direction_dir[input] + except: + raise ValueError( + "Invalid Input. Did you spell the direction wrong, it should be lowercase?" + ) + elif type(input) == int: + try: + return direction_dir_inv[input] + except: + raise ValueError("Invalid Input. Did you pick a number 1 through 4?") + else: + raise ValueError("Invalid type of Input, can only be string or int.") ## Auxiliary functions @@ -187,34 +222,6 @@ def longitude_slicer(data, longitude_extent, longitude_coords): return data -def find_MOM6_rectangular_orientation(input): - """ - Convert between MOM6 boundary and the specific segment number needed, or the inverse - """ - direction_dir = { - "south": 1, - "north": 2, - "west": 3, - "east": 4, - } - direction_dir_inv = {v: k for k, v in direction_dir.items()} - - if type(input) == str: - try: - return direction_dir[input] - except: - raise ValueError( - "Invalid Input. Did you spell the direction wrong, it should be lowercase?" - ) - elif type(input) == int: - try: - return direction_dir_inv[input] - except: - raise ValueError("Invalid Input. Did you pick a number 1 through 4?") - else: - raise ValueError("Invalid type of Input, can only be string or int.") - - def get_glorys_data( longitude_extent, latitude_extent, @@ -1884,6 +1891,7 @@ def setup_run_directory( using_payu=False, overwrite=False, with_tides_rectangular=False, + boundaries=["south", "north", "west", "east"], ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify @@ -2060,9 +2068,75 @@ def setup_run_directory( MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") MOM_override_dict = self.read_MOM_file_as_dict("MOM_override") + # The number of boundaries is reflected in the number of segments setup in setup_ocean_state_boundary under expt.segments. + # The setup_tides_boundaries function currently only works with rectangular grids amd sets up 4 segments, but DOESN"T save them to expt.segments. + # Therefore, we can use expt.segments to determine how many segments we need for MOM_input. We can fill the empty segments with a empty string to make sure it is overriden correctly. + + # Others MOM_override_dict["MINIMUM_DEPTH"]["value"] = float(self.min_depth) MOM_override_dict["NK"]["value"] = len(self.vgrid.zl.values) + + # Define number of OBC segments + MOM_override_dict["OBC_NUMBER_OF_SEGMENTS"]["value"] = len( + boundaries + ) # This means that each SEGMENT_00{num} has to be configured to point to the right file, which based on our other functions needs to be specified. + + # More OBC Consts + MOM_override_dict["OBC_FREESLIP_VORTICITY"]["value"] = "False" + MOM_override_dict["OBC_FREESLIP_STRAIN"]["value"] = "False" + MOM_override_dict["OBC_COMPUTED_VORTICITY"]["value"] = "True" + MOM_override_dict["OBC_COMPUTED_STRAIN"]["value"] = "True" + MOM_override_dict["OBC_ZERO_BIHARMONIC"]["value"] = "True" + MOM_override_dict["OBC_TRACER_RESERVOIR_LENGTH_SCALE_OUT"]["value"] = "3.0E+04" + MOM_override_dict["OBC_TRACER_RESERVOIR_LENGTH_SCALE_IN"]["value"] = "3000.0" + MOM_override_dict["BRUSHCUTTER_MODE"]["value"] = "True" + # Define Specific Segments + + for ind, seg in enumerate(boundaries): + ind_seg = ind + 1 + key_start = "OBC_SEGMENT_00" + str(ind_seg) + ## Position and Config + key_POSITION = key_start + if find_MOM6_rectangular_orientation(seg) == 1: + j_str = "0" + i_str = "0:N" + elif find_MOM6_rectangular_orientation(seg) == 2: + j_str = "N" + i_str = "N:0" + elif find_MOM6_rectangular_orientation(seg) == 3: + j_str = "N:0" + i_str = "0" + elif find_MOM6_rectangular_orientation(seg) == 4: + j_str = "0:N" + i_str = "N" + index_str = '"J={},I={}'.format(j_str, i_str) + MOM_override_dict[key_POSITION]["value"] = ( + index_str + ',FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN"' + ) + + ## Nudget Key + key_NUDGING = key_start + "_VELOCITY_NUDGING_TIMESCALES" + MOM_override_dict[key_NUDGING]["value"] = "0.3, 360.0" + ## Data + key_DATA = key_start + "_DATA" + file_num_obc = str( + find_MOM6_rectangular_orientation(seg) + ) # 1,2,3,4 for rectangular boundaries, BUT if we have less than 4 segments we use the index to specific the number, but keep filenames as if we had four boundaries + MOM_override_dict[key_DATA][ + "value" + ] = f'"U=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(u),V=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(v),SSH=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(eta),TEMP=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(temp),SALT=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(salt)' + if with_tides_rectangular: + MOM_override_dict[key_DATA]["value"] = ( + MOM_override_dict[key_DATA]["value"] + + f',Uamp=file:forcing/tu_segment_00{file_num_obc}.nc(uamp),Uphase=file:forcing/tu_segment_00{file_num_obc}.nc(uphase),Vamp=file:forcing/tu_segment_00{file_num_obc}.nc(vamp),Vphase=file:forcing/tu_segment_00{file_num_obc}.nc(vphase),SSHamp=file:forcing/tz_segment_00{file_num_obc}.nc(zamp),SSHphase=file:forcing/tz_segment_00{file_num_obc}.nc(zphase)"' + ) + else: + MOM_override_dict[key_DATA]["value"] = ( + MOM_override_dict[key_DATA]["value"] + '"' + ) + if with_tides_rectangular: + MOM_override_dict["OBC_TIDE_ADD_EQ_PHASE"]["value"] = "True" MOM_override_dict["TIDES"]["value"] = "True" MOM_override_dict["OBC_TIDE_N_CONSTITUENTS"]["value"] = len( self.tidal_constituents @@ -2070,31 +2144,13 @@ def setup_run_directory( MOM_override_dict["OBC_TIDE_CONSTITUENTS"]["value"] = ( '"' + ", ".join(self.tidal_constituents) + '"' ) - MOM_override_dict["OBC_SEGMENT_001_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt),Uamp=file:forcing/tu_segment_001.nc(uamp),Uphase=file:forcing/tu_segment_001.nc(uphase),Vamp=file:forcing/tu_segment_001.nc(vamp),Vphase=file:forcing/tu_segment_001.nc(vphase),SSHamp=file:forcing/tz_segment_001.nc(zamp),SSHphase=file:forcing/tz_segment_001.nc(zphase)"' - MOM_override_dict["OBC_SEGMENT_002_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt),Uamp=file:forcing/tu_segment_002.nc(uamp),Uphase=file:forcing/tu_segment_002.nc(uphase),Vamp=file:forcing/tu_segment_002.nc(vamp),Vphase=file:forcing/tu_segment_002.nc(vphase),SSHamp=file:forcing/tz_segment_002.nc(zamp),SSHphase=file:forcing/tz_segment_002.nc(zphase)"' - MOM_override_dict["OBC_SEGMENT_003_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt),Uamp=file:forcing/tu_segment_003.nc(uamp),Uphase=file:forcing/tu_segment_003.nc(uphase),Vamp=file:forcing/tu_segment_003.nc(vamp),Vphase=file:forcing/tu_segment_003.nc(vphase),SSHamp=file:forcing/tz_segment_003.nc(zamp),SSHphase=file:forcing/tz_segment_003.nc(zphase)"' - MOM_override_dict["OBC_SEGMENT_004_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt),Uamp=file:forcing/tu_segment_004.nc(uamp),Uphase=file:forcing/tu_segment_004.nc(uphase),Vamp=file:forcing/tu_segment_004.nc(vamp),Vphase=file:forcing/tu_segment_004.nc(vphase),SSHamp=file:forcing/tz_segment_004.nc(zamp),SSHphase=file:forcing/tz_segment_004.nc(zphase)"' - else: - MOM_override_dict["OBC_SEGMENT_001_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt)"' - MOM_override_dict["OBC_SEGMENT_002_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt)"' - MOM_override_dict["OBC_SEGMENT_003_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt)"' - MOM_override_dict["OBC_SEGMENT_004_DATA"][ - "value" - ] = '"U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt)"' + MOM_override_dict["OBC_TIDE_REF_DATE"]["value"] = ( + str(self.date_range[0].year) + + ", " + + str(self.date_range[0].month) + + ", " + + str(self.date_range[0].day) + ) for key in MOM_override_dict.keys(): if type(MOM_override_dict[key]) == dict: @@ -2141,6 +2197,63 @@ def setup_run_directory( nml.write(self.mom_run_dir / "input.nml", force=True) return + def change_MOM_parameter( + self, param_name, param_value=None, comment=None, delete=False + ): + """ + Change a parameter in the MOM_input or MOM_override file. Returns original value if there was one. + If delete is specified, ONLY MOM_override version will be deleted. Deleting from MOM_input is not safe. + If the parameter does not exist, it will be added to the file. if delete is set to True, the parameter will be removed. + Args: + param_name (str): + Parameter name we are working with + param_value (Optional[str]): + New Assigned Value + comment (Optional[str]): + Any comment to add + delete (Optional[bool]): + Whether to delete the specified param_name + + """ + if not delete and param_value is None: + raise ValueError( + "If not deleting a parameter, you must specify a new value for it." + ) + + MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") + MOM_override_dict = self.read_MOM_file_as_dict("MOM_override") + original_val = "No original val" + if not delete: + # We don't want to keep any parameters in MOM_input that we change. We want to clearly list them in MOM_override. + if param_name in MOM_input_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print("Removing original value {} from MOM_input".format(original_val)) + del MOM_input_dict[param_name] + if param_name in MOM_override_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print( + "This parameter {} is begin replaced from {} to {} in MOM_override".format( + param_name, original_val, param_value + ) + ) + + MOM_override_dict[param_name]["value"] = param_value + MOM_override_dict[param_name]["comment"] = comment + else: + if param_name in MOM_override_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print("Deleting parameter {} from MOM_override".format(param_name)) + del MOM_override_dict[param_name] + else: + print( + "Key to be deleted {} was not in MOM_override to begin with.".format( + param_name + ) + ) + self.write_MOM_file(MOM_input_dict) + self.write_MOM_file(MOM_override_dict) + return original_val + def read_MOM_file_as_dict(self, filename): """ Read the MOM_input file and return a dictionary of the variables and their values. @@ -2237,10 +2350,22 @@ def write_MOM_file(self, MOM_file_dict): # Check any fields removed for key in original_MOM_file_dict.keys(): if key not in MOM_file_dict.keys(): + search_words = [ + key, + original_MOM_file_dict[key]["value"], + original_MOM_file_dict[key]["comment"], + ] + lines = [ + line + for line in lines + if not all(word in line for word in search_words) + ] print( - "WARNING: Field", + "Removed", key, - "was not found in the new dictionary. Keeping the original value of", + "in", + MOM_file_dict["filename"], + "with value", original_MOM_file_dict[key], ) From b6a96b2496ebbb03e2b8bec07c923a9fc574a32b Mon Sep 17 00:00:00 2001 From: manishvenu Date: Mon, 30 Sep 2024 11:25:40 -0600 Subject: [PATCH 49/81] Deleting MOM_input Indexed OBC Vars This is because we don't want to hard code the 4 boundaries, this is a start of making it so that the indexed stuff is at least for sure in MOM_override so an extra boundary doesn't sneak into MOM. For example if I have three boundaries but 4 OBC_segment data defs, that's kinda sus. --- demos/reanalysis-forced.ipynb | 2 +- regional_mom6/regional_mom6.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index d449d55f..35f7eabe 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -301,7 +301,7 @@ " ) \n", "\n", "# Set up the four boundary conditions. Remember that in the glorys_path, we have four boundary files names north_unprocessed.nc etc. \n", - "expt.setup_ocean_state(\n", + "expt.setup_ocean_state_boundaries(\n", " glorys_path,\n", " ocean_varnames,\n", " boundaries = [\"south\", \"north\", \"west\", \"east\"],\n", diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 247ca7e2..a15b2e51 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1935,7 +1935,7 @@ def setup_run_directory( + "There may be an issue with package installation. Check that the `premade_run_directory` folder is present in one of these two locations" ) else: - print("It is! Found them!") + print("Found run files. Continuing...") # Define the locations of the directories we'll copy files across from. Base contains most of the files, and overwrite replaces files in the base directory. base_run_dir = Path(os.path.join(premade_rundir_path, "common_files")) @@ -2076,6 +2076,16 @@ def setup_run_directory( MOM_override_dict["MINIMUM_DEPTH"]["value"] = float(self.min_depth) MOM_override_dict["NK"]["value"] = len(self.vgrid.zl.values) + # OBC Adjustments + + # Delete MOM_input OBC stuff that is indexed because we want them only in MOM_override. + print( + "Deleting indexed OBC keys from MOM_input_dict in case we have a different number of segments" + ) + keys_to_delete = [key for key in MOM_input_dict if "_SEGMENT_00" in key] + for key in keys_to_delete: + del MOM_input_dict[key] + # Define number of OBC segments MOM_override_dict["OBC_NUMBER_OF_SEGMENTS"]["value"] = len( boundaries @@ -2090,8 +2100,8 @@ def setup_run_directory( MOM_override_dict["OBC_TRACER_RESERVOIR_LENGTH_SCALE_OUT"]["value"] = "3.0E+04" MOM_override_dict["OBC_TRACER_RESERVOIR_LENGTH_SCALE_IN"]["value"] = "3000.0" MOM_override_dict["BRUSHCUTTER_MODE"]["value"] = "True" - # Define Specific Segments + # Define Specific Segments for ind, seg in enumerate(boundaries): ind_seg = ind + 1 key_start = "OBC_SEGMENT_00" + str(ind_seg) @@ -2114,10 +2124,11 @@ def setup_run_directory( index_str + ',FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN"' ) - ## Nudget Key + # Nudging Key key_NUDGING = key_start + "_VELOCITY_NUDGING_TIMESCALES" MOM_override_dict[key_NUDGING]["value"] = "0.3, 360.0" - ## Data + + # Data Key key_DATA = key_start + "_DATA" file_num_obc = str( find_MOM6_rectangular_orientation(seg) @@ -2135,9 +2146,14 @@ def setup_run_directory( MOM_override_dict[key_DATA]["value"] + '"' ) + # Tides OBC adjustments if with_tides_rectangular: - MOM_override_dict["OBC_TIDE_ADD_EQ_PHASE"]["value"] = "True" + + # Include internal tide forcing MOM_override_dict["TIDES"]["value"] = "True" + + # OBC tides + MOM_override_dict["OBC_TIDE_ADD_EQ_PHASE"]["value"] = "True" MOM_override_dict["OBC_TIDE_N_CONSTITUENTS"]["value"] = len( self.tidal_constituents ) From be8195af9f5e1981047370d79f51eb81ab280afc Mon Sep 17 00:00:00 2001 From: manishvenu Date: Mon, 30 Sep 2024 16:36:57 -0600 Subject: [PATCH 50/81] Config File: First Attempt --- regional_mom6/regional_mom6.py | 206 ++++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 14 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index a15b2e51..54572741 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -26,7 +26,7 @@ from pathlib import Path import glob from collections import defaultdict - +import json warnings.filterwarnings("ignore") __all__ = [ @@ -35,6 +35,7 @@ "calculate_rectangular_hgrid", "experiment", "segment", + "load_experiment", ] @@ -104,6 +105,87 @@ def find_MOM6_rectangular_orientation(input): else: raise ValueError("Invalid type of Input, can only be string or int.") +## Load Expirement Function + +def load_experiment(config_file_path): + print("Reading from config file....") + with open(config_file_path, "r") as f: + config_dict = json.load(f) + + print("Creating Empty Experiment Object....") + expt = experiment.create_empty() + + print("Setting Default Variables.....") + expt.name = config_dict["name"] + expt.longitude_extent = tuple(config_dict["longitude_extent"]) + expt.latitude_extent = tuple(config_dict["latitude_extent"]) + expt.date_range = (config_dict["date_range"]) + expt.date_range[0] = dt.datetime.strptime(expt.date_range[0], "%Y-%m-%d") + expt.date_range[1] = dt.datetime.strptime(expt.date_range[1], "%Y-%m-%d") + expt.mom_run_dir = Path(config_dict["run_dir"]) + expt.mom_input_dir = Path(config_dict["input_dir"]) + expt.toolpath_dir = Path(config_dict["toolpath_dir"]) + expt.resolution = config_dict["resolution"] + expt.number_vertical_layers = config_dict["number_vertical_layers"] + expt.layer_thickness_ratio = config_dict["layer_thickness_ratio"] + expt.depth = config_dict["depth"] + expt.grid_type = config_dict["grid_type"] + expt.repeat_year_forcing = config_dict["repeat_year_forcing"] + expt.ocean_mask = None + expt.layout = None + expt.min_depth = config_dict["min_depth"] + expt.tidal_constituents = config_dict["tidal_constituents"] + + print("Checking for hgrid and vgrid....") + if os.path.exists(config_dict["hgrid"]): + print("Found") + expt.hgrid = xr.open_dataset(config_dict["hgrid"]) + else: + print("Hgrid not found, creating hgrid") + expt.hgrid = expt._make_hgrid() + if os.path.exists(config_dict["vgrid"]): + print("Found") + expt.vgrid = xr.open_dataset(config_dict["vgrid"]) + else: + print("Vgrid not found, creating vgrid") + expt.vgrid = expt._make_vgrid() + + print("Checking for bathymetry...") + if config_dict["bathymetry"] is not None and os.path.exists(config_dict["bathymetry"]): + print("Found") + expt.bathymetry = xr.open_dataset(config_dict["bathymetry"]) + else: + print("Bathymetry not found. Please provide bathymetry, or call setup_bathymetry method to set up bathymetry.") + + print("Checking for ocean state files....") + found = True + for path in config_dict["ocean_state"]: + if not os.path.exists(path): + foud = False + print("At least one ocean state file not found. Please provide ocean state files, or call setup_ocean_state_boundaries method to set up ocean state.") + break + if found: + print("Found") + found = True + print("Checking for initial condition files....") + for path in config_dict["initial_conditions"]: + if not os.path.exists(path): + print("At least one initial condition file not found. Please provide initial condition files, or call setup_initial_condition method to set up initial condition.") + break + if found: + print("Found") + found = True + print("Checking for tides files....") + for path in config_dict["tides"]: + if not os.path.exists(path): + print("At least one tides file not found. If you would like tides, call setup_tides_boundaries method to set up tides") + break + if found: + print("Found") + found = True + + return expt + ## Auxiliary functions @@ -525,6 +607,26 @@ class experiment: minimum_depth (Optional[int]): The minimum depth in meters of a grid cell allowed before it is masked out and treated as land. """ + @classmethod + def create_empty(self): + + + expt = self( + longitude_extent=None, + latitude_extent=None, + date_range=None, + resolution=None, + number_vertical_layers=None, + layer_thickness_ratio=None, + depth=None, + minimum_depth=None, + mom_run_dir=None, + mom_input_dir=None, + toolpath_dir=None, + create_empty = True + ) + return expt + def __init__( self, *, @@ -543,8 +645,16 @@ def __init__( read_existing_grids=False, minimum_depth=4, tidal_constituents=["M2"], + create_empty = False, + name = None ): + if create_empty: + return + + # ## Set up the experiment with no config file ## in case list was given, convert to tuples + if name is not None: + self.name = name self.longitude_extent = tuple(longitude_extent) self.latitude_extent = tuple(latitude_extent) self.date_range = tuple(date_range) @@ -572,6 +682,7 @@ def __init__( minimum_depth # Minimum depth. Shallower water will be masked out. ) self.tidal_constituents = tidal_constituents + if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -601,6 +712,9 @@ def __init__( if not input_rundir.exists(): input_rundir.symlink_to(self.mom_run_dir.resolve()) + def __str__(self) -> str: + return json.dumps(self.write_config_file(export = False, quiet = True), indent=4) + def __getattr__(self, name): available_methods = [ method for method in dir(self) if not method.startswith("__") @@ -714,10 +828,11 @@ def ocean_state_boundaries(self): ocean_state_path = self.mom_input_dir / "forcing" try: # Use glob to find all tides files - patterns = ["forcing_*", "weights/bi*"] + patterns = ["forcing_*", "weights/bi*",] all_files = [] for pattern in patterns: all_files.extend(glob.glob(os.path.join(ocean_state_path, pattern))) + all_files.extend(glob.glob(os.path.join(self.mom_input_dir, pattern))) if len(all_files) == 0: return "No ocean state files set up yet (or files misplaced from {}). Call `setup_ocean_state_boundaries` method to set up ocean state.".format( @@ -725,8 +840,8 @@ def ocean_state_boundaries(self): ) # Open the files as xarray datasets - datasets = [xr.open_dataset(file) for file in all_files] - return datasets + # datasets = [xr.open_dataset(file) for file in all_files] + return all_files except: return "Error retrieving ocean state files" @@ -742,6 +857,7 @@ def tides_boundaries(self): all_files = [] for pattern in patterns: all_files.extend(glob.glob(os.path.join(tides_path, pattern))) + all_files.extend(glob.glob(os.path.join(self.mom_input_dir, pattern))) if len(all_files) == 0: return "No tides files set up yet (or files misplaced from {}). Call `setup_tides_boundaries` method to set up tides.".format( @@ -749,8 +865,8 @@ def tides_boundaries(self): ) # Open the files as xarray datasets - datasets = [xr.open_dataset(file) for file in all_files] - return datasets + # datasets = [xr.open_dataset(file) for file in all_files] + return all_files except: return "Error retrieving tides files" @@ -769,8 +885,8 @@ def era5(self): ) # Open the files as xarray datasets - datasets = [xr.open_dataset(file) for file in all_files] - return datasets + # datasets = [xr.open_dataset(file) for file in all_files] + return all_files except: return "Error retrieving ERA5 files" @@ -779,12 +895,20 @@ def initial_condition(self): """ Read the ic's from disk, and print 'em """ - + forcing_path = self.mom_input_dir / "forcing" try: - ic_tracers = xr.open_dataset(self.mom_input_dir / "forcing/init_tracers.nc") - ic_vel = xr.open_dataset(self.mom_input_dir / "forcing/init_vel.nc") - ic_eta = xr.open_dataset(self.mom_input_dir / "forcing/init_eta.nc") - return [ic_tracers, ic_vel, ic_eta] + all_files = glob.glob(os.path.join(forcing_path, "init_*.nc")) + all_files = glob.glob(os.path.join(self.mom_input_dir , "init_*.nc")) + if len(all_files) == 0: + return "No initial conditions files set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( + forcing_path + ) + + # Open the files as xarray datasets + # datasets = [xr.open_dataset(file) for file in all_files] + # return datasets + + return all_files except: return "No initial condition set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( self.mom_input_dir / "forcing" @@ -798,12 +922,66 @@ def bathymetry_property(self): try: bathy = xr.open_dataset(self.mom_input_dir / "bathymetry.nc") - return [bathy] + #return [bathy] + return str(self.mom_input_dir / "bathymetry.nc") except: return "No bathymetry set up yet (or files misplaced from {}). Call `setup_bathymetry` method to set up bathymetry.".format( self.mom_input_dir ) + def write_config_file(self, export=True, quiet = False): + """ + Write a configuration file for the experiment. This is a simple json file + that contains the expirment object information to allow for reproducibility, to pick up where a user left off, and + to make information about the expirement readable. + """ + if not quiet: + print("Writing Config File.....") + ## check if files exist + vgrid_path = None + hgrid_path = None + if os.path.exists(self.mom_input_dir/"vcoord.nc"): + vgrid_path = self.mom_input_dir/"vcoord.nc" + if os.path.exists(self.mom_input_dir/"hgrid.nc"): + hgrid_path = self.mom_input_dir/"hgrid.nc" + config_dict = { + "name": self.name, + "date_range": [ + self.date_range[0].strftime("%Y-%m-%d"), + self.date_range[1].strftime("%Y-%m-%d"), + ], + "latitude_extent": self.latitude_extent, + "longitude_extent": self.longitude_extent, + "run_dir": str(self.mom_run_dir), + "input_dir": str(self.mom_input_dir), + 'toolpath_dir': str(self.toolpath_dir), + "resolution": self.resolution, + "number_vertical_layers": self.number_vertical_layers, + "layer_thickness_ratio": self.layer_thickness_ratio, + "depth": self.depth, + "grid_type": self.grid_type, + "repeat_year_forcing": self.repeat_year_forcing, + "ocean_mask": self.ocean_mask, + "layout": self.layout, + "min_depth": self.min_depth, + "vgrid": str(vgrid_path), + "hgrid": str(hgrid_path), + "bathymetry": self.bathymetry_property, + "ocean_state": self.ocean_state_boundaries, + "tides": self.tides_boundaries, + "initial_conditions": self.initial_condition, + "tidal_constituents": self.tidal_constituents + } + if export: + with open(self.mom_run_dir/"config.json", "w") as f: + json.dump( + config_dict, + f, + indent=4, + ) + if not quiet: + print("Done.") + return config_dict def setup_initial_condition( self, raw_ic_path, From 1dd7692d13895513a9338275f30bdf14018e3503 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Mon, 30 Sep 2024 16:38:43 -0600 Subject: [PATCH 51/81] Black Formatting --- regional_mom6/regional_mom6.py | 122 +++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 54572741..af883e15 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -26,7 +26,8 @@ from pathlib import Path import glob from collections import defaultdict -import json +import json + warnings.filterwarnings("ignore") __all__ = [ @@ -105,8 +106,10 @@ def find_MOM6_rectangular_orientation(input): else: raise ValueError("Invalid type of Input, can only be string or int.") + ## Load Expirement Function + def load_experiment(config_file_path): print("Reading from config file....") with open(config_file_path, "r") as f: @@ -119,7 +122,7 @@ def load_experiment(config_file_path): expt.name = config_dict["name"] expt.longitude_extent = tuple(config_dict["longitude_extent"]) expt.latitude_extent = tuple(config_dict["latitude_extent"]) - expt.date_range = (config_dict["date_range"]) + expt.date_range = config_dict["date_range"] expt.date_range[0] = dt.datetime.strptime(expt.date_range[0], "%Y-%m-%d") expt.date_range[1] = dt.datetime.strptime(expt.date_range[1], "%Y-%m-%d") expt.mom_run_dir = Path(config_dict["run_dir"]) @@ -151,18 +154,24 @@ def load_experiment(config_file_path): expt.vgrid = expt._make_vgrid() print("Checking for bathymetry...") - if config_dict["bathymetry"] is not None and os.path.exists(config_dict["bathymetry"]): + if config_dict["bathymetry"] is not None and os.path.exists( + config_dict["bathymetry"] + ): print("Found") expt.bathymetry = xr.open_dataset(config_dict["bathymetry"]) else: - print("Bathymetry not found. Please provide bathymetry, or call setup_bathymetry method to set up bathymetry.") + print( + "Bathymetry not found. Please provide bathymetry, or call setup_bathymetry method to set up bathymetry." + ) print("Checking for ocean state files....") found = True for path in config_dict["ocean_state"]: if not os.path.exists(path): foud = False - print("At least one ocean state file not found. Please provide ocean state files, or call setup_ocean_state_boundaries method to set up ocean state.") + print( + "At least one ocean state file not found. Please provide ocean state files, or call setup_ocean_state_boundaries method to set up ocean state." + ) break if found: print("Found") @@ -170,7 +179,9 @@ def load_experiment(config_file_path): print("Checking for initial condition files....") for path in config_dict["initial_conditions"]: if not os.path.exists(path): - print("At least one initial condition file not found. Please provide initial condition files, or call setup_initial_condition method to set up initial condition.") + print( + "At least one initial condition file not found. Please provide initial condition files, or call setup_initial_condition method to set up initial condition." + ) break if found: print("Found") @@ -178,12 +189,14 @@ def load_experiment(config_file_path): print("Checking for tides files....") for path in config_dict["tides"]: if not os.path.exists(path): - print("At least one tides file not found. If you would like tides, call setup_tides_boundaries method to set up tides") + print( + "At least one tides file not found. If you would like tides, call setup_tides_boundaries method to set up tides" + ) break if found: print("Found") found = True - + return expt @@ -610,7 +623,6 @@ class experiment: @classmethod def create_empty(self): - expt = self( longitude_extent=None, latitude_extent=None, @@ -623,10 +635,10 @@ def create_empty(self): mom_run_dir=None, mom_input_dir=None, toolpath_dir=None, - create_empty = True + create_empty=True, ) return expt - + def __init__( self, *, @@ -645,8 +657,8 @@ def __init__( read_existing_grids=False, minimum_depth=4, tidal_constituents=["M2"], - create_empty = False, - name = None + create_empty=False, + name=None, ): if create_empty: return @@ -682,7 +694,7 @@ def __init__( minimum_depth # Minimum depth. Shallower water will be masked out. ) self.tidal_constituents = tidal_constituents - + if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -713,8 +725,8 @@ def __init__( input_rundir.symlink_to(self.mom_run_dir.resolve()) def __str__(self) -> str: - return json.dumps(self.write_config_file(export = False, quiet = True), indent=4) - + return json.dumps(self.write_config_file(export=False, quiet=True), indent=4) + def __getattr__(self, name): available_methods = [ method for method in dir(self) if not method.startswith("__") @@ -828,7 +840,10 @@ def ocean_state_boundaries(self): ocean_state_path = self.mom_input_dir / "forcing" try: # Use glob to find all tides files - patterns = ["forcing_*", "weights/bi*",] + patterns = [ + "forcing_*", + "weights/bi*", + ] all_files = [] for pattern in patterns: all_files.extend(glob.glob(os.path.join(ocean_state_path, pattern))) @@ -898,7 +913,7 @@ def initial_condition(self): forcing_path = self.mom_input_dir / "forcing" try: all_files = glob.glob(os.path.join(forcing_path, "init_*.nc")) - all_files = glob.glob(os.path.join(self.mom_input_dir , "init_*.nc")) + all_files = glob.glob(os.path.join(self.mom_input_dir, "init_*.nc")) if len(all_files) == 0: return "No initial conditions files set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( forcing_path @@ -922,17 +937,17 @@ def bathymetry_property(self): try: bathy = xr.open_dataset(self.mom_input_dir / "bathymetry.nc") - #return [bathy] + # return [bathy] return str(self.mom_input_dir / "bathymetry.nc") except: return "No bathymetry set up yet (or files misplaced from {}). Call `setup_bathymetry` method to set up bathymetry.".format( self.mom_input_dir ) - def write_config_file(self, export=True, quiet = False): + def write_config_file(self, export=True, quiet=False): """ Write a configuration file for the experiment. This is a simple json file - that contains the expirment object information to allow for reproducibility, to pick up where a user left off, and + that contains the expirment object information to allow for reproducibility, to pick up where a user left off, and to make information about the expirement readable. """ if not quiet: @@ -940,40 +955,40 @@ def write_config_file(self, export=True, quiet = False): ## check if files exist vgrid_path = None hgrid_path = None - if os.path.exists(self.mom_input_dir/"vcoord.nc"): - vgrid_path = self.mom_input_dir/"vcoord.nc" - if os.path.exists(self.mom_input_dir/"hgrid.nc"): - hgrid_path = self.mom_input_dir/"hgrid.nc" + if os.path.exists(self.mom_input_dir / "vcoord.nc"): + vgrid_path = self.mom_input_dir / "vcoord.nc" + if os.path.exists(self.mom_input_dir / "hgrid.nc"): + hgrid_path = self.mom_input_dir / "hgrid.nc" config_dict = { - "name": self.name, - "date_range": [ - self.date_range[0].strftime("%Y-%m-%d"), - self.date_range[1].strftime("%Y-%m-%d"), - ], - "latitude_extent": self.latitude_extent, - "longitude_extent": self.longitude_extent, - "run_dir": str(self.mom_run_dir), - "input_dir": str(self.mom_input_dir), - 'toolpath_dir': str(self.toolpath_dir), - "resolution": self.resolution, - "number_vertical_layers": self.number_vertical_layers, - "layer_thickness_ratio": self.layer_thickness_ratio, - "depth": self.depth, - "grid_type": self.grid_type, - "repeat_year_forcing": self.repeat_year_forcing, - "ocean_mask": self.ocean_mask, - "layout": self.layout, - "min_depth": self.min_depth, - "vgrid": str(vgrid_path), - "hgrid": str(hgrid_path), - "bathymetry": self.bathymetry_property, - "ocean_state": self.ocean_state_boundaries, - "tides": self.tides_boundaries, - "initial_conditions": self.initial_condition, - "tidal_constituents": self.tidal_constituents - } + "name": self.name, + "date_range": [ + self.date_range[0].strftime("%Y-%m-%d"), + self.date_range[1].strftime("%Y-%m-%d"), + ], + "latitude_extent": self.latitude_extent, + "longitude_extent": self.longitude_extent, + "run_dir": str(self.mom_run_dir), + "input_dir": str(self.mom_input_dir), + "toolpath_dir": str(self.toolpath_dir), + "resolution": self.resolution, + "number_vertical_layers": self.number_vertical_layers, + "layer_thickness_ratio": self.layer_thickness_ratio, + "depth": self.depth, + "grid_type": self.grid_type, + "repeat_year_forcing": self.repeat_year_forcing, + "ocean_mask": self.ocean_mask, + "layout": self.layout, + "min_depth": self.min_depth, + "vgrid": str(vgrid_path), + "hgrid": str(hgrid_path), + "bathymetry": self.bathymetry_property, + "ocean_state": self.ocean_state_boundaries, + "tides": self.tides_boundaries, + "initial_conditions": self.initial_condition, + "tidal_constituents": self.tidal_constituents, + } if export: - with open(self.mom_run_dir/"config.json", "w") as f: + with open(self.mom_run_dir / "config.json", "w") as f: json.dump( config_dict, f, @@ -982,6 +997,7 @@ def write_config_file(self, export=True, quiet = False): if not quiet: print("Done.") return config_dict + def setup_initial_condition( self, raw_ic_path, From 4bf2044291fc09245de1605e9170d77894b5d93f Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 09:52:32 -0600 Subject: [PATCH 52/81] Start of Testing --- regional_mom6/regional_mom6.py | 58 +++++++--- tests/test_pr_12.py | 163 +++++++++++++++++++++++++++ tests/test_tides_functions_config.py | 117 ------------------- 3 files changed, 204 insertions(+), 134 deletions(-) create mode 100644 tests/test_pr_12.py delete mode 100644 tests/test_tides_functions_config.py diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index af883e15..d1bb603f 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -621,21 +621,44 @@ class experiment: """ @classmethod - def create_empty(self): - + def create_empty( + self, + longitude_extent=None, + latitude_extent=None, + date_range=None, + resolution=None, + number_vertical_layers=None, + layer_thickness_ratio=None, + depth=None, + mom_run_dir=None, + mom_input_dir=None, + toolpath_dir=None, + grid_type="even_spacing", + repeat_year_forcing=False, + minimum_depth=4, + tidal_constituents=["M2"], + name=None, + ): + """ + Substitute init method to create an empty expirement object, with the opportunity to override whatever values wanted. + """ expt = self( - longitude_extent=None, - latitude_extent=None, - date_range=None, - resolution=None, - number_vertical_layers=None, - layer_thickness_ratio=None, - depth=None, - minimum_depth=None, - mom_run_dir=None, - mom_input_dir=None, - toolpath_dir=None, + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=resolution, + number_vertical_layers=number_vertical_layers, + layer_thickness_ratio=layer_thickness_ratio, + depth=depth, + minimum_depth=minimum_depth, + mom_run_dir=mom_run_dir, + mom_input_dir=mom_input_dir, + toolpath_dir=toolpath_dir, create_empty=True, + grid_type=grid_type, + repeat_year_forcing=repeat_year_forcing, + tidal_constituents=tidal_constituents, + name=name, ) return expt @@ -660,6 +683,11 @@ def __init__( create_empty=False, name=None, ): + + # Creates empty experiment object for testing and experienced user manipulation. + # Kinda seems like a logical spinoff of this is to divorce the hgrid/vgrid creation from the experiment object initialization. + # Probably more of a CS workflow. That way read_existing_grids could be a function on its own, which ties in better with + # For now, check out the create_empty method for more explanation if create_empty: return @@ -3362,10 +3390,6 @@ def encode_tidal_files_and_output(self, ds, filename): ) if "z" in ds.coords: ds = ds.rename({"z": f"nz_{self.segment_name}"}) - if self.orientation in ["south", "north"]: - ds = ds.rename({"locations": f"nx_{self.segment_name}"}) - elif self.orientation in ["west", "east"]: - ds = ds.rename({"locations": f"ny_{self.segment_name}"}) ## Perform Encoding ## for v in ds: diff --git a/tests/test_pr_12.py b/tests/test_pr_12.py new file mode 100644 index 00000000..ee03563a --- /dev/null +++ b/tests/test_pr_12.py @@ -0,0 +1,163 @@ +""" +Test suite for +""" + +import regional_mom6 as rmom6 +import os +import pytest +import logging +from pathlib import Path +import xarray as xr +import numpy as np +from test_expt_class import generate_silly_coords, number_of_gridpoints +IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" + + +class TestAll: + @classmethod + def setup_class(self, tmp_path): # tmp_path is a pytest fixture + expt_name = "testing" + ## User-1st, test if we can even read the angled nc files. + self.dump_files_dir = tmp_path + self.expt = rmom6.experiment.create_empty(name = expt_name, mom_input_dir=self.dump_files_dir, mom_run_dir=self.dump_files_dir) + + @pytest.fixture(scope="module") + def dummy_h_tidal_data(self): + nx = 2160 + ny = 1081 + nc = 15 + nct = 4 + + # Define tidal constituents + con_list = [ + "m2 ", "s2 ", "n2 ", "k2 ", "k1 ", "o1 ", "p1 ", "q1 ", + "mm ", "mf ", "m4 ", "mn4 ", "ms4 ", "2n2 ", "s1 " + ] + con_data = np.array([list(con) for con in con_list], dtype='S1') + + # Generate random data for the variables + lon_z_data = np.random.rand(nx, ny) * 360 - 180 # Random longitudes between -180 and 180 + lat_z_data = np.random.rand(nx, ny) * 180 - 90 # Random latitudes between -90 and 90 + ha_data = np.random.rand(nc, nx, ny) + hp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + hRe_data = np.random.rand(nc, nx, ny) + hIm_data = np.random.rand(nc, nx, ny) + + # Create the xarray dataset + ds_h = xr.Dataset( + { + "con": (["nc", "nct"], con_data), + "lon_z": (["nx", "ny"], lon_z_data), + "lat_z": (["nx", "ny"], lat_z_data), + "ha": (["nc", "nx", "ny"], ha_data), + "hp": (["nc", "nx", "ny"], hp_data), + "hRe": (["nc", "nx", "ny"], hRe_data), + "hIm": (["nc", "nx", "ny"], hIm_data), + }, + coords={ + "nc": np.arange(nc), + "nct": np.arange(nct), + "nx": np.arange(nx), + "ny": np.arange(ny), + }, + attrs={ + "type": "Fake OTIS tidal elevation file", + "title": "Fake TPXO9.v1 2018 tidal elevation file", + } + ) + + # Generate random data for the variables for u_tpxo9.v1 + lon_u_data = np.random.rand(nx, ny) * 360 - 180 # Random longitudes between -180 and 180 + lat_u_data = np.random.rand(nx, ny) * 180 - 90 # Random latitudes between -90 and 90 + lon_v_data = np.random.rand(nx, ny) * 360 - 180 # Random longitudes between -180 and 180 + lat_v_data = np.random.rand(nx, ny) * 180 - 90 # Random latitudes between -90 and 90 + Ua_data = np.random.rand(nc, nx, ny) + ua_data = np.random.rand(nc, nx, ny) + up_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + Va_data = np.random.rand(nc, nx, ny) + va_data = np.random.rand(nc, nx, ny) + vp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + URe_data = np.random.rand(nc, nx, ny) + UIm_data = np.random.rand(nc, nx, ny) + VRe_data = np.random.rand(nc, nx, ny) + VIm_data = np.random.rand(nc, nx, ny) + + # Create the xarray dataset for u_tpxo9.v1 + ds_u = xr.Dataset( + { + "con": (["nc", "nct"], con_data), + "lon_u": (["nx", "ny"], lon_u_data), + "lat_u": (["nx", "ny"], lat_u_data), + "lon_v": (["nx", "ny"], lon_v_data), + "lat_v": (["nx", "ny"], lat_v_data), + "Ua": (["nc", "nx", "ny"], Ua_data), + "ua": (["nc", "nx", "ny"], ua_data), + "up": (["nc", "nx", "ny"], up_data), + "Va": (["nc", "nx", "ny"], Va_data), + "va": (["nc", "nx", "ny"], va_data), + "vp": (["nc", "nx", "ny"], vp_data), + "URe": (["nc", "nx", "ny"], URe_data), + "UIm": (["nc", "nx", "ny"], UIm_data), + "VRe": (["nc", "nx", "ny"], VRe_data), + "VIm": (["nc", "nx", "ny"], VIm_data), + }, + coords={ + "nc": np.arange(nc), + "nct": np.arange(nct), + "nx": np.arange(nx), + "ny": np.arange(ny), + }, + attrs={ + "type": "Fake OTIS tidal transport file", + "title": "Fake TPXO9.v1 2018 WE/SN transports/currents file", + } + ) + + + + return ds_h, ds_u + + + + @pytest.mark.skipif( + IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." + ) + def test_tides(self, dummy_tidal_data): + """ + Test the main setup tides function! + """ + + # Generate Fake Tidal Data + ds_h, ds_u = dummy_tidal_data + + # Save to Fake Folder + ds_h.to_netcdf(self.dump_files_dir / "h_fake_tidal_data.nc") + ds_u.to_netcdf(self.dump_files_dir / "u_fake_tidal_data.nc") + + # Set other required variables needed in setup_tides + + # Lat Long + self.expt.longitude_extent = (-5, 5) + self.expt.latitude_extent = (0, 10) + + # Dates + self.expt.date_range = ("2000-01-01", "2000-01-02") + + # Generate Hgrid Data + self.resolution = 0.1 + self.expt._make_hgrid() + + self.expt.setup_tides_boundaries(self.dump_files_dir,"fake_tidal_data") + + + + def test_properties_empty(self): + """ + Test the properties + """ + dss = self.expt.era5 + dss_2 = self.expt.tides_boundaries + dss_3 = self.expt.ocean_state_boundaries + dss_4 = self.expt.initial_condition + dss_5 = self.expt.bathymetry_property + print(dss,dss_2, dss_3, dss_4, dss_5) diff --git a/tests/test_tides_functions_config.py b/tests/test_tides_functions_config.py deleted file mode 100644 index 9bb9b37c..00000000 --- a/tests/test_tides_functions_config.py +++ /dev/null @@ -1,117 +0,0 @@ -import regional_mom6 as rmom6 -import os -import pytest -import logging -from pathlib import Path - -IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" - - -class TestAll: - @classmethod - def setup_class(self): - expt_name = "testing" - - latitude_extent = [16.0, 27] - longitude_extent = [192, 209] - - date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] - - ## Place where all your input files go - input_dir = Path( - os.path.join( - "/", - "glade", - "u", - "home", - "manishrv", - "documents", - "nwa12_0.1", - "regional_mom_workflows", - "rm6", - expt_name, - "inputs", - ) - ) - - ## Directory where you'll run the experiment from - run_dir = Path( - os.path.join( - "/", - "glade", - "u", - "home", - "manishrv", - "documents", - "nwa12_0.1", - "regional_mom_workflows", - "rm6", - expt_name, - "run_files", - ) - ) - for path in (run_dir, input_dir): - os.makedirs(str(path), exist_ok=True) - self.glorys_path = os.path.join( - "/", - "glade", - "derecho", - "scratch", - "manishrv", - "inputs_rm6_hawaii", - "glorys", - ) - ## User-1st, test if we can even read the angled nc files. - self.expt = rmom6.experiment( - longitude_extent=longitude_extent, - latitude_extent=latitude_extent, - date_range=date_range, - resolution=0.05, - number_vertical_layers=75, - layer_thickness_ratio=10, - depth=4500, - minimum_depth=5, - mom_run_dir=run_dir, - mom_input_dir=input_dir, - toolpath_dir="", - ) - - @pytest.mark.skipif( - IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." - ) - def test_initial_condition(self): - ocean_varnames = { - "time": "time", - "yh": "latitude", - "xh": "longitude", - "zl": "depth", - "eta": "zos", - "u": "uo", - "v": "vo", - "tracers": {"salt": "so", "temp": "thetao"}, - } - - # Set up the initial condition - self.expt.setup_initial_condition( - Path( - os.path.join(self.glorys_path, "ic_unprocessed.nc") - ), # directory where the unprocessed initial condition is stored, as defined earlier - ocean_varnames, - arakawa_grid="A", - ) - dss = self.expt.initial_condition - print(dss) - - @pytest.mark.skipif( - IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." - ) - def test_properties(self): - """ - Test the properties - """ - dss = self.expt.era5 - dss_2 = self.expt.tides_boundaries - dss_3 = self.expt.ocean_state_boundaries - dss_4 = self.expt.initial_condition - dss_5 = self.expt.bathymetry_property - print(dss_2, dss_3, dss_4, dss_5) From f0fcbe9ddf2b4e489ba0151f836b12d302685eba Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 12:12:33 -0600 Subject: [PATCH 53/81] Add testing for pr 12 --- regional_mom6/regional_mom6.py | 111 +++++++---- tests/test_pr_12.py | 350 +++++++++++++++++++++++---------- 2 files changed, 320 insertions(+), 141 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d1bb603f..e6ccad6f 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -120,11 +120,18 @@ def load_experiment(config_file_path): print("Setting Default Variables.....") expt.name = config_dict["name"] - expt.longitude_extent = tuple(config_dict["longitude_extent"]) - expt.latitude_extent = tuple(config_dict["latitude_extent"]) - expt.date_range = config_dict["date_range"] - expt.date_range[0] = dt.datetime.strptime(expt.date_range[0], "%Y-%m-%d") - expt.date_range[1] = dt.datetime.strptime(expt.date_range[1], "%Y-%m-%d") + try: + expt.longitude_extent = tuple(config_dict["longitude_extent"]) + expt.latitude_extent = tuple(config_dict["latitude_extent"]) + except: + expt.longitude_extent = None + expt.latitude_extent = None + try: + expt.date_range = config_dict["date_range"] + expt.date_range[0] = dt.datetime.strptime(expt.date_range[0], "%Y-%m-%d") + expt.date_range[1] = dt.datetime.strptime(expt.date_range[1], "%Y-%m-%d") + except: + expt.date_range = None expt.mom_run_dir = Path(config_dict["run_dir"]) expt.mom_input_dir = Path(config_dict["input_dir"]) expt.toolpath_dir = Path(config_dict["toolpath_dir"]) @@ -144,14 +151,14 @@ def load_experiment(config_file_path): print("Found") expt.hgrid = xr.open_dataset(config_dict["hgrid"]) else: - print("Hgrid not found, creating hgrid") - expt.hgrid = expt._make_hgrid() + print("Hgrid not found, call _make_hgrid when you're ready.") + expt.hgrid = None if os.path.exists(config_dict["vgrid"]): print("Found") expt.vgrid = xr.open_dataset(config_dict["vgrid"]) else: - print("Vgrid not found, creating vgrid") - expt.vgrid = expt._make_vgrid() + print("Vgrid not found, call _make_vgrid when ready") + expt.vgrid = None print("Checking for bathymetry...") if config_dict["bathymetry"] is not None and os.path.exists( @@ -640,26 +647,44 @@ def create_empty( name=None, ): """ - Substitute init method to create an empty expirement object, with the opportunity to override whatever values wanted. + Substitute init method to creates an empty expirement object, with the opportunity to override whatever values wanted. """ expt = self( - longitude_extent=longitude_extent, - latitude_extent=latitude_extent, - date_range=date_range, - resolution=resolution, - number_vertical_layers=number_vertical_layers, - layer_thickness_ratio=layer_thickness_ratio, - depth=depth, - minimum_depth=minimum_depth, - mom_run_dir=mom_run_dir, - mom_input_dir=mom_input_dir, - toolpath_dir=toolpath_dir, + longitude_extent=None, + latitude_extent=None, + date_range=None, + resolution=None, + number_vertical_layers=None, + layer_thickness_ratio=None, + depth=None, + minimum_depth=None, + mom_run_dir=None, + mom_input_dir=None, + toolpath_dir=None, create_empty=True, - grid_type=grid_type, - repeat_year_forcing=repeat_year_forcing, - tidal_constituents=tidal_constituents, - name=name, - ) + grid_type=None, + repeat_year_forcing=None, + tidal_constituents=None, + name=None, + ) + + expt.name = name + expt.tidal_constituents = tidal_constituents + expt.repeat_year_forcing = repeat_year_forcing + expt.grid_type = grid_type + expt.toolpath_dir = toolpath_dir + expt.mom_run_dir = mom_run_dir + expt.mom_input_dir = mom_input_dir + expt.min_depth = minimum_depth + expt.depth = depth + expt.layer_thickness_ratio = layer_thickness_ratio + expt.number_vertical_layers = number_vertical_layers + expt.resolution = resolution + expt.date_range = date_range + expt.latitude_extent = latitude_extent + expt.longitude_extent = longitude_extent + expt.ocean_mask = None + expt.layout = None return expt def __init__( @@ -693,8 +718,7 @@ def __init__( # ## Set up the experiment with no config file ## in case list was given, convert to tuples - if name is not None: - self.name = name + self.expt_name = name self.longitude_extent = tuple(longitude_extent) self.latitude_extent = tuple(latitude_extent) self.date_range = tuple(date_range) @@ -972,7 +996,7 @@ def bathymetry_property(self): self.mom_input_dir ) - def write_config_file(self, export=True, quiet=False): + def write_config_file(self, path=None, export=True, quiet=False): """ Write a configuration file for the experiment. This is a simple json file that contains the expirment object information to allow for reproducibility, to pick up where a user left off, and @@ -987,12 +1011,17 @@ def write_config_file(self, export=True, quiet=False): vgrid_path = self.mom_input_dir / "vcoord.nc" if os.path.exists(self.mom_input_dir / "hgrid.nc"): hgrid_path = self.mom_input_dir / "hgrid.nc" - config_dict = { - "name": self.name, - "date_range": [ + + try: + date_range = [ self.date_range[0].strftime("%Y-%m-%d"), self.date_range[1].strftime("%Y-%m-%d"), - ], + ] + except: + date_range = None + config_dict = { + "name": self.expt_name, + "date_range": date_range, "latitude_extent": self.latitude_extent, "longitude_extent": self.longitude_extent, "run_dir": str(self.mom_run_dir), @@ -1016,7 +1045,11 @@ def write_config_file(self, export=True, quiet=False): "tidal_constituents": self.tidal_constituents, } if export: - with open(self.mom_run_dir / "config.json", "w") as f: + if path is not None: + export_path = path + else: + export_path = self.mom_run_dir / "config.json" + with open(export_path, "w") as f: json.dump( config_dict, f, @@ -2439,6 +2472,7 @@ def change_MOM_parameter( self, param_name, param_value=None, comment=None, delete=False ): """ + *Requires already copied MOM parameter files in the run directory* Change a parameter in the MOM_input or MOM_override file. Returns original value if there was one. If delete is specified, ONLY MOM_override version will be deleted. Deleting from MOM_input is not safe. If the parameter does not exist, it will be added to the file. if delete is set to True, the parameter will be removed. @@ -2499,6 +2533,11 @@ def read_MOM_file_as_dict(self, filename): # Default information for each parameter default_layout = {"value": None, "override": False, "comment": None} + + if not os.path.exists(os.path.join(self.mom_run_dir, filename)): + raise ValueError( + f"File {filename} does not exist in the run directory {self.mom_run_dir}" + ) with open(os.path.join(self.mom_run_dir, filename), "r") as file: lines = file.readlines() @@ -3390,6 +3429,10 @@ def encode_tidal_files_and_output(self, ds, filename): ) if "z" in ds.coords: ds = ds.rename({"z": f"nz_{self.segment_name}"}) + if self.orientation in ["south", "north"]: + ds = ds.rename({"locations": f"nx_{self.segment_name}"}) + elif self.orientation in ["west", "east"]: + ds = ds.rename({"locations": f"ny_{self.segment_name}"}) ## Perform Encoding ## for v in ds: diff --git a/tests/test_pr_12.py b/tests/test_pr_12.py index ee03563a..bce3972d 100644 --- a/tests/test_pr_12.py +++ b/tests/test_pr_12.py @@ -1,5 +1,5 @@ """ -Test suite for +Test suite for everything involed in pr #12 """ import regional_mom6 as rmom6 @@ -9,122 +9,225 @@ from pathlib import Path import xarray as xr import numpy as np -from test_expt_class import generate_silly_coords, number_of_gridpoints +from tests.test_expt_class import generate_silly_coords, number_of_gridpoints +import shutil +import importlib + IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" +@pytest.fixture(scope="module") +def dummy_tidal_data(): + nx = 2160 + ny = 1081 + nc = 15 + nct = 4 + + # Define tidal constituents + con_list = [ + "m2 ", + "s2 ", + "n2 ", + "k2 ", + "k1 ", + "o1 ", + "p1 ", + "q1 ", + "mm ", + "mf ", + "m4 ", + "mn4 ", + "ms4 ", + "2n2 ", + "s1 ", + ] + con_data = np.array([list(con) for con in con_list], dtype="S1") + + # Generate random data for the variables + lon_z_data = np.tile(np.linspace(-180, 180, nx), (ny, 1)).T + lat_z_data = np.tile(np.linspace(-90, 90, ny), (nx, 1)) + ha_data = np.random.rand(nc, nx, ny) + hp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + hRe_data = np.random.rand(nc, nx, ny) + hIm_data = np.random.rand(nc, nx, ny) + + # Create the xarray dataset + ds_h = xr.Dataset( + { + "con": (["nc", "nct"], con_data), + "lon_z": (["nx", "ny"], lon_z_data), + "lat_z": (["nx", "ny"], lat_z_data), + "ha": (["nc", "nx", "ny"], ha_data), + "hp": (["nc", "nx", "ny"], hp_data), + "hRe": (["nc", "nx", "ny"], hRe_data), + "hIm": (["nc", "nx", "ny"], hIm_data), + }, + coords={ + "nc": np.arange(nc), + "nct": np.arange(nct), + "nx": np.arange(nx), + "ny": np.arange(ny), + }, + attrs={ + "type": "Fake OTIS tidal elevation file", + "title": "Fake TPXO9.v1 2018 tidal elevation file", + }, + ) + + # Generate random data for the variables for u_tpxo9.v1 + lon_u_data = ( + np.random.rand(nx, ny) * 360 - 180 + ) # Random longitudes between -180 and 180 + lat_u_data = ( + np.random.rand(nx, ny) * 180 - 90 + ) # Random latitudes between -90 and 90 + lon_v_data = ( + np.random.rand(nx, ny) * 360 - 180 + ) # Random longitudes between -180 and 180 + lat_v_data = ( + np.random.rand(nx, ny) * 180 - 90 + ) # Random latitudes between -90 and 90 + Ua_data = np.random.rand(nc, nx, ny) + ua_data = np.random.rand(nc, nx, ny) + up_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + Va_data = np.random.rand(nc, nx, ny) + va_data = np.random.rand(nc, nx, ny) + vp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + URe_data = np.random.rand(nc, nx, ny) + UIm_data = np.random.rand(nc, nx, ny) + VRe_data = np.random.rand(nc, nx, ny) + VIm_data = np.random.rand(nc, nx, ny) + + # Create the xarray dataset for u_tpxo9.v1 + ds_u = xr.Dataset( + { + "con": (["nc", "nct"], con_data), + "lon_u": (["nx", "ny"], lon_u_data), + "lat_u": (["nx", "ny"], lat_u_data), + "lon_v": (["nx", "ny"], lon_v_data), + "lat_v": (["nx", "ny"], lat_v_data), + "Ua": (["nc", "nx", "ny"], Ua_data), + "ua": (["nc", "nx", "ny"], ua_data), + "up": (["nc", "nx", "ny"], up_data), + "Va": (["nc", "nx", "ny"], Va_data), + "va": (["nc", "nx", "ny"], va_data), + "vp": (["nc", "nx", "ny"], vp_data), + "URe": (["nc", "nx", "ny"], URe_data), + "UIm": (["nc", "nx", "ny"], UIm_data), + "VRe": (["nc", "nx", "ny"], VRe_data), + "VIm": (["nc", "nx", "ny"], VIm_data), + }, + coords={ + "nc": np.arange(nc), + "nct": np.arange(nct), + "nx": np.arange(nx), + "ny": np.arange(ny), + }, + attrs={ + "type": "Fake OTIS tidal transport file", + "title": "Fake TPXO9.v1 2018 WE/SN transports/currents file", + }, + ) + + return ds_h, ds_u + + +@pytest.fixture(scope="module") +def dummy_bathymetry_data(): + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + + bathymetry = np.random.random((100, 100)) * (-100) + bathymetry = xr.DataArray( + bathymetry, + dims=["silly_lat", "silly_lon"], + coords={ + "silly_lat": np.linspace( + latitude_extent[0] - 5, latitude_extent[1] + 5, 100 + ), + "silly_lon": np.linspace( + longitude_extent[0] - 5, longitude_extent[1] + 5, 100 + ), + }, + ) + bathymetry.name = "silly_depth" + return bathymetry + + class TestAll: @classmethod - def setup_class(self, tmp_path): # tmp_path is a pytest fixture + def setup_class(self): # tmp_path is a pytest fixture expt_name = "testing" ## User-1st, test if we can even read the angled nc files. - self.dump_files_dir = tmp_path - self.expt = rmom6.experiment.create_empty(name = expt_name, mom_input_dir=self.dump_files_dir, mom_run_dir=self.dump_files_dir) + self.dump_files_dir = Path("testing_outputs") + os.makedirs(self.dump_files_dir, exist_ok=True) + self.expt = rmom6.experiment.create_empty( + name=expt_name, + mom_input_dir=self.dump_files_dir, + mom_run_dir=self.dump_files_dir, + ) + + @classmethod + def teardown_class(cls): + shutil.rmtree(cls.dump_files_dir) @pytest.fixture(scope="module") - def dummy_h_tidal_data(self): - nx = 2160 - ny = 1081 - nc = 15 - nct = 4 - - # Define tidal constituents - con_list = [ - "m2 ", "s2 ", "n2 ", "k2 ", "k1 ", "o1 ", "p1 ", "q1 ", - "mm ", "mf ", "m4 ", "mn4 ", "ms4 ", "2n2 ", "s1 " - ] - con_data = np.array([list(con) for con in con_list], dtype='S1') - - # Generate random data for the variables - lon_z_data = np.random.rand(nx, ny) * 360 - 180 # Random longitudes between -180 and 180 - lat_z_data = np.random.rand(nx, ny) * 180 - 90 # Random latitudes between -90 and 90 - ha_data = np.random.rand(nc, nx, ny) - hp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 - hRe_data = np.random.rand(nc, nx, ny) - hIm_data = np.random.rand(nc, nx, ny) - - # Create the xarray dataset - ds_h = xr.Dataset( - { - "con": (["nc", "nct"], con_data), - "lon_z": (["nx", "ny"], lon_z_data), - "lat_z": (["nx", "ny"], lat_z_data), - "ha": (["nc", "nx", "ny"], ha_data), - "hp": (["nc", "nx", "ny"], hp_data), - "hRe": (["nc", "nx", "ny"], hRe_data), - "hIm": (["nc", "nx", "ny"], hIm_data), - }, - coords={ - "nc": np.arange(nc), - "nct": np.arange(nct), - "nx": np.arange(nx), - "ny": np.arange(ny), - }, - attrs={ - "type": "Fake OTIS tidal elevation file", - "title": "Fake TPXO9.v1 2018 tidal elevation file", - } - ) + def full_legit_expt_setup(self, dummy_bathymetry_data): - # Generate random data for the variables for u_tpxo9.v1 - lon_u_data = np.random.rand(nx, ny) * 360 - 180 # Random longitudes between -180 and 180 - lat_u_data = np.random.rand(nx, ny) * 180 - 90 # Random latitudes between -90 and 90 - lon_v_data = np.random.rand(nx, ny) * 360 - 180 # Random longitudes between -180 and 180 - lat_v_data = np.random.rand(nx, ny) * 180 - 90 # Random latitudes between -90 and 90 - Ua_data = np.random.rand(nc, nx, ny) - ua_data = np.random.rand(nc, nx, ny) - up_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 - Va_data = np.random.rand(nc, nx, ny) - va_data = np.random.rand(nc, nx, ny) - vp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 - URe_data = np.random.rand(nc, nx, ny) - UIm_data = np.random.rand(nc, nx, ny) - VRe_data = np.random.rand(nc, nx, ny) - VIm_data = np.random.rand(nc, nx, ny) - - # Create the xarray dataset for u_tpxo9.v1 - ds_u = xr.Dataset( - { - "con": (["nc", "nct"], con_data), - "lon_u": (["nx", "ny"], lon_u_data), - "lat_u": (["nx", "ny"], lat_u_data), - "lon_v": (["nx", "ny"], lon_v_data), - "lat_v": (["nx", "ny"], lat_v_data), - "Ua": (["nc", "nx", "ny"], Ua_data), - "ua": (["nc", "nx", "ny"], ua_data), - "up": (["nc", "nx", "ny"], up_data), - "Va": (["nc", "nx", "ny"], Va_data), - "va": (["nc", "nx", "ny"], va_data), - "vp": (["nc", "nx", "ny"], vp_data), - "URe": (["nc", "nx", "ny"], URe_data), - "UIm": (["nc", "nx", "ny"], UIm_data), - "VRe": (["nc", "nx", "ny"], VRe_data), - "VIm": (["nc", "nx", "ny"], VIm_data), - }, - coords={ - "nc": np.arange(nc), - "nct": np.arange(nct), - "nx": np.arange(nx), - "ny": np.arange(ny), - }, - attrs={ - "type": "Fake OTIS tidal transport file", - "title": "Fake TPXO9.v1 2018 WE/SN transports/currents file", - } - ) + expt_name = "testing" + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] - return ds_h, ds_u + ## Place where all your input files go + input_dir = Path( + os.path.join( + expt_name, + "inputs", + ) + ) + ## Directory where you'll run the experiment from + run_dir = Path( + os.path.join( + expt_name, + "run_files", + ) + ) + data_path = Path("data") + for path in (run_dir, input_dir, data_path): + os.makedirs(str(path), exist_ok=True) + bathy_path = data_path / "bathymetry.nc" + bathymetry = dummy_bathymetry_data + bathymetry.to_netcdf(bathy_path) + self.glorys_path = bathy_path + ## User-1st, test if we can even read the angled nc files. + expt = rmom6.experiment( + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=0.05, + number_vertical_layers=75, + layer_thickness_ratio=10, + depth=4500, + minimum_depth=5, + mom_run_dir=run_dir, + mom_input_dir=input_dir, + toolpath_dir="", + ) + return expt + def test_dummy_test_expt_setup(self, full_legit_expt_setup): + assert str(full_legit_expt_setup) - @pytest.mark.skipif( - IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." - ) + # @pytest.mark.skipif( + # IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." + # ) def test_tides(self, dummy_tidal_data): """ - Test the main setup tides function! + Test the main setup tides function! """ # Generate Fake Tidal Data @@ -137,19 +240,52 @@ def test_tides(self, dummy_tidal_data): # Set other required variables needed in setup_tides # Lat Long - self.expt.longitude_extent = (-5, 5) - self.expt.latitude_extent = (0, 10) - + self.expt.longitude_extent = (-5, 5) + self.expt.latitude_extent = (0, 30) + # Grid Type + self.expt.grid_type = "even_spacing" # Dates self.expt.date_range = ("2000-01-01", "2000-01-02") - + self.expt.segments = [] # Generate Hgrid Data - self.resolution = 0.1 - self.expt._make_hgrid() + self.expt.resolution = 0.1 + self.expt.hgrid = self.expt._make_hgrid() + # Create Forcing Folder + os.makedirs(self.dump_files_dir / "forcing", exist_ok=True) - self.expt.setup_tides_boundaries(self.dump_files_dir,"fake_tidal_data") + self.expt.setup_tides_boundaries(self.dump_files_dir, "fake_tidal_data.nc") + def test_read_write_config(self): + """ + Test the read and write config functions + """ + # Write the config + self.expt.write_config_file(path=self.dump_files_dir / "config.yaml") + # Read the config + expt = rmom6.load_experiment(self.dump_files_dir / "config.yaml") + # Check if the config is the same + assert str(self.expt) == str(expt) + def test_change_MOM_parameter(self): + """ + Test the change MOM parameter function, as well as read_MOM_file and write_MOM_file under the hood. + """ + + # Copy over the MOM Files to the dump_files_dir + base_run_dir = Path( + os.path.join( + importlib.resources.files("regional_mom6").parent, + "demos", + "premade_run_directories", + ) + ) + shutil.copytree( + base_run_dir / "common_files", self.expt.mom_run_dir, dirs_exist_ok=True + ) + self.expt.change_MOM_parameter("OBC_SEGMENT_001", "adasd", "COOL COMMENT") + MOM_override_dict = self.expt.read_MOM_file_as_dict("MOM_override") + assert MOM_override_dict["OBC_SEGMENT_001"]["value"] == "adasd" + assert MOM_override_dict["OBC_SEGMENT_001"]["comment"] == "COOL COMMENT\n" def test_properties_empty(self): """ @@ -160,4 +296,4 @@ def test_properties_empty(self): dss_3 = self.expt.ocean_state_boundaries dss_4 = self.expt.initial_condition dss_5 = self.expt.bathymetry_property - print(dss,dss_2, dss_3, dss_4, dss_5) + print(dss, dss_2, dss_3, dss_4, dss_5) From 34fd055163d19bd2a61965547937127b2bd2f32f Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 13:10:22 -0600 Subject: [PATCH 54/81] Minor bug in config read/write --- regional_mom6/regional_mom6.py | 8 +++++--- tests/test_pr_12.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index e6ccad6f..c8664078 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -119,7 +119,7 @@ def load_experiment(config_file_path): expt = experiment.create_empty() print("Setting Default Variables.....") - expt.name = config_dict["name"] + expt.expt_name = config_dict["name"] try: expt.longitude_extent = tuple(config_dict["longitude_extent"]) expt.latitude_extent = tuple(config_dict["latitude_extent"]) @@ -175,7 +175,7 @@ def load_experiment(config_file_path): found = True for path in config_dict["ocean_state"]: if not os.path.exists(path): - foud = False + found = False print( "At least one ocean state file not found. Please provide ocean state files, or call setup_ocean_state_boundaries method to set up ocean state." ) @@ -186,6 +186,7 @@ def load_experiment(config_file_path): print("Checking for initial condition files....") for path in config_dict["initial_conditions"]: if not os.path.exists(path): + found = False print( "At least one initial condition file not found. Please provide initial condition files, or call setup_initial_condition method to set up initial condition." ) @@ -196,6 +197,7 @@ def load_experiment(config_file_path): print("Checking for tides files....") for path in config_dict["tides"]: if not os.path.exists(path): + found = False print( "At least one tides file not found. If you would like tides, call setup_tides_boundaries method to set up tides" ) @@ -668,7 +670,7 @@ def create_empty( name=None, ) - expt.name = name + expt.expt_name = name expt.tidal_constituents = tidal_constituents expt.repeat_year_forcing = repeat_year_forcing expt.grid_type = grid_type diff --git a/tests/test_pr_12.py b/tests/test_pr_12.py index bce3972d..df735a45 100644 --- a/tests/test_pr_12.py +++ b/tests/test_pr_12.py @@ -219,7 +219,7 @@ def full_legit_expt_setup(self, dummy_bathymetry_data): ) return expt - def test_dummy_test_expt_setup(self, full_legit_expt_setup): + def test_full_legit_expt_setup(self, full_legit_expt_setup): assert str(full_legit_expt_setup) # @pytest.mark.skipif( From 55d169ae45d85a5d0417e3ae48c330af1a191746 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 13:26:50 -0600 Subject: [PATCH 55/81] Remove MOM_input OBC segment specific code --- .../common_files/MOM_input | 33 ------------------- regional_mom6/regional_mom6.py | 3 +- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/demos/premade_run_directories/common_files/MOM_input b/demos/premade_run_directories/common_files/MOM_input index 8b4ccc70..79ea05a3 100755 --- a/demos/premade_run_directories/common_files/MOM_input +++ b/demos/premade_run_directories/common_files/MOM_input @@ -107,30 +107,6 @@ OBC_ZERO_BIHARMONIC = True ! [Boolean] default = False ! viscosity term. OBC_TIDE_N_CONSTITUENTS = 0 ! default = 0 ! Number of tidal constituents being added to the open boundary. -OBC_SEGMENT_001 = "J=0,I=0:N,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_001_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. -OBC_SEGMENT_002 = "J=N,I=N:0,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_002_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. -OBC_SEGMENT_003 = "I=0,J=N:0,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_003_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. -OBC_SEGMENT_004 = "I=N,J=0:N,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_004_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. OBC_TRACER_RESERVOIR_LENGTH_SCALE_OUT = 3.0E+04 ! [m] default = 0.0 ! An effective length scale for restoring the tracer concentration at the ! boundaries to externally imposed values when the flow is exiting the domain. @@ -264,15 +240,6 @@ VELOCITY_CONFIG = "file" ! default = "zero" ! USER - call a user modified routine. VELOCITY_FILE = "forcing/init_vel.nc" ! ! The name of the velocity initial condition file. -OBC_SEGMENT_001_DATA = "U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt)" ! - ! OBC segment docs -OBC_SEGMENT_002_DATA = "U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt)" ! - ! OBC segment docs -OBC_SEGMENT_003_DATA = "U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt)" ! - ! OBC segment docs -OBC_SEGMENT_004_DATA = "U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt)" ! - ! OBC segment docs - ! === module MOM_diag_mediator === NUM_DIAG_COORDS = 1 ! default = 1 ! The number of diagnostic vertical coordinates to use. For each coordinate, an diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index c8664078..fbe02977 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -19,7 +19,6 @@ ap2ep, ep2ap, find_roughly_nearest_ny_nx, - convert_lon_180_to_360, ) import pandas as pd import re @@ -107,7 +106,7 @@ def find_MOM6_rectangular_orientation(input): raise ValueError("Invalid type of Input, can only be string or int.") -## Load Expirement Function +## Load Experiment Function def load_experiment(config_file_path): From 2a882ae0dcb6f75ea6882210aed1dab22c74a86e Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 14:45:07 -0600 Subject: [PATCH 56/81] Responding to @ajbarnes comments --- regional_mom6/regional_mom6.py | 33 ++++++--------------------------- regional_mom6/utils.py | 18 ------------------ 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index fbe02977..e5225043 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -18,7 +18,6 @@ quadrilateral_areas, ap2ep, ep2ap, - find_roughly_nearest_ny_nx, ) import pandas as pd import re @@ -1614,19 +1613,6 @@ def setup_tides_boundaries( constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) ) ) - tidal_360_lon = [ - self.longitude_extent[0], - self.longitude_extent[1], - ] - ny0, nx0 = find_roughly_nearest_ny_nx( - self.latitude_extent[0] - 0.5, tidal_360_lon[0] - 0.5, tpxo_h - ) - ny1, nx1 = find_roughly_nearest_ny_nx( - self.latitude_extent[1] + 0.5, tidal_360_lon[1] + 0.5, tpxo_h - ) - horizontal_subset = dict(ny=slice(ny0, ny1), nx=slice(nx0, nx1)) - - tpxo_h = tpxo_h.isel(**horizontal_subset) h = tpxo_h["ha"] * np.exp(-1j * np.radians(tpxo_h["hp"])) tpxo_h["hRe"] = np.real(h) @@ -1635,8 +1621,7 @@ def setup_tides_boundaries( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) .isel( - constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), - **horizontal_subset, + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) ) ) tpxo_u["ua"] *= 0.01 # convert to m/s @@ -1647,8 +1632,7 @@ def setup_tides_boundaries( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) .isel( - constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), - **horizontal_subset, + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) ) ) tpxo_v["va"] *= 0.01 # convert to m/s @@ -2364,18 +2348,13 @@ def setup_run_directory( ## Position and Config key_POSITION = key_start if find_MOM6_rectangular_orientation(seg) == 1: - j_str = "0" - i_str = "0:N" + index_str = '"J=0,I=0:N' elif find_MOM6_rectangular_orientation(seg) == 2: - j_str = "N" - i_str = "N:0" + index_str = '"J=N,I=N:0' elif find_MOM6_rectangular_orientation(seg) == 3: - j_str = "N:0" - i_str = "0" + index_str = '"I=0,J=N:0' elif find_MOM6_rectangular_orientation(seg) == 4: - j_str = "0:N" - i_str = "N" - index_str = '"J={},I={}'.format(j_str, i_str) + index_str = '"I=N,J=0:N' MOM_override_dict[key_POSITION]["value"] = ( index_str + ',FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN"' ) diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index ed70266d..447a2e4f 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -294,21 +294,3 @@ def ep2ap(SEMA, ECC, INC, PHA): vp = -np.angle(cv) return ua, va, up, vp - - -def find_roughly_nearest_ny_nx(lat, lon, ds): - """ - Accepts a lat lon and returns a ROUGH closest ny,nx. in ds - """ - ny = (np.abs(ds.lat.values - lat)).argmin(axis=1)[ - 0 - ] # We're looking for an nx, I know it's not exact, but this works - nx = (np.abs(ds.lon.values - lon)).argmin(axis=0)[0] - return ny, nx - - -def convert_lon_180_to_360(lon): - """ - Converts a longitude from -180 to 180 to 0 to 360 - """ - return lon + 180 From 874e012ebdc8eb91dbd0324305ceecef9beebf67 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Tue, 1 Oct 2024 14:46:43 -0600 Subject: [PATCH 57/81] remove horizontal subsets from tides --- regional_mom6/regional_mom6.py | 72 ++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 0b1d4c15..50539dcb 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -208,14 +208,14 @@ def get_glorys_data( path = os.path.join(download_path) if modify_existing: - file = open(os.path.join(path, "get_glorysdata.sh"), "r") + file = open(os.path.join(path, "get_glorys_data.sh"), "r") lines = file.readlines() file.close() else: lines = ["#!/bin/bash\n"] - file = open(os.path.join(path, "get_glorysdata.sh"), "w") + file = open(os.path.join(path, "get_glorys_data.sh"), "w") lines.append( f""" @@ -1003,7 +1003,7 @@ def get_glorys_rectangular( self, raw_boundaries_path, boundaries=["south", "north", "west", "east"] ): """ - This function is a wrapper for `get_glorys_data`, calling this function once for each of the rectangular boundary segments and the initial condition. For more complex boundary shapes, call `get_glorys_data` directly for each of your boundaries that aren't parallel to lines of constant latitude or longitude. + This function is a wrapper for `get_glorys_data`, calling this function once for each of the rectangular boundary segments and the initial condition. For more complex boundary shapes, call `get_glorys_data` directly for each of your boundaries that aren't parallel to lines of constant latitude or longitude. For example, for an angled Northern boundary that spans multiple latitudes, you'll need to download a wider rectangle containing the entire boundary. args: raw_boundaries_path (str): Path to the directory containing the raw boundary forcing files. @@ -1061,7 +1061,7 @@ def get_glorys_rectangular( ) return - def setup_ocean_state_rectangular_boundaries( + def setup_ocean_state_boundaries( self, raw_boundaries_path, varnames, @@ -1099,7 +1099,7 @@ def setup_ocean_state_rectangular_boundaries( ) # Now iterate through our four boundaries for orientation in boundaries: - self.setup_ocean_state_simple_boundary( + self.setup_single_boundary( Path( os.path.join( (raw_boundaries_path), (orientation + "_unprocessed.nc") @@ -1113,7 +1113,7 @@ def setup_ocean_state_rectangular_boundaries( arakawa_grid=arakawa_grid, ) - def setup_ocean_state_simple_boundary( + def setup_single_boundary( self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" ): """ @@ -1152,14 +1152,14 @@ def setup_ocean_state_simple_boundary( repeat_year_forcing=self.repeat_year_forcing, ) - seg.rectangular_brushcut() + seg.regrid_velocity_tracers() # Save Segment to Experiment self.segments[orientation] = seg print("Done.") return - def setup_tides_rectangle_boundaries( + def setup_boundary_tide( self, path_to_td, tidal_filename, tidal_constituents="read_from_expt_init" ): """ @@ -1215,15 +1215,6 @@ def setup_tides_rectangle_boundaries( self.longitude_extent[0], self.longitude_extent[1], ] - ny0, nx0 = find_roughly_nearest_ny_nx( - self.latitude_extent[0] - 0.5, tidal_360_lon[0] - 0.5, tpxo_h - ) - ny1, nx1 = find_roughly_nearest_ny_nx( - self.latitude_extent[1] + 0.5, tidal_360_lon[1] + 0.5, tpxo_h - ) - horizontal_subset = dict(ny=slice(ny0, ny1), nx=slice(nx0, nx1)) - - tpxo_h = tpxo_h.isel(**horizontal_subset) h = tpxo_h["ha"] * np.exp(-1j * np.radians(tpxo_h["hp"])) tpxo_h["hRe"] = np.real(h) @@ -1231,7 +1222,7 @@ def setup_tides_rectangle_boundaries( tpxo_u = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) - .isel(constituent=self.tidal_constituents, **horizontal_subset) + .isel(constituent=self.tidal_constituents) ) tpxo_u["ua"] *= 0.01 # convert to m/s u = tpxo_u["ua"] * np.exp(-1j * np.radians(tpxo_u["up"])) @@ -1240,7 +1231,7 @@ def setup_tides_rectangle_boundaries( tpxo_v = ( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) - .isel(constituent=self.tidal_constituents, **horizontal_subset) + .isel(constituent=self.tidal_constituents) ) tpxo_v["va"] *= 0.01 # convert to m/s v = tpxo_v["va"] * np.exp(-1j * np.radians(tpxo_v["vp"])) @@ -1459,7 +1450,7 @@ def setup_bathymetry( + "If this process hangs it means that the chosen domain might be too big to handle this way. " + "After ensuring access to appropriate computational resources, try calling ESMF " + "directly from a terminal in the input directory via\n\n" - + "mpirun ESMF_Regrid -s bathymetry_original.nc -d bathymetry_unfinished.nc -m bilinear --src_var elevation --dst_var elevation --netcdf4 --src_regional --dst_regional\n\n" + + "mpirun -np `NUMBER_OF_CPUS` ESMF_Regrid -s bathymetry_original.nc -d bathymetry_unfinished.nc -m bilinear --src_var elevation --dst_var elevation --netcdf4 --src_regional --dst_regional\n\n" + "For details see https://xesmf.readthedocs.io/en/latest/large_problems_on_HPC.html\n\n" + "Afterwards, we run 'tidy_bathymetry' method to skip the expensive interpolation step, and finishing metadata, encoding and cleanup." ) @@ -2263,7 +2254,7 @@ def coords(self): - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) - Original Code was sourced from: + Code adapted from: Author(s): GFDL, James Simkins, Rob Cermak, etc.. Year: 2022 Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" @@ -2285,7 +2276,7 @@ def coords(self): rcoord.attrs["perpendicular"] = "ny" rcoord.attrs["parallel"] = "nx" rcoord.attrs["axis_to_expand"] = ( - 2 ## Need to keep track of which axis the 'main' coordinate corresponds to for rectangular_brushcut on when re-adding the 'secondary' axis + 2 ## Need to keep track of which axis the 'main' coordinate corresponds to when re-adding the 'secondary' axis ) rcoord.attrs["locations_name"] = ( f"nx_{self.segment_name}" # Legacy name of nx_... was locations. This provides a clear transform in regrid_tides @@ -2335,10 +2326,29 @@ def coords(self): return rcoord - def rectangular_brushcut(self): + def rotate(self,u,v): + # Make docstring + + """ + Rotate the velocities to the grid orientation. + + Args: + u (xarray.DataArray): The u-component of the velocity. + v (xarray.DataArray): The v-component of the velocity. + + Returns: + Tuple[xarray.DataArray, xarray.DataArray]: The rotated u and v components of the velocity. """ - Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary - is a simple Northern, Southern, Eastern, or Western boundary. + + angle = self.coords.angle.values * np.pi / 180 + u_rot = u*np.cos(angle) - v*np.sin(angle) + v_rot = u*np.sin(angle) + v*np.cos(angle) + return u_rot, v_rot + + + def regrid_velocity_tracers(self): + """ + Cut out and interpolate the velocities and tracers """ rawseg = xr.open_dataset(self.infile, decode_times=False, engine="netcdf4") @@ -2389,13 +2399,17 @@ def rectangular_brushcut(self): / f"weights/bilinear_tracer_weights_{self.orientation}.nc", ) - segment_out = xr.merge( - [ - regridder_velocity( + velocities_out = regridder_velocity( rawseg[[self.u, self.v]].rename( {self.xq: "lon", self.yq: "lat"} ) - ), + ) + + velocities_out["u"], velocities_out["v"] = self.rotate(velocities_out["u"], velocities_out["v"]) + + segment_out = xr.merge( + [ + velocities_out, regridder_tracer( rawseg[ [self.eta] + [self.tracers[i] for i in self.tracers] From 1b1532d411d4177afb35c1972cbcd12d8aceada2 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 15:06:29 -0600 Subject: [PATCH 58/81] Black formatting --- regional_mom6/regional_mom6.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index e65386d7..96484922 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1048,7 +1048,7 @@ def write_config_file(self, path=None, export=True, quiet=False): if path is not None: export_path = path else: - export_path = self.mom_run_dir / "config.json" + export_path = self.mom_run_dir / "rmom6_config.json" with open(export_path, "w") as f: json.dump( config_dict, @@ -1395,7 +1395,7 @@ def get_glorys_rectangular( self, raw_boundaries_path, boundaries=["south", "north", "west", "east"] ): """ - This function is a wrapper for `get_glorys_data`, calling this function once for each of the rectangular boundary segments and the initial condition. For more complex boundary shapes, call `get_glorys_data` directly for each of your boundaries that aren't parallel to lines of constant latitude or longitude. For example, for an angled Northern boundary that spans multiple latitudes, you'll need to download a wider rectangle containing the entire boundary. + This function is a wrapper for `get_glorys_data`, calling this function once for each of the rectangular boundary segments and the initial condition. For more complex boundary shapes, call `get_glorys_data` directly for each of your boundaries that aren't parallel to lines of constant latitude or longitude. For example, for an angled Northern boundary that spans multiple latitudes, you'll need to download a wider rectangle containing the entire boundary. args: raw_boundaries_path (str): Path to the directory containing the raw boundary forcing files. @@ -2919,9 +2919,9 @@ def coords(self): return rcoord - def rotate(self,u,v): + def rotate(self, u, v): # Make docstring - + """ Rotate the velocities to the grid orientation. @@ -2934,10 +2934,9 @@ def rotate(self,u,v): """ angle = self.coords.angle.values * np.pi / 180 - u_rot = u*np.cos(angle) - v*np.sin(angle) - v_rot = u*np.sin(angle) + v*np.cos(angle) + u_rot = u * np.cos(angle) - v * np.sin(angle) + v_rot = u * np.sin(angle) + v * np.cos(angle) return u_rot, v_rot - def regrid_velocity_tracers(self): """ @@ -2993,15 +2992,15 @@ def regrid_velocity_tracers(self): ) velocities_out = regridder_velocity( - rawseg[[self.u, self.v]].rename( - {self.xq: "lon", self.yq: "lat"} - ) - ) - - velocities_out["u"], velocities_out["v"] = self.rotate(velocities_out["u"], velocities_out["v"]) + rawseg[[self.u, self.v]].rename({self.xq: "lon", self.yq: "lat"}) + ) + + velocities_out["u"], velocities_out["v"] = self.rotate( + velocities_out["u"], velocities_out["v"] + ) segment_out = xr.merge( - [ + [ velocities_out, regridder_tracer( rawseg[ From 6d05448fa5747f76d7cfaa29239e5417ceb5186a Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 14:45:07 -0600 Subject: [PATCH 59/81] Responding to @ajbarnes comments --- regional_mom6/regional_mom6.py | 33 ++++++--------------------------- regional_mom6/utils.py | 18 ------------------ 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index fbe02977..e5225043 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -18,7 +18,6 @@ quadrilateral_areas, ap2ep, ep2ap, - find_roughly_nearest_ny_nx, ) import pandas as pd import re @@ -1614,19 +1613,6 @@ def setup_tides_boundaries( constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) ) ) - tidal_360_lon = [ - self.longitude_extent[0], - self.longitude_extent[1], - ] - ny0, nx0 = find_roughly_nearest_ny_nx( - self.latitude_extent[0] - 0.5, tidal_360_lon[0] - 0.5, tpxo_h - ) - ny1, nx1 = find_roughly_nearest_ny_nx( - self.latitude_extent[1] + 0.5, tidal_360_lon[1] + 0.5, tpxo_h - ) - horizontal_subset = dict(ny=slice(ny0, ny1), nx=slice(nx0, nx1)) - - tpxo_h = tpxo_h.isel(**horizontal_subset) h = tpxo_h["ha"] * np.exp(-1j * np.radians(tpxo_h["hp"])) tpxo_h["hRe"] = np.real(h) @@ -1635,8 +1621,7 @@ def setup_tides_boundaries( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) .isel( - constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), - **horizontal_subset, + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) ) ) tpxo_u["ua"] *= 0.01 # convert to m/s @@ -1647,8 +1632,7 @@ def setup_tides_boundaries( xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) .isel( - constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents), - **horizontal_subset, + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) ) ) tpxo_v["va"] *= 0.01 # convert to m/s @@ -2364,18 +2348,13 @@ def setup_run_directory( ## Position and Config key_POSITION = key_start if find_MOM6_rectangular_orientation(seg) == 1: - j_str = "0" - i_str = "0:N" + index_str = '"J=0,I=0:N' elif find_MOM6_rectangular_orientation(seg) == 2: - j_str = "N" - i_str = "N:0" + index_str = '"J=N,I=N:0' elif find_MOM6_rectangular_orientation(seg) == 3: - j_str = "N:0" - i_str = "0" + index_str = '"I=0,J=N:0' elif find_MOM6_rectangular_orientation(seg) == 4: - j_str = "0:N" - i_str = "N" - index_str = '"J={},I={}'.format(j_str, i_str) + index_str = '"I=N,J=0:N' MOM_override_dict[key_POSITION]["value"] = ( index_str + ',FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN"' ) diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index ed70266d..447a2e4f 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -294,21 +294,3 @@ def ep2ap(SEMA, ECC, INC, PHA): vp = -np.angle(cv) return ua, va, up, vp - - -def find_roughly_nearest_ny_nx(lat, lon, ds): - """ - Accepts a lat lon and returns a ROUGH closest ny,nx. in ds - """ - ny = (np.abs(ds.lat.values - lat)).argmin(axis=1)[ - 0 - ] # We're looking for an nx, I know it's not exact, but this works - nx = (np.abs(ds.lon.values - lon)).argmin(axis=0)[0] - return ny, nx - - -def convert_lon_180_to_360(lon): - """ - Converts a longitude from -180 to 180 to 0 to 360 - """ - return lon + 180 From 2675021f14b332257ce4cca0891c866ae02b1157 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 15:10:34 -0600 Subject: [PATCH 60/81] Change testing branch --- tests/{test_pr_12.py => test_manish_branch.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_pr_12.py => test_manish_branch.py} (100%) diff --git a/tests/test_pr_12.py b/tests/test_manish_branch.py similarity index 100% rename from tests/test_pr_12.py rename to tests/test_manish_branch.py From 70ef6eb4d9cbd59479da492202b34d55019c592c Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 15:23:46 -0600 Subject: [PATCH 61/81] Additional comments --- regional_mom6/regional_mom6.py | 84 +++++++++++++++++----------------- tests/test_grid_generation.py | 4 +- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index e5225043..c463eafd 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -31,7 +31,7 @@ __all__ = [ "longitude_slicer", "hyperbolictan_thickness_profile", - "calculate_rectangular_hgrid", + "generate_rectangular_hgrid", "experiment", "segment", "load_experiment", @@ -346,17 +346,17 @@ def get_glorys_data( """ buffer = 0.24 # Pads downloads to ensure that interpolation onto desired domain doesn't fail. Default of 0.24 is twice Glorys cell width (12th degree) - path = os.path.join(download_path) + path = Path(download_path) if modify_existing: - file = open(os.path.join(path, "get_glorysdata.sh"), "r") + file = open(Path(path / "get_glorysdata.sh"), "r") lines = file.readlines() file.close() else: lines = ["#!/bin/bash\n"] - file = open(os.path.join(path, "get_glorysdata.sh"), "w") + file = open(Path(path / "get_glorysdata.sh"), "w") lines.append( f""" @@ -484,7 +484,7 @@ def hyperbolictan_thickness_profile(nlayers, ratio, total_depth): return layer_thicknesses -def calculate_rectangular_hgrid(lons, lats): +def generate_rectangular_hgrid(lons, lats): """ Construct a horizontal grid with all the metadata required by MOM6, based on arrays of longitudes (``lons``) and latitudes (``lats``) on the supergrid. @@ -853,7 +853,7 @@ def _make_hgrid(self): self.latitude_extent[0], self.latitude_extent[1], ny ) # latitudes in degrees - hgrid = calculate_rectangular_hgrid(lons, lats) + hgrid = generate_rectangular_hgrid(lons, lats) hgrid.to_netcdf(self.mom_input_dir / "hgrid.nc") return hgrid @@ -898,8 +898,8 @@ def ocean_state_boundaries(self): ] all_files = [] for pattern in patterns: - all_files.extend(glob.glob(os.path.join(ocean_state_path, pattern))) - all_files.extend(glob.glob(os.path.join(self.mom_input_dir, pattern))) + all_files.extend(glob.glob(Path(ocean_state_path / pattern))) + all_files.extend(glob.glob(Path(self.mom_input_dir / pattern))) if len(all_files) == 0: return "No ocean state files set up yet (or files misplaced from {}). Call `setup_ocean_state_boundaries` method to set up ocean state.".format( @@ -923,8 +923,8 @@ def tides_boundaries(self): patterns = ["regrid*", "tu_*", "tz_*"] all_files = [] for pattern in patterns: - all_files.extend(glob.glob(os.path.join(tides_path, pattern))) - all_files.extend(glob.glob(os.path.join(self.mom_input_dir, pattern))) + all_files.extend(glob.glob(Path(tides_path / pattern))) + all_files.extend(glob.glob(Path(self.mom_input_dir / pattern))) if len(all_files) == 0: return "No tides files set up yet (or files misplaced from {}). Call `setup_tides_boundaries` method to set up tides.".format( @@ -945,7 +945,7 @@ def era5(self): era5_path = self.mom_input_dir / "forcing" try: # Use glob to find all *_ERA5.nc files - all_files = glob.glob(os.path.join(era5_path, "*_ERA5.nc")) + all_files = glob.glob(Path(era5_path / "*_ERA5.nc")) if len(all_files) == 0: return "No era5 files set up yet (or files misplaced from {}). Call `setup_era5` method to set up era5.".format( era5_path @@ -964,8 +964,8 @@ def initial_condition(self): """ forcing_path = self.mom_input_dir / "forcing" try: - all_files = glob.glob(os.path.join(forcing_path, "init_*.nc")) - all_files = glob.glob(os.path.join(self.mom_input_dir, "init_*.nc")) + all_files = glob.glob(Path(forcing_path / "init_*.nc")) + all_files = glob.glob(Path(self.mom_input_dir / "init_*.nc")) if len(all_files) == 0: return "No initial conditions files set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( forcing_path @@ -1499,10 +1499,9 @@ def setup_ocean_state_boundaries( for orientation in boundaries: self.setup_ocean_state_boundary( Path( - os.path.join( - (raw_boundaries_path), (orientation + "_unprocessed.nc") + (raw_boundaries_path) / (orientation + "_unprocessed.nc") ) - ), + , varnames, orientation, # The cardinal direction of the boundary find_MOM6_rectangular_orientation( @@ -1607,7 +1606,7 @@ def setup_tides_boundaries( if tidal_constituents != "read_from_expt_init": self.tidal_constituents = tidal_constituents tpxo_h = ( - xr.open_dataset(os.path.join(path_to_td, f"h_{tidal_filename}")) + xr.open_dataset(Path(path_to_td / f"h_{tidal_filename}")) .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) .isel( constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) @@ -1618,7 +1617,7 @@ def setup_tides_boundaries( tpxo_h["hRe"] = np.real(h) tpxo_h["hIm"] = np.imag(h) tpxo_u = ( - xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) + xr.open_dataset(Path(path_to_td / f"u_{tidal_filename}")) .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) .isel( constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) @@ -1629,7 +1628,7 @@ def setup_tides_boundaries( tpxo_u["uRe"] = np.real(u) tpxo_u["uIm"] = np.imag(u) tpxo_v = ( - xr.open_dataset(os.path.join(path_to_td, f"u_{tidal_filename}")) + xr.open_dataset(Path(path_to_td / f"u_{tidal_filename}")) .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) .isel( constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) @@ -2150,11 +2149,11 @@ def setup_run_directory( ## Get the path to the regional_mom package on this computer premade_rundir_path = Path( - os.path.join( - importlib.resources.files("regional_mom6"), - "demos", - "premade_run_directories", - ) + + importlib.resources.files("regional_mom6") / + "demos" / + "premade_run_directories" + ) if not premade_rundir_path.exists(): print("Could not find premade run directories at ", premade_rundir_path) @@ -2163,11 +2162,11 @@ def setup_run_directory( ) premade_rundir_path = Path( - os.path.join( - importlib.resources.files("regional_mom6").parent, - "demos", - "premade_run_directories", - ) + + importlib.resources.files("regional_mom6").parent / + "demos"/ + "premade_run_directories" + ) if not premade_rundir_path.exists(): raise ValueError( @@ -2178,16 +2177,15 @@ def setup_run_directory( print("Found run files. Continuing...") # Define the locations of the directories we'll copy files across from. Base contains most of the files, and overwrite replaces files in the base directory. - base_run_dir = Path(os.path.join(premade_rundir_path, "common_files")) + base_run_dir = Path(premade_rundir_path / "common_files") if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path}.\n\n" + "These files missing might be indicating an error during the package installation!" ) if surface_forcing: - overwrite_run_dir = Path( - os.path.join(premade_rundir_path, f"{surface_forcing}_surface") - ) + overwrite_run_dir = Path(premade_rundir_path / f"{surface_forcing}_surface") + if not overwrite_run_dir.exists(): available = [x for x in premade_rundir_path.iterdir() if x.is_dir()] raise ValueError( @@ -2202,8 +2200,8 @@ def setup_run_directory( tidal_files_exist = any( "tidal" in filename for filename in ( - os.listdir(os.path.join(self.mom_input_dir, "forcing")) - + os.listdir(os.path.join(self.mom_input_dir)) + os.listdir(Path(self.mom_input_dir / "forcing")) + + os.listdir(Path(self.mom_input_dir)) ) ) if not tidal_files_exist: @@ -2514,11 +2512,11 @@ def read_MOM_file_as_dict(self, filename): # Default information for each parameter default_layout = {"value": None, "override": False, "comment": None} - if not os.path.exists(os.path.join(self.mom_run_dir, filename)): + if not os.path.exists(Path(self.mom_run_dir / filename)): raise ValueError( f"File {filename} does not exist in the run directory {self.mom_run_dir}" ) - with open(os.path.join(self.mom_run_dir, filename), "r") as file: + with open(Path(self.mom_run_dir / filename), "r") as file: lines = file.readlines() # Set the default initialization for a new key @@ -2556,7 +2554,7 @@ def write_MOM_file(self, MOM_file_dict): # Replace specific variable values original_MOM_file_dict = MOM_file_dict.pop("original") with open( - os.path.join(self.mom_run_dir, MOM_file_dict["filename"]), "r" + Path(self.mom_run_dir / MOM_file_dict["filename"]), "r" ) as file: lines = file.readlines() for jj in range(len(lines)): @@ -2626,7 +2624,7 @@ def write_MOM_file(self, MOM_file_dict): original_MOM_file_dict[key], ) - with open(os.path.join(self.mom_run_dir, MOM_file_dict["filename"]), "w") as f: + with open(Path(self.mom_run_dir / MOM_file_dict["filename"]), "w") as f: f.writelines(lines) def setup_era5(self, era5_path): @@ -2652,7 +2650,7 @@ def setup_era5(self, era5_path): i for i in range(self.date_range[0].year, self.date_range[1].year + 1) ] # construct a list of all paths for all years to use for open_mfdataset - paths_per_year = [os.path.join(era5_path, fname, year) for year in years] + paths_per_year = [Path(era5_path / fname / year) for year in years] all_files = [] for path in paths_per_year: # Use glob to find all files that match the pattern @@ -3257,8 +3255,8 @@ def regrid_tides( method="nearest_s2d", locstream_out=True, periodic=False, - filename=os.path.join( - self.outfolder, "forcing", f"regrid_{self.segment_name}_tidal_elev.nc" + filename=Path( + self.outfolder / "forcing" / f"regrid_{self.segment_name}_tidal_elev.nc" ), reuse_weights=False, ) @@ -3433,7 +3431,7 @@ def encode_tidal_files_and_output(self, ds, filename): ## Export Files ## ds.to_netcdf( - os.path.join(self.outfolder, "forcing", fname), + Path(self.outfolder / "forcing" / fname), engine="netcdf4", encoding=encoding, unlimited_dims="time", diff --git a/tests/test_grid_generation.py b/tests/test_grid_generation.py index 538cf2ad..d9eea88e 100644 --- a/tests/test_grid_generation.py +++ b/tests/test_grid_generation.py @@ -2,7 +2,7 @@ import pytest from regional_mom6 import hyperbolictan_thickness_profile -from regional_mom6 import calculate_rectangular_hgrid +from regional_mom6 import generate_rectangular_hgrid from regional_mom6 import longitude_slicer from regional_mom6.utils import angle_between @@ -129,7 +129,7 @@ def test_quadrilateral_areas(lat, lon, true_area): ], ) def test_rectangular_hgrid(lat, lon): - assert isinstance(calculate_rectangular_hgrid(lat, lon), xr.Dataset) + assert isinstance(generate_rectangular_hgrid(lat, lon), xr.Dataset) def test_longitude_slicer(): From 35901a8d9875098ccf2e92ef0e2969d40838f80f Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 15:25:37 -0600 Subject: [PATCH 62/81] Black formatting --- regional_mom6/regional_mom6.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index c463eafd..b844598d 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1498,10 +1498,7 @@ def setup_ocean_state_boundaries( # Now iterate through our four boundaries for orientation in boundaries: self.setup_ocean_state_boundary( - Path( - (raw_boundaries_path) / (orientation + "_unprocessed.nc") - ) - , + Path((raw_boundaries_path) / (orientation + "_unprocessed.nc")), varnames, orientation, # The cardinal direction of the boundary find_MOM6_rectangular_orientation( @@ -2149,11 +2146,9 @@ def setup_run_directory( ## Get the path to the regional_mom package on this computer premade_rundir_path = Path( - - importlib.resources.files("regional_mom6") / - "demos" / - "premade_run_directories" - + importlib.resources.files("regional_mom6") + / "demos" + / "premade_run_directories" ) if not premade_rundir_path.exists(): print("Could not find premade run directories at ", premade_rundir_path) @@ -2162,11 +2157,9 @@ def setup_run_directory( ) premade_rundir_path = Path( - - importlib.resources.files("regional_mom6").parent / - "demos"/ - "premade_run_directories" - + importlib.resources.files("regional_mom6").parent + / "demos" + / "premade_run_directories" ) if not premade_rundir_path.exists(): raise ValueError( @@ -2185,7 +2178,7 @@ def setup_run_directory( ) if surface_forcing: overwrite_run_dir = Path(premade_rundir_path / f"{surface_forcing}_surface") - + if not overwrite_run_dir.exists(): available = [x for x in premade_rundir_path.iterdir() if x.is_dir()] raise ValueError( @@ -2553,9 +2546,7 @@ def write_MOM_file(self, MOM_file_dict): """ # Replace specific variable values original_MOM_file_dict = MOM_file_dict.pop("original") - with open( - Path(self.mom_run_dir / MOM_file_dict["filename"]), "r" - ) as file: + with open(Path(self.mom_run_dir / MOM_file_dict["filename"]), "r") as file: lines = file.readlines() for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: From 245066e551f58b74759195e10e1307876789d7eb Mon Sep 17 00:00:00 2001 From: manishvenu <80477243+manishvenu@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:41:51 -0400 Subject: [PATCH 63/81] Merge various quality-of-life changes & tides boundary functions (From GFDL NWA25) into RMOM6 (#12) * Start Setup Tides * First Attempt: RM6 Tides * Clean up setup_tides and adjust rect_boundaries with hard-coded segment num After talking with Ashley, the find_MOM6_orientation will likely be removed * SOFT BREAK: Change Function Names, Add Rough Horiz Subset (See Below) The function names for rectangular and sinmple boundaries were changed because the tides are a kind of boundary function, the old names now give a warning and call the correct function. GFDL had rough horizontal subsetting for the tpxo dataset (probably for efficiency?), implemented in setup_tides_rectangular_boundaries. * Write MOM6 Vars * Add Docstring Cite and Merge Functions Collapsed the *_tidal_dims functions into the encode_tides function in segment, and add citing documentation to the docstrings for now. * Minor Path Function Changes * First Implementation w/ Tides * Minor Edit * Additional Formatting Changes * Additional Debugging * black refomat * Officially change boundary function names to verb names * Minor debugging * Black formatting * Change function name for rect orientation * Remove Greek letters * Black Formatting * Wrap up tides adjustments, includinincluding string to tpxo number conversion * mend * Change functions to verb start * Remove test from github workflow * Formatting * Add properties and change function names * Shifting MOM_input to MOM_override Part 1 * Minor Changes * Add flexible OBC to help fix Issue #8, Move OBC params to MOM Override, Add a change_MOM_parameter function as suggested to be fredc * Deleting MOM_input Indexed OBC Vars This is because we don't want to hard code the 4 boundaries, this is a start of making it so that the indexed stuff is at least for sure in MOM_override so an extra boundary doesn't sneak into MOM. For example if I have three boundaries but 4 OBC_segment data defs, that's kinda sus. * Config File: First Attempt * Black Formatting * Start of Testing * Add testing for pr 12 * Minor bug in config read/write * Remove MOM_input OBC segment specific code * Responding to @ashjbarnes comments * Responding to @ashjbarnes comments * Change testing branch * Additional comments * Black formatting --------- Co-authored-by: Ashley Barnes <53282288+ashjbarnes@users.noreply.github.com> Co-authored-by: ashjbarnes --- .../common_files/MOM_input | 33 - demos/reanalysis-forced.ipynb | 6 +- regional_mom6/regional_mom6.py | 1344 +++++++++++++++-- regional_mom6/utils.py | 117 ++ tests/__init__.py | 0 tests/test_expt_class.py | 4 +- tests/test_grid_generation.py | 4 +- tests/test_manish_branch.py | 299 ++++ 8 files changed, 1669 insertions(+), 138 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_manish_branch.py diff --git a/demos/premade_run_directories/common_files/MOM_input b/demos/premade_run_directories/common_files/MOM_input index 8b4ccc70..79ea05a3 100755 --- a/demos/premade_run_directories/common_files/MOM_input +++ b/demos/premade_run_directories/common_files/MOM_input @@ -107,30 +107,6 @@ OBC_ZERO_BIHARMONIC = True ! [Boolean] default = False ! viscosity term. OBC_TIDE_N_CONSTITUENTS = 0 ! default = 0 ! Number of tidal constituents being added to the open boundary. -OBC_SEGMENT_001 = "J=0,I=0:N,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_001_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. -OBC_SEGMENT_002 = "J=N,I=N:0,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_002_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. -OBC_SEGMENT_003 = "I=0,J=N:0,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_003_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. -OBC_SEGMENT_004 = "I=N,J=0:N,FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN" ! - ! Documentation needs to be dynamic????? -OBC_SEGMENT_004_VELOCITY_NUDGING_TIMESCALES = 0.3, 360.0 ! [days] default = 0.0 - ! Timescales in days for nudging along a segment, for inflow, then outflow. - ! Setting both to zero should behave like SIMPLE obcs for the baroclinic - ! velocities. OBC_TRACER_RESERVOIR_LENGTH_SCALE_OUT = 3.0E+04 ! [m] default = 0.0 ! An effective length scale for restoring the tracer concentration at the ! boundaries to externally imposed values when the flow is exiting the domain. @@ -264,15 +240,6 @@ VELOCITY_CONFIG = "file" ! default = "zero" ! USER - call a user modified routine. VELOCITY_FILE = "forcing/init_vel.nc" ! ! The name of the velocity initial condition file. -OBC_SEGMENT_001_DATA = "U=file:forcing/forcing_obc_segment_001.nc(u),V=file:forcing/forcing_obc_segment_001.nc(v),SSH=file:forcing/forcing_obc_segment_001.nc(eta),TEMP=file:forcing/forcing_obc_segment_001.nc(temp),SALT=file:forcing/forcing_obc_segment_001.nc(salt)" ! - ! OBC segment docs -OBC_SEGMENT_002_DATA = "U=file:forcing/forcing_obc_segment_002.nc(u),V=file:forcing/forcing_obc_segment_002.nc(v),SSH=file:forcing/forcing_obc_segment_002.nc(eta),TEMP=file:forcing/forcing_obc_segment_002.nc(temp),SALT=file:forcing/forcing_obc_segment_002.nc(salt)" ! - ! OBC segment docs -OBC_SEGMENT_003_DATA = "U=file:forcing/forcing_obc_segment_003.nc(u),V=file:forcing/forcing_obc_segment_003.nc(v),SSH=file:forcing/forcing_obc_segment_003.nc(eta),TEMP=file:forcing/forcing_obc_segment_003.nc(temp),SALT=file:forcing/forcing_obc_segment_003.nc(salt)" ! - ! OBC segment docs -OBC_SEGMENT_004_DATA = "U=file:forcing/forcing_obc_segment_004.nc(u),V=file:forcing/forcing_obc_segment_004.nc(v),SSH=file:forcing/forcing_obc_segment_004.nc(eta),TEMP=file:forcing/forcing_obc_segment_004.nc(temp),SALT=file:forcing/forcing_obc_segment_004.nc(salt)" ! - ! OBC segment docs - ! === module MOM_diag_mediator === NUM_DIAG_COORDS = 1 ! default = 1 ! The number of diagnostic vertical coordinates to use. For each coordinate, an diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index 9f1dcf2d..35f7eabe 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -294,14 +294,14 @@ " }\n", "\n", "# Set up the initial condition\n", - "expt.initial_condition(\n", + "expt.setup_initial_condition(\n", " glorys_path / \"ic_unprocessed.nc\", # directory where the unprocessed initial condition is stored, as defined earlier\n", " ocean_varnames,\n", " arakawa_grid=\"A\"\n", " ) \n", "\n", "# Set up the four boundary conditions. Remember that in the glorys_path, we have four boundary files names north_unprocessed.nc etc. \n", - "expt.rectangular_boundaries(\n", + "expt.setup_ocean_state_boundaries(\n", " glorys_path,\n", " ocean_varnames,\n", " boundaries = [\"south\", \"north\", \"west\", \"east\"],\n", @@ -324,7 +324,7 @@ "metadata": {}, "outputs": [], "source": [ - "expt.FRE_tools(layout=(10, 10)) ## Here the tuple defines the processor layout" + "expt.run_FRE_tools(layout=(10, 10)) ## Here the tuple defines the processor layout" ] }, { diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 5c06412d..b844598d 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,20 +14,199 @@ import os import importlib.resources import datetime -from .utils import quadrilateral_areas - +from .utils import ( + quadrilateral_areas, + ap2ep, + ep2ap, +) +import pandas as pd +import re +from pathlib import Path +import glob +from collections import defaultdict +import json warnings.filterwarnings("ignore") __all__ = [ "longitude_slicer", "hyperbolictan_thickness_profile", - "rectangular_hgrid", + "generate_rectangular_hgrid", "experiment", "segment", + "load_experiment", ] +## Mapping Functions + + +def convert_to_tpxo_tidal_constituents(tidal_constituents): + """ + Convert tidal constituents from strings to integers using a dictionary. + + Parameters: + tidal_constituents (list of str): List of tidal constituent names as strings. + + Returns: + list of int: List of tidal constituent indices as integers. + """ + tidal_constituents_tpxo_dict = { + "M2": 0, + "S2": 1, + "N2": 2, + "K2": 3, + "K1": 4, + "O1": 5, + "P1": 6, + "Q1": 7, + "MM": 8, + "MF": 9, + # Only supported tidal bc's + } + + list_of_ints = [] + for tc in tidal_constituents: + try: + list_of_ints.append(tidal_constituents_tpxo_dict[tc]) + except: + raise ValueError( + "Invalid Input. Tidal constituent {} is not supported.".format(tc) + ) + + return list_of_ints + + +def find_MOM6_rectangular_orientation(input): + """ + Convert between MOM6 boundary and the specific segment number needed, or the inverse + """ + direction_dir = { + "south": 1, + "north": 2, + "west": 3, + "east": 4, + } + direction_dir_inv = {v: k for k, v in direction_dir.items()} + + if type(input) == str: + try: + return direction_dir[input] + except: + raise ValueError( + "Invalid Input. Did you spell the direction wrong, it should be lowercase?" + ) + elif type(input) == int: + try: + return direction_dir_inv[input] + except: + raise ValueError("Invalid Input. Did you pick a number 1 through 4?") + else: + raise ValueError("Invalid type of Input, can only be string or int.") + + +## Load Experiment Function + + +def load_experiment(config_file_path): + print("Reading from config file....") + with open(config_file_path, "r") as f: + config_dict = json.load(f) + + print("Creating Empty Experiment Object....") + expt = experiment.create_empty() + + print("Setting Default Variables.....") + expt.expt_name = config_dict["name"] + try: + expt.longitude_extent = tuple(config_dict["longitude_extent"]) + expt.latitude_extent = tuple(config_dict["latitude_extent"]) + except: + expt.longitude_extent = None + expt.latitude_extent = None + try: + expt.date_range = config_dict["date_range"] + expt.date_range[0] = dt.datetime.strptime(expt.date_range[0], "%Y-%m-%d") + expt.date_range[1] = dt.datetime.strptime(expt.date_range[1], "%Y-%m-%d") + except: + expt.date_range = None + expt.mom_run_dir = Path(config_dict["run_dir"]) + expt.mom_input_dir = Path(config_dict["input_dir"]) + expt.toolpath_dir = Path(config_dict["toolpath_dir"]) + expt.resolution = config_dict["resolution"] + expt.number_vertical_layers = config_dict["number_vertical_layers"] + expt.layer_thickness_ratio = config_dict["layer_thickness_ratio"] + expt.depth = config_dict["depth"] + expt.grid_type = config_dict["grid_type"] + expt.repeat_year_forcing = config_dict["repeat_year_forcing"] + expt.ocean_mask = None + expt.layout = None + expt.min_depth = config_dict["min_depth"] + expt.tidal_constituents = config_dict["tidal_constituents"] + + print("Checking for hgrid and vgrid....") + if os.path.exists(config_dict["hgrid"]): + print("Found") + expt.hgrid = xr.open_dataset(config_dict["hgrid"]) + else: + print("Hgrid not found, call _make_hgrid when you're ready.") + expt.hgrid = None + if os.path.exists(config_dict["vgrid"]): + print("Found") + expt.vgrid = xr.open_dataset(config_dict["vgrid"]) + else: + print("Vgrid not found, call _make_vgrid when ready") + expt.vgrid = None + + print("Checking for bathymetry...") + if config_dict["bathymetry"] is not None and os.path.exists( + config_dict["bathymetry"] + ): + print("Found") + expt.bathymetry = xr.open_dataset(config_dict["bathymetry"]) + else: + print( + "Bathymetry not found. Please provide bathymetry, or call setup_bathymetry method to set up bathymetry." + ) + + print("Checking for ocean state files....") + found = True + for path in config_dict["ocean_state"]: + if not os.path.exists(path): + found = False + print( + "At least one ocean state file not found. Please provide ocean state files, or call setup_ocean_state_boundaries method to set up ocean state." + ) + break + if found: + print("Found") + found = True + print("Checking for initial condition files....") + for path in config_dict["initial_conditions"]: + if not os.path.exists(path): + found = False + print( + "At least one initial condition file not found. Please provide initial condition files, or call setup_initial_condition method to set up initial condition." + ) + break + if found: + print("Found") + found = True + print("Checking for tides files....") + for path in config_dict["tides"]: + if not os.path.exists(path): + found = False + print( + "At least one tides file not found. If you would like tides, call setup_tides_boundaries method to set up tides" + ) + break + if found: + print("Found") + found = True + + return expt + + ## Auxiliary functions @@ -77,11 +256,11 @@ def longitude_slicer(data, longitude_extent, longitude_coords): ## Find a corresponding value for the intended domain midpoint in our data. ## It's assumed that data has equally-spaced longitude values. - λ = data[lon].data - dλ = λ[1] - λ[0] + lons = data[lon].data + dlons = lons[1] - lons[0] assert np.allclose( - np.diff(λ), dλ * np.ones(np.size(λ) - 1) + np.diff(lons), dlons * np.ones(np.size(lons) - 1) ), "provided longitude coordinate must be uniformly spaced" for i in range(-1, 2, 1): @@ -145,9 +324,6 @@ def longitude_slicer(data, longitude_extent, longitude_coords): return data -from pathlib import Path - - def get_glorys_data( longitude_extent, latitude_extent, @@ -173,14 +349,14 @@ def get_glorys_data( path = Path(download_path) if modify_existing: - file = open(path / "get_glorysdata.sh", "r") + file = open(Path(path / "get_glorysdata.sh"), "r") lines = file.readlines() file.close() else: lines = ["#!/bin/bash\n"] - file = open(path / "get_glorysdata.sh", "w") + file = open(Path(path / "get_glorysdata.sh"), "w") lines.append( f""" @@ -308,10 +484,10 @@ def hyperbolictan_thickness_profile(nlayers, ratio, total_depth): return layer_thicknesses -def rectangular_hgrid(λ, φ): +def generate_rectangular_hgrid(lons, lats): """ Construct a horizontal grid with all the metadata required by MOM6, based on - arrays of longitudes (``λ``) and latitudes (``φ``) on the supergrid. + arrays of longitudes (``lons``) and latitudes (``lats``) on the supergrid. Here, 'supergrid' refers to both cell edges and centres, meaning that there are twice as many points along each axis than for any individual field. @@ -321,40 +497,46 @@ def rectangular_hgrid(λ, φ): It is also assumed here that the longitude array values are uniformly spaced. - Ensure both ``λ`` and ``φ`` are monotonically increasing. + Ensure both ``lons`` and ``lats`` are monotonically increasing. Args: - λ (numpy.array): All longitude points on the supergrid. Must be uniformly spaced. - φ (numpy.array): All latitude points on the supergrid. + lons (numpy.array): All longitude points on the supergrid. Must be uniformly spaced. + lats (numpy.array): All latitude points on the supergrid. Returns: xarray.Dataset: An FMS-compatible horizontal grid (``hgrid``) that includes all required attributes. """ - assert np.all(np.diff(λ) > 0), "longitudes array λ must be monotonically increasing" - assert np.all(np.diff(φ) > 0), "latitudes array φ must be monotonically increasing" + assert np.all( + np.diff(lons) > 0 + ), "longitudes array lons must be monotonically increasing" + assert np.all( + np.diff(lats) > 0 + ), "latitudes array lats must be monotonically increasing" R = 6371e3 # mean radius of the Earth; https://en.wikipedia.org/wiki/Earth_radius # compute longitude spacing and ensure that longitudes are uniformly spaced - dλ = λ[1] - λ[0] + dlons = lons[1] - lons[0] assert np.allclose( - np.diff(λ), dλ * np.ones(np.size(λ) - 1) + np.diff(lons), dlons * np.ones(np.size(lons) - 1) ), "provided array of longitudes must be uniformly spaced" - # dx = R * cos(np.deg2rad(φ)) * np.deg2rad(dλ) / 2 + # dx = R * cos(np.deg2rad(lats)) * np.deg2rad(dlons) / 2 # Note: division by 2 because we're on the supergrid dx = np.broadcast_to( - R * np.cos(np.deg2rad(φ)) * np.deg2rad(dλ) / 2, - (λ.shape[0] - 1, φ.shape[0]), + R * np.cos(np.deg2rad(lats)) * np.deg2rad(dlons) / 2, + (lons.shape[0] - 1, lats.shape[0]), ).T - # dy = R * np.deg2rad(dφ) / 2 + # dy = R * np.deg2rad(dlats) / 2 # Note: division by 2 because we're on the supergrid - dy = np.broadcast_to(R * np.deg2rad(np.diff(φ)) / 2, (λ.shape[0], φ.shape[0] - 1)).T + dy = np.broadcast_to( + R * np.deg2rad(np.diff(lats)) / 2, (lons.shape[0], lats.shape[0] - 1) + ).T - lon, lat = np.meshgrid(λ, φ) + lon, lat = np.meshgrid(lons, lats) area = quadrilateral_areas(lat, lon, R) @@ -445,6 +627,66 @@ class experiment: minimum_depth (Optional[int]): The minimum depth in meters of a grid cell allowed before it is masked out and treated as land. """ + @classmethod + def create_empty( + self, + longitude_extent=None, + latitude_extent=None, + date_range=None, + resolution=None, + number_vertical_layers=None, + layer_thickness_ratio=None, + depth=None, + mom_run_dir=None, + mom_input_dir=None, + toolpath_dir=None, + grid_type="even_spacing", + repeat_year_forcing=False, + minimum_depth=4, + tidal_constituents=["M2"], + name=None, + ): + """ + Substitute init method to creates an empty expirement object, with the opportunity to override whatever values wanted. + """ + expt = self( + longitude_extent=None, + latitude_extent=None, + date_range=None, + resolution=None, + number_vertical_layers=None, + layer_thickness_ratio=None, + depth=None, + minimum_depth=None, + mom_run_dir=None, + mom_input_dir=None, + toolpath_dir=None, + create_empty=True, + grid_type=None, + repeat_year_forcing=None, + tidal_constituents=None, + name=None, + ) + + expt.expt_name = name + expt.tidal_constituents = tidal_constituents + expt.repeat_year_forcing = repeat_year_forcing + expt.grid_type = grid_type + expt.toolpath_dir = toolpath_dir + expt.mom_run_dir = mom_run_dir + expt.mom_input_dir = mom_input_dir + expt.min_depth = minimum_depth + expt.depth = depth + expt.layer_thickness_ratio = layer_thickness_ratio + expt.number_vertical_layers = number_vertical_layers + expt.resolution = resolution + expt.date_range = date_range + expt.latitude_extent = latitude_extent + expt.longitude_extent = longitude_extent + expt.ocean_mask = None + expt.layout = None + return expt + def __init__( self, *, @@ -462,8 +704,21 @@ def __init__( repeat_year_forcing=False, read_existing_grids=False, minimum_depth=4, + tidal_constituents=["M2"], + create_empty=False, + name=None, ): + + # Creates empty experiment object for testing and experienced user manipulation. + # Kinda seems like a logical spinoff of this is to divorce the hgrid/vgrid creation from the experiment object initialization. + # Probably more of a CS workflow. That way read_existing_grids could be a function on its own, which ties in better with + # For now, check out the create_empty method for more explanation + if create_empty: + return + + # ## Set up the experiment with no config file ## in case list was given, convert to tuples + self.expt_name = name self.longitude_extent = tuple(longitude_extent) self.latitude_extent = tuple(latitude_extent) self.date_range = tuple(date_range) @@ -490,6 +745,8 @@ def __init__( self.min_depth = ( minimum_depth # Minimum depth. Shallower water will be masked out. ) + self.tidal_constituents = tidal_constituents + if read_existing_grids: try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") @@ -503,6 +760,11 @@ def __init__( else: self.hgrid = self._make_hgrid() self.vgrid = self._make_vgrid() + + self.segments = ( + {} + ) # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) + # create additional directories and links (self.mom_input_dir / "weights").mkdir(exist_ok=True) (self.mom_input_dir / "forcing").mkdir(exist_ok=True) @@ -514,6 +776,9 @@ def __init__( if not input_rundir.exists(): input_rundir.symlink_to(self.mom_run_dir.resolve()) + def __str__(self) -> str: + return json.dumps(self.write_config_file(export=False, quiet=True), indent=4) + def __getattr__(self, name): available_methods = [ method for method in dir(self) if not method.startswith("__") @@ -530,14 +795,14 @@ def _make_hgrid(self): and in latitude. The latitudinal resolution is scaled with the cosine of the central - latitude of the domain, i.e., ``Δφ = cos(φ_central) * Δλ``, where ``Δλ`` + latitude of the domain, i.e., ``Δlats = cos(lats_central) * Δlons``, where ``Δlons`` is the longitudinal spacing. This way, for a sufficiently small domain, the linear distances between grid points are nearly identical: - ``Δx = R * cos(φ) * Δλ`` and ``Δy = R * Δφ = R * cos(φ_central) * Δλ`` - (here ``R`` is Earth's radius and ``φ``, ``φ_central``, ``Δλ``, and ``Δφ`` + ``Δx = R * cos(lats) * Δlons`` and ``Δy = R * Δlats = R * cos(lats_central) * Δlons`` + (here ``R`` is Earth's radius and ``lats``, ``lats_central``, ``Δlons``, and ``Δlats`` are all expressed in radians). - That is, if the domain is small enough that so that ``cos(φ_North_Side)`` - is not much different from ``cos(φ_South_Side)``, then ``Δx`` and ``Δy`` + That is, if the domain is small enough that so that ``cos(lats_North_Side)`` + is not much different from ``cos(lats_South_Side)``, then ``Δx`` and ``Δy`` are similar. Note: @@ -563,7 +828,7 @@ def _make_hgrid(self): if nx % 2 != 1: nx += 1 - λ = np.linspace( + lons = np.linspace( self.longitude_extent[0], self.longitude_extent[1], nx ) # longitudes in degrees @@ -584,11 +849,11 @@ def _make_hgrid(self): if ny % 2 != 1: ny += 1 - φ = np.linspace( + lats = np.linspace( self.latitude_extent[0], self.latitude_extent[1], ny ) # latitudes in degrees - hgrid = rectangular_hgrid(λ, φ) + hgrid = generate_rectangular_hgrid(lons, lats) hgrid.to_netcdf(self.mom_input_dir / "hgrid.nc") return hgrid @@ -619,13 +884,187 @@ def _make_vgrid(self): return vcoord - def initial_condition( + @property + def ocean_state_boundaries(self): + """ + Read the ocean state files from disk, and print 'em + """ + ocean_state_path = self.mom_input_dir / "forcing" + try: + # Use glob to find all tides files + patterns = [ + "forcing_*", + "weights/bi*", + ] + all_files = [] + for pattern in patterns: + all_files.extend(glob.glob(Path(ocean_state_path / pattern))) + all_files.extend(glob.glob(Path(self.mom_input_dir / pattern))) + + if len(all_files) == 0: + return "No ocean state files set up yet (or files misplaced from {}). Call `setup_ocean_state_boundaries` method to set up ocean state.".format( + ocean_state_path + ) + + # Open the files as xarray datasets + # datasets = [xr.open_dataset(file) for file in all_files] + return all_files + except: + return "Error retrieving ocean state files" + + @property + def tides_boundaries(self): + """ + Read the tides from disk, and print 'em + """ + tides_path = self.mom_input_dir / "forcing" + try: + # Use glob to find all tides files + patterns = ["regrid*", "tu_*", "tz_*"] + all_files = [] + for pattern in patterns: + all_files.extend(glob.glob(Path(tides_path / pattern))) + all_files.extend(glob.glob(Path(self.mom_input_dir / pattern))) + + if len(all_files) == 0: + return "No tides files set up yet (or files misplaced from {}). Call `setup_tides_boundaries` method to set up tides.".format( + tides_path + ) + + # Open the files as xarray datasets + # datasets = [xr.open_dataset(file) for file in all_files] + return all_files + except: + return "Error retrieving tides files" + + @property + def era5(self): + """ + Read the era5's from disk, and print 'em + """ + era5_path = self.mom_input_dir / "forcing" + try: + # Use glob to find all *_ERA5.nc files + all_files = glob.glob(Path(era5_path / "*_ERA5.nc")) + if len(all_files) == 0: + return "No era5 files set up yet (or files misplaced from {}). Call `setup_era5` method to set up era5.".format( + era5_path + ) + + # Open the files as xarray datasets + # datasets = [xr.open_dataset(file) for file in all_files] + return all_files + except: + return "Error retrieving ERA5 files" + + @property + def initial_condition(self): + """ + Read the ic's from disk, and print 'em + """ + forcing_path = self.mom_input_dir / "forcing" + try: + all_files = glob.glob(Path(forcing_path / "init_*.nc")) + all_files = glob.glob(Path(self.mom_input_dir / "init_*.nc")) + if len(all_files) == 0: + return "No initial conditions files set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( + forcing_path + ) + + # Open the files as xarray datasets + # datasets = [xr.open_dataset(file) for file in all_files] + # return datasets + + return all_files + except: + return "No initial condition set up yet (or files misplaced from {}). Call `setup_initial_condition` method to set up initial conditions.".format( + self.mom_input_dir / "forcing" + ) + + @property + def bathymetry_property(self): + """ + Read the bathymetry from disk, and print 'em + """ + + try: + bathy = xr.open_dataset(self.mom_input_dir / "bathymetry.nc") + # return [bathy] + return str(self.mom_input_dir / "bathymetry.nc") + except: + return "No bathymetry set up yet (or files misplaced from {}). Call `setup_bathymetry` method to set up bathymetry.".format( + self.mom_input_dir + ) + + def write_config_file(self, path=None, export=True, quiet=False): + """ + Write a configuration file for the experiment. This is a simple json file + that contains the expirment object information to allow for reproducibility, to pick up where a user left off, and + to make information about the expirement readable. + """ + if not quiet: + print("Writing Config File.....") + ## check if files exist + vgrid_path = None + hgrid_path = None + if os.path.exists(self.mom_input_dir / "vcoord.nc"): + vgrid_path = self.mom_input_dir / "vcoord.nc" + if os.path.exists(self.mom_input_dir / "hgrid.nc"): + hgrid_path = self.mom_input_dir / "hgrid.nc" + + try: + date_range = [ + self.date_range[0].strftime("%Y-%m-%d"), + self.date_range[1].strftime("%Y-%m-%d"), + ] + except: + date_range = None + config_dict = { + "name": self.expt_name, + "date_range": date_range, + "latitude_extent": self.latitude_extent, + "longitude_extent": self.longitude_extent, + "run_dir": str(self.mom_run_dir), + "input_dir": str(self.mom_input_dir), + "toolpath_dir": str(self.toolpath_dir), + "resolution": self.resolution, + "number_vertical_layers": self.number_vertical_layers, + "layer_thickness_ratio": self.layer_thickness_ratio, + "depth": self.depth, + "grid_type": self.grid_type, + "repeat_year_forcing": self.repeat_year_forcing, + "ocean_mask": self.ocean_mask, + "layout": self.layout, + "min_depth": self.min_depth, + "vgrid": str(vgrid_path), + "hgrid": str(hgrid_path), + "bathymetry": self.bathymetry_property, + "ocean_state": self.ocean_state_boundaries, + "tides": self.tides_boundaries, + "initial_conditions": self.initial_condition, + "tidal_constituents": self.tidal_constituents, + } + if export: + if path is not None: + export_path = path + else: + export_path = self.mom_run_dir / "config.json" + with open(export_path, "w") as f: + json.dump( + config_dict, + f, + indent=4, + ) + if not quiet: + print("Done.") + return config_dict + + def setup_initial_condition( self, raw_ic_path, varnames, arakawa_grid="A", vcoord_type="height", - ): """ Reads the initial condition from files in ``ic_path``, interpolates to the @@ -1010,16 +1449,17 @@ def get_glorys_rectangular( ) print( - f"script `get_glorys_data.sh` has been greated at {raw_boundaries_path}.\n Run this script via bash to download the data from a terminal with internet access. \nYou will need to enter your Copernicus Marine username and password.\nIf you don't have an account, make one here:\nhttps://data.marine.copernicus.eu/register" + f"script `get_glorys_data.sh` has been created at {raw_boundaries_path}.\n Run this script via bash to download the data from a terminal with internet access. \nYou will need to enter your Copernicus Marine username and password.\nIf you don't have an account, make one here:\nhttps://data.marine.copernicus.eu/register" ) return - def rectangular_boundaries( + def setup_ocean_state_boundaries( self, raw_boundaries_path, varnames, boundaries=["south", "north", "west", "east"], arakawa_grid="A", + boundary_type="rectangular", ): """ This function is a wrapper for `simple_boundary`. Given a list of up to four cardinal directions, @@ -1034,6 +1474,7 @@ def rectangular_boundaries( Default is `["south", "north", "west", "east"]`. arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. Either ``'A'`` (default), ``'B'``, or ``'C'``. + boundary_type (Optional[str]): Type of box around region. Currently, only ``'rectangular'`` is supported. """ for i in boundaries: if i not in ["south", "north", "west", "east"]: @@ -1050,18 +1491,30 @@ def rectangular_boundaries( raise ValueError( "This method only supports up to four boundaries. To set up more complex boundary shapes you can manually call the 'simple_boundary' method for each boundary." ) + if boundary_type != "rectangular": + raise ValueError( + "Only rectangular boundaries are supported by this method. To set up more complex boundary shapes you can manually call the 'simple_boundary' method for each boundary." + ) # Now iterate through our four boundaries - for i, orientation in enumerate(boundaries, start=1): - self.simple_boundary( - Path(raw_boundaries_path) / (orientation + "_unprocessed.nc"), + for orientation in boundaries: + self.setup_ocean_state_boundary( + Path((raw_boundaries_path) / (orientation + "_unprocessed.nc")), varnames, orientation, # The cardinal direction of the boundary - i, # A number to identify the boundary; indexes from 1 + find_MOM6_rectangular_orientation( + orientation + ), # A number to identify the boundary; indexes from 1 arakawa_grid=arakawa_grid, ) - def simple_boundary( - self, path_to_bc, varnames, orientation, segment_number, arakawa_grid="A" + def setup_ocean_state_boundary( + self, + path_to_bc, + varnames, + orientation, + segment_number, + arakawa_grid="A", + boundary_type="simple", ): """ Here 'simple' refers to boundaries that are parallel to lines of constant longitude or latitude. @@ -1080,6 +1533,7 @@ def simple_boundary( the ``MOM_input``. arakawa_grid (Optional[str]): Arakawa grid staggering type of the boundary forcing. Either ``'A'`` (default), ``'B'``, or ``'C'``. + boundary_type (Optional[str]): Type of boundary. Currently, only ``'simple'`` is supported. Here 'simple' refers to boundaries that are parallel to lines of constant longitude or latitude. """ print("Processing {} boundary...".format(orientation), end="") @@ -1087,6 +1541,8 @@ def simple_boundary( raise FileNotFoundError( f"Boundary file not found at {path_to_bc}. Please ensure that the files are named in the format `east_unprocessed.nc`." ) + if boundary_type != "simple": + raise ValueError("Only simple boundaries are supported by this method.") seg = segment( hgrid=self.hgrid, infile=path_to_bc, # location of raw boundary @@ -1099,10 +1555,119 @@ def simple_boundary( repeat_year_forcing=self.repeat_year_forcing, ) - seg.rectangular_brushcut() + seg.regrid_rectangle_tracers() + + # Save Segment to Experiment + self.segments[orientation] = seg print("Done.") return + def setup_tides_boundaries( + self, + path_to_td, + tidal_filename, + tidal_constituents="read_from_expt_init", + boundary_type="rectangle", + ): + """ + This function: + We subset our tidal data and generate more boundary files! + + Args: + path_to_td (str): Path to boundary tidal file. + tidal_filename: Name of the tpxo product that's used in the tidal_filename. Should be h_{tidal_filename}, u_{tidal_filename} + tidal_constiuents: List of tidal constituents to include in the regridding. Default is [0] which is the M2 constituent. + boundary_type (Optional[str]): Type of boundary. Currently, only ``'rectangle'`` is supported. Here 'rectangle' refers to boundaries that are parallel to lines of constant longitude or latitude. + Returns: + *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 + """ + if boundary_type != "rectangle": + raise ValueError( + "Only rectangular boundaries are supported by this method." + ) + if tidal_constituents != "read_from_expt_init": + self.tidal_constituents = tidal_constituents + tpxo_h = ( + xr.open_dataset(Path(path_to_td / f"h_{tidal_filename}")) + .rename({"lon_z": "lon", "lat_z": "lat", "nc": "constituent"}) + .isel( + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) + ) + ) + + h = tpxo_h["ha"] * np.exp(-1j * np.radians(tpxo_h["hp"])) + tpxo_h["hRe"] = np.real(h) + tpxo_h["hIm"] = np.imag(h) + tpxo_u = ( + xr.open_dataset(Path(path_to_td / f"u_{tidal_filename}")) + .rename({"lon_u": "lon", "lat_u": "lat", "nc": "constituent"}) + .isel( + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) + ) + ) + tpxo_u["ua"] *= 0.01 # convert to m/s + u = tpxo_u["ua"] * np.exp(-1j * np.radians(tpxo_u["up"])) + tpxo_u["uRe"] = np.real(u) + tpxo_u["uIm"] = np.imag(u) + tpxo_v = ( + xr.open_dataset(Path(path_to_td / f"u_{tidal_filename}")) + .rename({"lon_v": "lon", "lat_v": "lat", "nc": "constituent"}) + .isel( + constituent=convert_to_tpxo_tidal_constituents(self.tidal_constituents) + ) + ) + tpxo_v["va"] *= 0.01 # convert to m/s + v = tpxo_v["va"] * np.exp(-1j * np.radians(tpxo_v["vp"])) + tpxo_v["vRe"] = np.real(v) + tpxo_v["vIm"] = np.imag(v) + times = xr.DataArray( + pd.date_range( + self.date_range[0], periods=1 + ), # Import pandas for this shouldn't be a big deal b/c it's already required in rm6 dependencies + dims=["time"], + ) + boundaries = ["south", "north", "west", "east"] + + # Initialize or find boundary segment + for b in boundaries: + print("Processing {} boundary...".format(b), end="") + + # If the GLORYS ocean_state has already created segments, we don't create them again. + if b not in self.segments: + seg = segment( + hgrid=self.hgrid, + infile=None, # location of raw boundary + outfolder=self.mom_input_dir, + varnames=None, + segment_name="segment_{:03d}".format( + find_MOM6_rectangular_orientation(b) + ), + orientation=b, # orienataion + startdate=self.date_range[0], + repeat_year_forcing=self.repeat_year_forcing, + ) + else: + seg = self.segments[b] + + # Output and regrid tides + seg.regrid_tides(tpxo_v, tpxo_u, tpxo_h, times) + print("Done") + def setup_bathymetry( self, *, @@ -1350,8 +1915,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): ## REMOVE INLAND LAKES - ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= self.min_depth, 0,1 - ) + ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= self.min_depth, 0, 1) land_mask = np.abs(ocean_mask - 1) changed = True ## keeps track of whether solution has converged or not @@ -1499,7 +2063,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): print("done.") self.bathymetry = bathymetry - def FRE_tools(self, layout=None): + def run_FRE_tools(self, layout=None): """A wrapper for FRE Tools ``check_mask``, ``make_solo_mosaic``, and ``make_quick_mosaic``. User provides processor ``layout`` tuple of processing units. """ @@ -1537,9 +2101,9 @@ def FRE_tools(self, layout=None): ) if layout != None: - self.cpu_layout(layout) + self.configure_cpu_layout(layout) - def cpu_layout(self, layout): + def configure_cpu_layout(self, layout): """ Wrapper for the ``check_mask`` function of GFDL's FRE Tools. User provides processor ``layout`` tuple of processing units. @@ -1562,6 +2126,8 @@ def setup_run_directory( surface_forcing=None, using_payu=False, overwrite=False, + with_tides_rectangular=False, + boundaries=["south", "north", "west", "east"], ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify @@ -1580,7 +2146,9 @@ def setup_run_directory( ## Get the path to the regional_mom package on this computer premade_rundir_path = Path( - importlib.resources.files("regional_mom6") / "demos/premade_run_directories" + importlib.resources.files("regional_mom6") + / "demos" + / "premade_run_directories" ) if not premade_rundir_path.exists(): print("Could not find premade run directories at ", premade_rundir_path) @@ -1590,23 +2158,27 @@ def setup_run_directory( premade_rundir_path = Path( importlib.resources.files("regional_mom6").parent - / "demos/premade_run_directories" + / "demos" + / "premade_run_directories" ) if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path} either.\n\n" + "There may be an issue with package installation. Check that the `premade_run_directory` folder is present in one of these two locations" ) + else: + print("Found run files. Continuing...") # Define the locations of the directories we'll copy files across from. Base contains most of the files, and overwrite replaces files in the base directory. - base_run_dir = premade_rundir_path / "common_files" + base_run_dir = Path(premade_rundir_path / "common_files") if not premade_rundir_path.exists(): raise ValueError( f"Cannot find the premade run directory files at {premade_rundir_path}.\n\n" + "These files missing might be indicating an error during the package installation!" ) if surface_forcing: - overwrite_run_dir = premade_rundir_path / f"{surface_forcing}_surface" + overwrite_run_dir = Path(premade_rundir_path / f"{surface_forcing}_surface") + if not overwrite_run_dir.exists(): available = [x for x in premade_rundir_path.iterdir() if x.is_dir()] raise ValueError( @@ -1616,6 +2188,20 @@ def setup_run_directory( ## In case there is additional forcing (e.g., tides) then we need to modify the run dir to include the additional forcing. overwrite_run_dir = False + # Check if we can implement tides + if with_tides_rectangular: + tidal_files_exist = any( + "tidal" in filename + for filename in ( + os.listdir(Path(self.mom_input_dir / "forcing")) + + os.listdir(Path(self.mom_input_dir)) + ) + ) + if not tidal_files_exist: + raise ( + "No files with 'tidal' in their names found in the forcing or input directory. If you meant to use tides, please run the setup_tides_rectangle_boundaries method first. That does output some tidal files." + ) + # 3 different cases to handle: # 1. User is creating a new run directory from scratch. Here we copy across all files and modify. # 2. User has already created a run directory, and wants to modify it. Here we only modify the MOM_layout file. @@ -1693,35 +2279,126 @@ def setup_run_directory( ## Modify the MOM_layout file to have correct horizontal dimensions and CPU layout # TODO Re-implement with package that works for this file type? or at least tidy up code - with open(self.mom_run_dir / "MOM_layout", "r") as file: - lines = file.readlines() - for jj in range(len(lines)): - if "MASKTABLE" in lines[jj]: - if mask_table != None: - lines[jj] = f'MASKTABLE = "{mask_table}"\n' - else: - lines[jj] = "# MASKTABLE = no mask table" - if "LAYOUT =" in lines[jj] and "IO" not in lines[jj] and layout != None: - lines[jj] = f"LAYOUT = {layout[1]},{layout[0]}\n" + MOM_layout_dict = self.read_MOM_file_as_dict("MOM_layout") + if "MASKTABLE" in MOM_layout_dict.keys(): + if mask_table != None: + MOM_layout_dict["MASKTABLE"]["value"] = mask_table + else: + MOM_layout_dict["MASKTABLE"]["value"] = "# MASKTABLE = no mask table" + if ( + "LAYOUT" in MOM_layout_dict.keys() + and "IO" not in MOM_layout_dict.keys() + and layout != None + ): + MOM_layout_dict["LAYOUT"]["value"] = str(layout[1]) + "," + str(layout[0]) + if "NIGLOBAL" in MOM_layout_dict.keys(): + MOM_layout_dict["NIGLOBAL"]["value"] = self.hgrid.nx.shape[0] // 2 + if "NJGLOBAL" in MOM_layout_dict.keys(): + MOM_layout_dict["NJGLOBAL"]["value"] = self.hgrid.ny.shape[0] // 2 + self.write_MOM_file(MOM_layout_dict) + + MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") + MOM_override_dict = self.read_MOM_file_as_dict("MOM_override") + # The number of boundaries is reflected in the number of segments setup in setup_ocean_state_boundary under expt.segments. + # The setup_tides_boundaries function currently only works with rectangular grids amd sets up 4 segments, but DOESN"T save them to expt.segments. + # Therefore, we can use expt.segments to determine how many segments we need for MOM_input. We can fill the empty segments with a empty string to make sure it is overriden correctly. + + # Others + MOM_override_dict["MINIMUM_DEPTH"]["value"] = float(self.min_depth) + MOM_override_dict["NK"]["value"] = len(self.vgrid.zl.values) + + # OBC Adjustments + + # Delete MOM_input OBC stuff that is indexed because we want them only in MOM_override. + print( + "Deleting indexed OBC keys from MOM_input_dict in case we have a different number of segments" + ) + keys_to_delete = [key for key in MOM_input_dict if "_SEGMENT_00" in key] + for key in keys_to_delete: + del MOM_input_dict[key] + + # Define number of OBC segments + MOM_override_dict["OBC_NUMBER_OF_SEGMENTS"]["value"] = len( + boundaries + ) # This means that each SEGMENT_00{num} has to be configured to point to the right file, which based on our other functions needs to be specified. + + # More OBC Consts + MOM_override_dict["OBC_FREESLIP_VORTICITY"]["value"] = "False" + MOM_override_dict["OBC_FREESLIP_STRAIN"]["value"] = "False" + MOM_override_dict["OBC_COMPUTED_VORTICITY"]["value"] = "True" + MOM_override_dict["OBC_COMPUTED_STRAIN"]["value"] = "True" + MOM_override_dict["OBC_ZERO_BIHARMONIC"]["value"] = "True" + MOM_override_dict["OBC_TRACER_RESERVOIR_LENGTH_SCALE_OUT"]["value"] = "3.0E+04" + MOM_override_dict["OBC_TRACER_RESERVOIR_LENGTH_SCALE_IN"]["value"] = "3000.0" + MOM_override_dict["BRUSHCUTTER_MODE"]["value"] = "True" + + # Define Specific Segments + for ind, seg in enumerate(boundaries): + ind_seg = ind + 1 + key_start = "OBC_SEGMENT_00" + str(ind_seg) + ## Position and Config + key_POSITION = key_start + if find_MOM6_rectangular_orientation(seg) == 1: + index_str = '"J=0,I=0:N' + elif find_MOM6_rectangular_orientation(seg) == 2: + index_str = '"J=N,I=N:0' + elif find_MOM6_rectangular_orientation(seg) == 3: + index_str = '"I=0,J=N:0' + elif find_MOM6_rectangular_orientation(seg) == 4: + index_str = '"I=N,J=0:N' + MOM_override_dict[key_POSITION]["value"] = ( + index_str + ',FLATHER,ORLANSKI,NUDGED,ORLANSKI_TAN,NUDGED_TAN"' + ) - if "NIGLOBAL" in lines[jj]: - lines[jj] = f"NIGLOBAL = {self.hgrid.nx.shape[0]//2}\n" + # Nudging Key + key_NUDGING = key_start + "_VELOCITY_NUDGING_TIMESCALES" + MOM_override_dict[key_NUDGING]["value"] = "0.3, 360.0" + + # Data Key + key_DATA = key_start + "_DATA" + file_num_obc = str( + find_MOM6_rectangular_orientation(seg) + ) # 1,2,3,4 for rectangular boundaries, BUT if we have less than 4 segments we use the index to specific the number, but keep filenames as if we had four boundaries + MOM_override_dict[key_DATA][ + "value" + ] = f'"U=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(u),V=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(v),SSH=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(eta),TEMP=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(temp),SALT=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(salt)' + if with_tides_rectangular: + MOM_override_dict[key_DATA]["value"] = ( + MOM_override_dict[key_DATA]["value"] + + f',Uamp=file:forcing/tu_segment_00{file_num_obc}.nc(uamp),Uphase=file:forcing/tu_segment_00{file_num_obc}.nc(uphase),Vamp=file:forcing/tu_segment_00{file_num_obc}.nc(vamp),Vphase=file:forcing/tu_segment_00{file_num_obc}.nc(vphase),SSHamp=file:forcing/tz_segment_00{file_num_obc}.nc(zamp),SSHphase=file:forcing/tz_segment_00{file_num_obc}.nc(zphase)"' + ) + else: + MOM_override_dict[key_DATA]["value"] = ( + MOM_override_dict[key_DATA]["value"] + '"' + ) - if "NJGLOBAL" in lines[jj]: - lines[jj] = f"NJGLOBAL = {self.hgrid.ny.shape[0]//2}\n" - with open(self.mom_run_dir / "MOM_layout", "w") as f: - f.writelines(lines) + # Tides OBC adjustments + if with_tides_rectangular: - # Overwrite values pertaining to vertical structure in the MOM_input file - with open(self.mom_run_dir / "MOM_input", "r") as file: - lines = file.readlines() - for jj in range(len(lines)): - if "MINIMUM_DEPTH = " in lines[jj]: - lines[jj] = f'MINIMUM_DEPTH = {float(self.min_depth)}\n' - if "NK =" in lines[jj]: - lines[jj] = f"NK = {len(self.vgrid.zl.values)}\n" - with open(self.mom_run_dir / "MOM_input", "w") as f: - f.writelines(lines) + # Include internal tide forcing + MOM_override_dict["TIDES"]["value"] = "True" + + # OBC tides + MOM_override_dict["OBC_TIDE_ADD_EQ_PHASE"]["value"] = "True" + MOM_override_dict["OBC_TIDE_N_CONSTITUENTS"]["value"] = len( + self.tidal_constituents + ) + MOM_override_dict["OBC_TIDE_CONSTITUENTS"]["value"] = ( + '"' + ", ".join(self.tidal_constituents) + '"' + ) + MOM_override_dict["OBC_TIDE_REF_DATE"]["value"] = ( + str(self.date_range[0].year) + + ", " + + str(self.date_range[0].month) + + ", " + + str(self.date_range[0].day) + ) + + for key in MOM_override_dict.keys(): + if type(MOM_override_dict[key]) == dict: + MOM_override_dict[key]["override"] = True + self.write_MOM_file(MOM_input_dict) + self.write_MOM_file(MOM_override_dict) ## If using payu to run the model, create a payu configuration file if not using_payu and os.path.exists(f"{self.mom_run_dir}/config.yaml"): @@ -1760,6 +2437,186 @@ def setup_run_directory( 0, ] nml.write(self.mom_run_dir / "input.nml", force=True) + return + + def change_MOM_parameter( + self, param_name, param_value=None, comment=None, delete=False + ): + """ + *Requires already copied MOM parameter files in the run directory* + Change a parameter in the MOM_input or MOM_override file. Returns original value if there was one. + If delete is specified, ONLY MOM_override version will be deleted. Deleting from MOM_input is not safe. + If the parameter does not exist, it will be added to the file. if delete is set to True, the parameter will be removed. + Args: + param_name (str): + Parameter name we are working with + param_value (Optional[str]): + New Assigned Value + comment (Optional[str]): + Any comment to add + delete (Optional[bool]): + Whether to delete the specified param_name + + """ + if not delete and param_value is None: + raise ValueError( + "If not deleting a parameter, you must specify a new value for it." + ) + + MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") + MOM_override_dict = self.read_MOM_file_as_dict("MOM_override") + original_val = "No original val" + if not delete: + # We don't want to keep any parameters in MOM_input that we change. We want to clearly list them in MOM_override. + if param_name in MOM_input_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print("Removing original value {} from MOM_input".format(original_val)) + del MOM_input_dict[param_name] + if param_name in MOM_override_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print( + "This parameter {} is begin replaced from {} to {} in MOM_override".format( + param_name, original_val, param_value + ) + ) + + MOM_override_dict[param_name]["value"] = param_value + MOM_override_dict[param_name]["comment"] = comment + else: + if param_name in MOM_override_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print("Deleting parameter {} from MOM_override".format(param_name)) + del MOM_override_dict[param_name] + else: + print( + "Key to be deleted {} was not in MOM_override to begin with.".format( + param_name + ) + ) + self.write_MOM_file(MOM_input_dict) + self.write_MOM_file(MOM_override_dict) + return original_val + + def read_MOM_file_as_dict(self, filename): + """ + Read the MOM_input file and return a dictionary of the variables and their values. + """ + + # Default information for each parameter + default_layout = {"value": None, "override": False, "comment": None} + + if not os.path.exists(Path(self.mom_run_dir / filename)): + raise ValueError( + f"File {filename} does not exist in the run directory {self.mom_run_dir}" + ) + with open(Path(self.mom_run_dir / filename), "r") as file: + lines = file.readlines() + + # Set the default initialization for a new key + MOM_file_dict = defaultdict(lambda: default_layout.copy()) + MOM_file_dict["filename"] = filename + dlc = default_layout.copy() + for jj in range(len(lines)): + if "=" in lines[jj] and not "===" in lines[jj]: + split = lines[jj].split("=", 1) + var = split[0] + value = split[1] + if "#override" in var: + var = var.split("#override")[1].strip() + dlc["override"] = True + else: + dlc["override"] = False + if "!" in value: + dlc["comment"] = value.split("!")[1] + value = value.split("!")[0].strip() # Remove Comments + dlc["value"] = str(value) + else: + dlc["value"] = str(value.strip()) + dlc["comment"] = None + + MOM_file_dict[var.strip()] = dlc.copy() + + # Save a copy of the original dictionary + MOM_file_dict["original"] = MOM_file_dict.copy() + return MOM_file_dict + + def write_MOM_file(self, MOM_file_dict): + """ + Write the MOM_input file from a dictionary of variables and their values. Does not support removing fields. + """ + # Replace specific variable values + original_MOM_file_dict = MOM_file_dict.pop("original") + with open(Path(self.mom_run_dir / MOM_file_dict["filename"]), "r") as file: + lines = file.readlines() + for jj in range(len(lines)): + if "=" in lines[jj] and not "===" in lines[jj]: + var = lines[jj].split("=", 1)[0].strip() + if var in MOM_file_dict.keys() and ( + str(MOM_file_dict[var]["value"]) + ) != str(original_MOM_file_dict[var]["value"]): + lines[jj] = lines[jj].replace( + str(original_MOM_file_dict[var]["value"]), + str(MOM_file_dict[var]["value"]), + ) + lines[jj] = lines[jj].replace( + original_MOM_file_dict[var]["comment"], + str(MOM_file_dict[var]["comment"]), + ) + print( + "Changed", + var, + "from", + original_MOM_file_dict[var], + "to", + MOM_file_dict[var], + "in {}!".format(MOM_file_dict["filename"]), + ) + + # Add new fields + lines.append("! === Added with RM6 ===\n") + for key in MOM_file_dict.keys(): + if key not in original_MOM_file_dict.keys(): + if MOM_file_dict[key]["override"]: + lines.append( + f"#override {key} = {MOM_file_dict[key]['value']} !{MOM_file_dict[key]['comment']}\n" + ) + else: + lines.append( + f"{key} = {MOM_file_dict[key]['value']} !{MOM_file_dict[key]['comment']}\n" + ) + print( + "Added", + key, + "to", + MOM_file_dict["filename"], + "with value", + MOM_file_dict[key], + ) + + # Check any fields removed + for key in original_MOM_file_dict.keys(): + if key not in MOM_file_dict.keys(): + search_words = [ + key, + original_MOM_file_dict[key]["value"], + original_MOM_file_dict[key]["comment"], + ] + lines = [ + line + for line in lines + if not all(word in line for word in search_words) + ] + print( + "Removed", + key, + "in", + MOM_file_dict["filename"], + "with value", + original_MOM_file_dict[key], + ) + + with open(Path(self.mom_run_dir / MOM_file_dict["filename"]), "w") as f: + f.writelines(lines) def setup_era5(self, era5_path): """ @@ -1784,7 +2641,7 @@ def setup_era5(self, era5_path): i for i in range(self.date_range[0].year, self.date_range[1].year + 1) ] # construct a list of all paths for all years to use for open_mfdataset - paths_per_year = [Path(f"{era5_path}/{fname}/{year}/") for year in years] + paths_per_year = [Path(era5_path / fname / year) for year in years] all_files = [] for path in paths_per_year: # Use glob to find all files that match the pattern @@ -1867,8 +2724,8 @@ def setup_era5(self, era5_path): class segment: """ - Class to turn raw boundary segment data into MOM6 boundary - segments. + Class to turn raw boundary and tidal segment data into MOM6 boundary + and tidal segments. Boundary segments should only contain the necessary data for that segment. No horizontal chunking is done here, so big fat segments @@ -1898,11 +2755,6 @@ class segment: Either ``'A'`` (default), ``'B'``, or ``'C'``. time_units (str): The units used by the raw forcing files, e.g., ``hours``, ``days`` (default). - tidal_constituents (Optional[int]): An integer determining the number of tidal - constituents to be included from the list: *M*:sub:`2`, *S*:sub:`2`, *N*:sub:`2`, - *K*:sub:`2`, *K*:sub:`1`, *O*:sub:`2`, *P*:sub:`1`, *Q*:sub:`1`, *Mm*, - *Mf*, and *M*:sub:`4`. For example, specifying ``1`` only includes *M*:sub:`2`; - specifying ``2`` includes *M*:sub:`2` and *S*:sub:`2`, etc. Default: ``None``. repeat_year_forcing (Optional[bool]): When ``True`` the experiment runs with repeat-year forcing. When ``False`` (default) then inter-annual forcing is used. """ @@ -1919,11 +2771,10 @@ def __init__( startdate, arakawa_grid="A", time_units="days", - tidal_constituents=None, repeat_year_forcing=False, ): ## Store coordinate names - if arakawa_grid == "A": + if arakawa_grid == "A" and infile is not None: self.x = varnames["x"] self.y = varnames["y"] @@ -1934,15 +2785,17 @@ def __init__( self.yh = varnames["yh"] ## Store velocity names - self.u = varnames["u"] - self.v = varnames["v"] - self.z = varnames["zl"] - self.eta = varnames["eta"] - self.time = varnames["time"] + if infile is not None: + self.u = varnames["u"] + self.v = varnames["v"] + self.z = varnames["zl"] + self.eta = varnames["eta"] + self.time = varnames["time"] self.startdate = startdate ## Store tracer names - self.tracers = varnames["tracers"] + if infile is not None: + self.tracers = varnames["tracers"] self.time_units = time_units ## Store other data @@ -1961,10 +2814,81 @@ def __init__( self.outfolder = outfolder self.hgrid = hgrid self.segment_name = segment_name - self.tidal_constituents = tidal_constituents self.repeat_year_forcing = repeat_year_forcing - def rectangular_brushcut(self): + @property + def coords(self): + """ + + + This function: + Allows us to call the self.coords for use in the xesmf.Regridder in the regrid_tides function. self.coords gives us the subset of the hgrid based on the orientation. + + Args: + None + Returns: + xr.Dataset: The correct coordinate space for the orientation + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 + + """ + # Rename nxp and nyp to locations + if self.orientation == "south": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nyp=0), + "lat": self.hgrid["y"].isel(nyp=0), + "angle": self.hgrid["angle_dx"].isel(nyp=0), + } + ) + rcoord = rcoord.rename_dims({"nxp": "locations"}) + elif self.orientation == "north": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nyp=-1), + "lat": self.hgrid["y"].isel(nyp=-1), + "angle": self.hgrid["angle_dx"].isel(nyp=-1), + } + ) + rcoord = rcoord.rename_dims({"nxp": "locations"}) + elif self.orientation == "west": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nxp=0), + "lat": self.hgrid["y"].isel(nxp=0), + "angle": self.hgrid["angle_dx"].isel(nxp=0), + } + ) + rcoord = rcoord.rename_dims({"nyp": "locations"}) + elif self.orientation == "east": + rcoord = xr.Dataset( + { + "lon": self.hgrid["x"].isel(nxp=-1), + "lat": self.hgrid["y"].isel(nxp=-1), + "angle": self.hgrid["angle_dx"].isel(nxp=-1), + } + ) + rcoord = rcoord.rename_dims({"nyp": "locations"}) + + # Make lat and lon coordinates + rcoord = rcoord.assign_coords(lat=rcoord["lat"], lon=rcoord["lon"]) + + return rcoord + + def regrid_rectangle_tracers(self): """ Cut out and interpolate tracers. ``rectangular_brushcut`` assumes that the boundary is a simple Northern, Southern, Eastern, or Western boundary. @@ -2280,3 +3204,227 @@ def rectangular_brushcut(self): ) return segment_out, encoding_dict + + def regrid_tides( + self, tpxo_v, tpxo_u, tpxo_h, times, method="nearest_s2d", periodic=False + ): + """ + This function: + Regrids and interpolates the tidal data for MOM6, originally inspired by GFDL NWA25 repo code & edited by Ashley. + - Read in raw tidal data (all constituents) + - Perform minor transformations/conversions + - Regridded the tidal elevation, and tidal velocity + - Encoding the output + + Args: + infile_td (str): Raw Tidal File/Dir + tpxo_v, tpxo_u, tpxo_h (xarray.Dataset): Specific adjusted for MOM6 tpxo datasets (Adjusted with setup_tides) + times (pd.DateRange): The start date of our model period + Returns: + *.nc files: Regridded tidal velocity and elevation files in 'inputdir/forcing' + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 + """ + + ########## Tidal Elevation: Horizontally interpolate elevation components ############ + regrid = xe.Regridder( + tpxo_h[["lon", "lat", "hRe"]], + self.coords, + method="nearest_s2d", + locstream_out=True, + periodic=False, + filename=Path( + self.outfolder / "forcing" / f"regrid_{self.segment_name}_tidal_elev.nc" + ), + reuse_weights=False, + ) + redest = regrid(tpxo_h[["lon", "lat", "hRe"]]) + imdest = regrid(tpxo_h[["lon", "lat", "hIm"]]) + + # Fill missing data. + # Need to do this first because complex would get converted to real + redest = redest.ffill(dim="locations", limit=None)["hRe"] + imdest = imdest.ffill(dim="locations", limit=None)["hIm"] + + # Convert complex + cplex = redest + 1j * imdest + + # Convert to real amplitude and phase. + ds_ap = xr.Dataset({f"zamp_{self.segment_name}": np.abs(cplex)}) + # np.angle doesn't return dataarray + ds_ap[f"zphase_{self.segment_name}"] = ( + ("constituent", "locations"), + -1 * np.angle(cplex), + ) # radians + + # Add time coordinate and transpose so that time is first, + # so that it can be the unlimited dimension + ds_ap, _ = xr.broadcast(ds_ap, times) + ds_ap = ds_ap.transpose("time", "constituent", "locations") + + self.encode_tidal_files_and_output(ds_ap, "tz") + + ########### Regrid Tidal Velocity ###################### + regrid_u = xe.Regridder( + tpxo_u[["lon", "lat", "uRe"]], + self.coords, + method=method, + locstream_out=True, + periodic=periodic, + reuse_weights=False, + ) + + regrid_v = xe.Regridder( + tpxo_v[["lon", "lat", "vRe"]], + self.coords, + method=method, + locstream_out=True, + periodic=periodic, + reuse_weights=False, + ) + + # Interpolate each real and imaginary parts to segment. + uredest = regrid_u(tpxo_u[["lon", "lat", "uRe"]])["uRe"] + uimdest = regrid_u(tpxo_u[["lon", "lat", "uIm"]])["uIm"] + vredest = regrid_v(tpxo_v[["lon", "lat", "vRe"]])["vRe"] + vimdest = regrid_v(tpxo_v[["lon", "lat", "vIm"]])["vIm"] + + # Fill missing data. + # Need to do this first because complex would get converted to real + uredest = uredest.ffill(dim="locations", limit=None) + uimdest = uimdest.ffill(dim="locations", limit=None) + vredest = vredest.ffill(dim="locations", limit=None) + vimdest = vimdest.ffill(dim="locations", limit=None) + + # Convert to complex, remaining separate for u and v. + ucplex = uredest + 1j * uimdest + vcplex = vredest + 1j * vimdest + + # Convert complex u and v to ellipse, + # rotate ellipse from earth-relative to model-relative, + # and convert ellipse back to amplitude and phase. + SEMA, ECC, INC, PHA = ap2ep(ucplex, vcplex) + + # Rotate to the model grid by adjusting the inclination. + # Requries that angle is in radians. + + ua, va, up, vp = ep2ap(SEMA, ECC, INC, PHA) + + ds_ap = xr.Dataset( + {f"uamp_{self.segment_name}": ua, f"vamp_{self.segment_name}": va} + ) + # up, vp aren't dataarrays + ds_ap[f"uphase_{self.segment_name}"] = ( + ("constituent", "locations"), + up, + ) # radians + ds_ap[f"vphase_{self.segment_name}"] = ( + ("constituent", "locations"), + vp, + ) # radians + + ds_ap, _ = xr.broadcast(ds_ap, times) + + # Need to transpose so that time is first, + # so that it can be the unlimited dimension + ds_ap = ds_ap.transpose("time", "constituent", "locations") + + # Some things may have become missing during the transformation + ds_ap = ds_ap.ffill(dim="locations", limit=None) + + self.encode_tidal_files_and_output(ds_ap, "tu") + + return + + def encode_tidal_files_and_output(self, ds, filename): + """ + This function: + - Expands the dimensions (with the segment name) + - Renames some dimensions to be more specific to the segment + - Provides an output file encoding + - Exports the files. + + Args: + self.outfolder (str/path): The output folder to save the tidal files into + dataset (xarray.Dataset): The processed tidal dataset + filename (str): The output file name + Returns: + *.nc files: Regridded [FILENAME] files in 'self.outfolder/forcing/[filename]_[segmentname].nc' + + General Description: + This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: + - Converted code for RM6 segment class + - Implemented Horizontal Subsetting + - Combined all functions of NWA25 into a four function process (in the style of rm6) (expt.setup_tides_rectangular_boundaries, segment.coords, segment.regrid_tides, segment.encode_tidal_files_and_output) + + + Original Code was sourced from: + Author(s): GFDL, James Simkins, Rob Cermak, etc.. + Year: 2022 + Title: "NWA25: Northwest Atlantic 1/25th Degree MOM6 Simulation" + Version: N/A + Type: Python Functions, Source Code + Web Address: https://github.com/jsimkins2/nwa25 + + + """ + + ## Expand Tidal Dimensions ## + if "z" in ds.coords or "constituent" in ds.dims: + offset = 0 + else: + offset = 1 + if self.orientation in ["south", "north"]: + ds = ds.expand_dims(f"ny_{self.segment_name}", 2 - offset) + elif self.orientation in ["west", "east"]: + ds = ds.expand_dims(f"nx_{self.segment_name}", 3 - offset) + + ## Rename Tidal Dimensions ## + ds = ds.rename( + {"lon": f"lon_{self.segment_name}", "lat": f"lat_{self.segment_name}"} + ) + if "z" in ds.coords: + ds = ds.rename({"z": f"nz_{self.segment_name}"}) + if self.orientation in ["south", "north"]: + ds = ds.rename({"locations": f"nx_{self.segment_name}"}) + elif self.orientation in ["west", "east"]: + ds = ds.rename({"locations": f"ny_{self.segment_name}"}) + + ## Perform Encoding ## + for v in ds: + ds[v].encoding["_FillValue"] = 1.0e20 + fname = f"{filename}_{self.segment_name}.nc" + # Set format and attributes for coordinates, including time if it does not already have calendar attribute + # (may change this to detect whether time is a time type or a float). + # Need to include the fillvalue or it will be back to nan + encoding = { + "time": dict(_FillValue=1.0e20), + f"lon_{self.segment_name}": dict(dtype="float64", _FillValue=1.0e20), + f"lat_{self.segment_name}": dict(dtype="float64", _FillValue=1.0e20), + } + if "calendar" not in ds["time"].attrs and "modulo" not in ds["time"].attrs: + encoding.update( + {"time": dict(dtype="float64", calendar="gregorian", _FillValue=1.0e20)} + ) + + ## Export Files ## + ds.to_netcdf( + Path(self.outfolder / "forcing" / fname), + engine="netcdf4", + encoding=encoding, + unlimited_dims="time", + ) + return diff --git a/regional_mom6/utils.py b/regional_mom6/utils.py index fb0ce865..447a2e4f 100644 --- a/regional_mom6/utils.py +++ b/regional_mom6/utils.py @@ -177,3 +177,120 @@ def quadrilateral_areas(lat, lon, R=1): return quadrilateral_area( coords[:-1, :-1, :], coords[:-1, 1:, :], coords[1:, 1:, :], coords[1:, :-1, :] ) + + +def ap2ep(uc, vc): + """Convert complex tidal u and v to tidal ellipse. + Adapted from ap2ep.m for matlab + Original copyright notice: + %Authorship Copyright: + % + % The author retains the copyright of this program, while you are welcome + % to use and distribute it as long as you credit the author properly and respect + % the program name itself. Particularly, you are expected to retain the original + % author's name in this original version or any of its modified version that + % you might make. You are also expected not to essentially change the name of + % the programs except for adding possible extension for your own version you + % might create, e.g. ap2ep_xx is acceptable. Any suggestions are welcome and + % enjoy my program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + % Release Date: Nov. 2000, Revised on May. 2002 to adopt Foreman's northern semi + % major axis convention. + + Args: + uc: complex tidal u velocity + vc: complex tidal v velocity + + Returns: + (semi-major axis, eccentricity, inclination [radians], phase [radians]) + """ + wp = (uc + 1j * vc) / 2.0 + wm = np.conj(uc - 1j * vc) / 2.0 + + Wp = np.abs(wp) + Wm = np.abs(wm) + THETAp = np.angle(wp) + THETAm = np.angle(wm) + + SEMA = Wp + Wm + SEMI = Wp - Wm + ECC = SEMI / SEMA + PHA = (THETAm - THETAp) / 2.0 + INC = (THETAm + THETAp) / 2.0 + + return SEMA, ECC, INC, PHA + + +def ep2ap(SEMA, ECC, INC, PHA): + """Convert tidal ellipse to real u and v amplitude and phase. + Adapted from ep2ap.m for matlab. + Original copyright notice: + %Authorship Copyright: + % + % The author of this program retains the copyright of this program, while + % you are welcome to use and distribute this program as long as you credit + % the author properly and respect the program name itself. Particularly, + % you are expected to retain the original author's name in this original + % version of the program or any of its modified version that you might make. + % You are also expected not to essentially change the name of the programs + % except for adding possible extension for your own version you might create, + % e.g. app2ep_xx is acceptable. Any suggestions are welcome and enjoy my + % program(s)! + % + % + %Author Info: + %_______________________________________________________________________ + % Zhigang Xu, Ph.D. + % (pronounced as Tsi Gahng Hsu) + % Research Scientist + % Coastal Circulation + % Bedford Institute of Oceanography + % 1 Challenge Dr. + % P.O. Box 1006 Phone (902) 426-2307 (o) + % Dartmouth, Nova Scotia Fax (902) 426-7827 + % CANADA B2Y 4A2 email xuz@dfo-mpo.gc.ca + %_______________________________________________________________________ + % + %Release Date: Nov. 2000 + + Args: + SEMA: semi-major axis + ECC: eccentricity + INC: inclination [radians] + PHA: phase [radians] + + Returns: + (u amplitude, u phase [radians], v amplitude, v phase [radians]) + + """ + Wp = (1 + ECC) / 2.0 * SEMA + Wm = (1 - ECC) / 2.0 * SEMA + THETAp = INC - PHA + THETAm = INC + PHA + + wp = Wp * np.exp(1j * THETAp) + wm = Wm * np.exp(1j * THETAm) + + cu = wp + np.conj(wm) + cv = -1j * (wp - np.conj(wm)) + + ua = np.abs(cu) + va = np.abs(cv) + up = -np.angle(cu) + vp = -np.angle(cv) + + return ua, va, up, vp diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index a601102f..d03c3bf5 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -311,7 +311,7 @@ def test_ocean_forcing( "tracers": {"temp": "temp", "salt": "salt"}, } - expt.initial_condition( + expt.setup_initial_condition( tmp_path / "ic_unprocessed", varnames, arakawa_grid="A", @@ -470,4 +470,4 @@ def test_rectangular_boundaries( "tracers": {"temp": "temp", "salt": "salt"}, } - expt.rectangular_boundaries(tmp_path, varnames, ["east"]) + expt.setup_ocean_state_boundaries(tmp_path, varnames, ["east"]) diff --git a/tests/test_grid_generation.py b/tests/test_grid_generation.py index d5158420..d9eea88e 100644 --- a/tests/test_grid_generation.py +++ b/tests/test_grid_generation.py @@ -2,7 +2,7 @@ import pytest from regional_mom6 import hyperbolictan_thickness_profile -from regional_mom6 import rectangular_hgrid +from regional_mom6 import generate_rectangular_hgrid from regional_mom6 import longitude_slicer from regional_mom6.utils import angle_between @@ -129,7 +129,7 @@ def test_quadrilateral_areas(lat, lon, true_area): ], ) def test_rectangular_hgrid(lat, lon): - assert isinstance(rectangular_hgrid(lat, lon), xr.Dataset) + assert isinstance(generate_rectangular_hgrid(lat, lon), xr.Dataset) def test_longitude_slicer(): diff --git a/tests/test_manish_branch.py b/tests/test_manish_branch.py new file mode 100644 index 00000000..df735a45 --- /dev/null +++ b/tests/test_manish_branch.py @@ -0,0 +1,299 @@ +""" +Test suite for everything involed in pr #12 +""" + +import regional_mom6 as rmom6 +import os +import pytest +import logging +from pathlib import Path +import xarray as xr +import numpy as np +from tests.test_expt_class import generate_silly_coords, number_of_gridpoints +import shutil +import importlib + +IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" + + +@pytest.fixture(scope="module") +def dummy_tidal_data(): + nx = 2160 + ny = 1081 + nc = 15 + nct = 4 + + # Define tidal constituents + con_list = [ + "m2 ", + "s2 ", + "n2 ", + "k2 ", + "k1 ", + "o1 ", + "p1 ", + "q1 ", + "mm ", + "mf ", + "m4 ", + "mn4 ", + "ms4 ", + "2n2 ", + "s1 ", + ] + con_data = np.array([list(con) for con in con_list], dtype="S1") + + # Generate random data for the variables + lon_z_data = np.tile(np.linspace(-180, 180, nx), (ny, 1)).T + lat_z_data = np.tile(np.linspace(-90, 90, ny), (nx, 1)) + ha_data = np.random.rand(nc, nx, ny) + hp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + hRe_data = np.random.rand(nc, nx, ny) + hIm_data = np.random.rand(nc, nx, ny) + + # Create the xarray dataset + ds_h = xr.Dataset( + { + "con": (["nc", "nct"], con_data), + "lon_z": (["nx", "ny"], lon_z_data), + "lat_z": (["nx", "ny"], lat_z_data), + "ha": (["nc", "nx", "ny"], ha_data), + "hp": (["nc", "nx", "ny"], hp_data), + "hRe": (["nc", "nx", "ny"], hRe_data), + "hIm": (["nc", "nx", "ny"], hIm_data), + }, + coords={ + "nc": np.arange(nc), + "nct": np.arange(nct), + "nx": np.arange(nx), + "ny": np.arange(ny), + }, + attrs={ + "type": "Fake OTIS tidal elevation file", + "title": "Fake TPXO9.v1 2018 tidal elevation file", + }, + ) + + # Generate random data for the variables for u_tpxo9.v1 + lon_u_data = ( + np.random.rand(nx, ny) * 360 - 180 + ) # Random longitudes between -180 and 180 + lat_u_data = ( + np.random.rand(nx, ny) * 180 - 90 + ) # Random latitudes between -90 and 90 + lon_v_data = ( + np.random.rand(nx, ny) * 360 - 180 + ) # Random longitudes between -180 and 180 + lat_v_data = ( + np.random.rand(nx, ny) * 180 - 90 + ) # Random latitudes between -90 and 90 + Ua_data = np.random.rand(nc, nx, ny) + ua_data = np.random.rand(nc, nx, ny) + up_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + Va_data = np.random.rand(nc, nx, ny) + va_data = np.random.rand(nc, nx, ny) + vp_data = np.random.rand(nc, nx, ny) * 360 # Random phases between 0 and 360 + URe_data = np.random.rand(nc, nx, ny) + UIm_data = np.random.rand(nc, nx, ny) + VRe_data = np.random.rand(nc, nx, ny) + VIm_data = np.random.rand(nc, nx, ny) + + # Create the xarray dataset for u_tpxo9.v1 + ds_u = xr.Dataset( + { + "con": (["nc", "nct"], con_data), + "lon_u": (["nx", "ny"], lon_u_data), + "lat_u": (["nx", "ny"], lat_u_data), + "lon_v": (["nx", "ny"], lon_v_data), + "lat_v": (["nx", "ny"], lat_v_data), + "Ua": (["nc", "nx", "ny"], Ua_data), + "ua": (["nc", "nx", "ny"], ua_data), + "up": (["nc", "nx", "ny"], up_data), + "Va": (["nc", "nx", "ny"], Va_data), + "va": (["nc", "nx", "ny"], va_data), + "vp": (["nc", "nx", "ny"], vp_data), + "URe": (["nc", "nx", "ny"], URe_data), + "UIm": (["nc", "nx", "ny"], UIm_data), + "VRe": (["nc", "nx", "ny"], VRe_data), + "VIm": (["nc", "nx", "ny"], VIm_data), + }, + coords={ + "nc": np.arange(nc), + "nct": np.arange(nct), + "nx": np.arange(nx), + "ny": np.arange(ny), + }, + attrs={ + "type": "Fake OTIS tidal transport file", + "title": "Fake TPXO9.v1 2018 WE/SN transports/currents file", + }, + ) + + return ds_h, ds_u + + +@pytest.fixture(scope="module") +def dummy_bathymetry_data(): + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + + bathymetry = np.random.random((100, 100)) * (-100) + bathymetry = xr.DataArray( + bathymetry, + dims=["silly_lat", "silly_lon"], + coords={ + "silly_lat": np.linspace( + latitude_extent[0] - 5, latitude_extent[1] + 5, 100 + ), + "silly_lon": np.linspace( + longitude_extent[0] - 5, longitude_extent[1] + 5, 100 + ), + }, + ) + bathymetry.name = "silly_depth" + return bathymetry + + +class TestAll: + @classmethod + def setup_class(self): # tmp_path is a pytest fixture + expt_name = "testing" + ## User-1st, test if we can even read the angled nc files. + self.dump_files_dir = Path("testing_outputs") + os.makedirs(self.dump_files_dir, exist_ok=True) + self.expt = rmom6.experiment.create_empty( + name=expt_name, + mom_input_dir=self.dump_files_dir, + mom_run_dir=self.dump_files_dir, + ) + + @classmethod + def teardown_class(cls): + shutil.rmtree(cls.dump_files_dir) + + @pytest.fixture(scope="module") + def full_legit_expt_setup(self, dummy_bathymetry_data): + + expt_name = "testing" + + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + + date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] + + ## Place where all your input files go + input_dir = Path( + os.path.join( + expt_name, + "inputs", + ) + ) + + ## Directory where you'll run the experiment from + run_dir = Path( + os.path.join( + expt_name, + "run_files", + ) + ) + data_path = Path("data") + for path in (run_dir, input_dir, data_path): + os.makedirs(str(path), exist_ok=True) + bathy_path = data_path / "bathymetry.nc" + bathymetry = dummy_bathymetry_data + bathymetry.to_netcdf(bathy_path) + self.glorys_path = bathy_path + ## User-1st, test if we can even read the angled nc files. + expt = rmom6.experiment( + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=0.05, + number_vertical_layers=75, + layer_thickness_ratio=10, + depth=4500, + minimum_depth=5, + mom_run_dir=run_dir, + mom_input_dir=input_dir, + toolpath_dir="", + ) + return expt + + def test_full_legit_expt_setup(self, full_legit_expt_setup): + assert str(full_legit_expt_setup) + + # @pytest.mark.skipif( + # IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions." + # ) + def test_tides(self, dummy_tidal_data): + """ + Test the main setup tides function! + """ + + # Generate Fake Tidal Data + ds_h, ds_u = dummy_tidal_data + + # Save to Fake Folder + ds_h.to_netcdf(self.dump_files_dir / "h_fake_tidal_data.nc") + ds_u.to_netcdf(self.dump_files_dir / "u_fake_tidal_data.nc") + + # Set other required variables needed in setup_tides + + # Lat Long + self.expt.longitude_extent = (-5, 5) + self.expt.latitude_extent = (0, 30) + # Grid Type + self.expt.grid_type = "even_spacing" + # Dates + self.expt.date_range = ("2000-01-01", "2000-01-02") + self.expt.segments = [] + # Generate Hgrid Data + self.expt.resolution = 0.1 + self.expt.hgrid = self.expt._make_hgrid() + # Create Forcing Folder + os.makedirs(self.dump_files_dir / "forcing", exist_ok=True) + + self.expt.setup_tides_boundaries(self.dump_files_dir, "fake_tidal_data.nc") + + def test_read_write_config(self): + """ + Test the read and write config functions + """ + # Write the config + self.expt.write_config_file(path=self.dump_files_dir / "config.yaml") + # Read the config + expt = rmom6.load_experiment(self.dump_files_dir / "config.yaml") + # Check if the config is the same + assert str(self.expt) == str(expt) + + def test_change_MOM_parameter(self): + """ + Test the change MOM parameter function, as well as read_MOM_file and write_MOM_file under the hood. + """ + + # Copy over the MOM Files to the dump_files_dir + base_run_dir = Path( + os.path.join( + importlib.resources.files("regional_mom6").parent, + "demos", + "premade_run_directories", + ) + ) + shutil.copytree( + base_run_dir / "common_files", self.expt.mom_run_dir, dirs_exist_ok=True + ) + self.expt.change_MOM_parameter("OBC_SEGMENT_001", "adasd", "COOL COMMENT") + MOM_override_dict = self.expt.read_MOM_file_as_dict("MOM_override") + assert MOM_override_dict["OBC_SEGMENT_001"]["value"] == "adasd" + assert MOM_override_dict["OBC_SEGMENT_001"]["comment"] == "COOL COMMENT\n" + + def test_properties_empty(self): + """ + Test the properties + """ + dss = self.expt.era5 + dss_2 = self.expt.tides_boundaries + dss_3 = self.expt.ocean_state_boundaries + dss_4 = self.expt.initial_condition + dss_5 = self.expt.bathymetry_property + print(dss, dss_2, dss_3, dss_4, dss_5) From 867e90c32c4d1c81cf8c88776608ba706b454e94 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Tue, 1 Oct 2024 15:55:49 -0600 Subject: [PATCH 64/81] Fix tides test --- tests/test_manish_branch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manish_branch.py b/tests/test_manish_branch.py index df735a45..e8d4e302 100644 --- a/tests/test_manish_branch.py +++ b/tests/test_manish_branch.py @@ -253,7 +253,7 @@ def test_tides(self, dummy_tidal_data): # Create Forcing Folder os.makedirs(self.dump_files_dir / "forcing", exist_ok=True) - self.expt.setup_tides_boundaries(self.dump_files_dir, "fake_tidal_data.nc") + self.expt.setup_boundary_tides(self.dump_files_dir, "fake_tidal_data.nc") def test_read_write_config(self): """ From 4171c3562770f5d07bc6409bf2ce0820419ad13f Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Thu, 3 Oct 2024 17:19:36 -0600 Subject: [PATCH 65/81] bathymetry now uses original land mask. Regrid in serial. Improve printed hints for large domains --- regional_mom6/regional_mom6.py | 114 +++++++++++++++------------------ 1 file changed, 53 insertions(+), 61 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 9316804a..767bf0ea 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -141,7 +141,7 @@ def load_experiment(config_file_path): expt.repeat_year_forcing = config_dict["repeat_year_forcing"] expt.ocean_mask = None expt.layout = None - expt.min_depth = config_dict["min_depth"] + expt.minimum_depth = config_dict["minimum_depth"] expt.tidal_constituents = config_dict["tidal_constituents"] print("Checking for hgrid and vgrid....") @@ -675,7 +675,7 @@ def create_empty( expt.toolpath_dir = toolpath_dir expt.mom_run_dir = mom_run_dir expt.mom_input_dir = mom_input_dir - expt.min_depth = minimum_depth + expt.minimum_depth = minimum_depth expt.depth = depth expt.layer_thickness_ratio = layer_thickness_ratio expt.number_vertical_layers = number_vertical_layers @@ -742,7 +742,7 @@ def __init__( self.repeat_year_forcing = repeat_year_forcing self.ocean_mask = None self.layout = None # This should be a tuple. Leaving in a dummy 'None' makes it easy to remind the user to provide a value later on. - self.min_depth = ( + self.minimum_depth = ( minimum_depth # Minimum depth. Shallower water will be masked out. ) self.tidal_constituents = tidal_constituents @@ -877,6 +877,15 @@ def _make_vgrid(self): vcoord = xr.Dataset({"zi": ("zi", zi), "zl": ("zl", zl)}) + ## Check whether the minimum depth is less than the first three layers + + if self.minimum_depth < zi[2]: + print( + f"Warning: Minimum depth of {self.minimum_depth}m is less than the depth of the third interface ({zi[2]}m)!\n" + + "This means that some areas may only have one or two layers between the surface and sea floor. \n" + + "For increased stability, consider increasing the minimum depth, or adjusting the vertical coordinate to add more layers near the surface." + ) + vcoord["zi"].attrs = {"units": "meters"} vcoord["zl"].attrs = {"units": "meters"} @@ -1035,7 +1044,7 @@ def write_config_file(self, path=None, export=True, quiet=False): "repeat_year_forcing": self.repeat_year_forcing, "ocean_mask": self.ocean_mask, "layout": self.layout, - "min_depth": self.min_depth, + "minimum_depth": self.minimum_depth, "vgrid": str(vgrid_path), "hgrid": str(hgrid_path), "bathymetry": self.bathymetry_property, @@ -1681,7 +1690,6 @@ def setup_bathymetry( vertical_coordinate_name="elevation", fill_channels=False, positive_down=False, - chunks="auto", ): """ Cut out and interpolate the chosen bathymetry and then fill inland lakes. @@ -1705,9 +1713,6 @@ def setup_bathymetry( but can also connect extra islands to land. Default: ``False``. positive_down (Optional[bool]): If ``True``, it assumes that bathymetry vertical coordinate is positive down. Default: ``False``. - chunks (Optional Dict[str, str]): Horizontal chunking scheme for the bathymetry, e.g., - ``{"longitude": 100, "latitude": 100}``. Use ``'longitude'`` and ``'latitude'`` rather - than the actual coordinate names in the input file. """ ## Convert the provided coordinate names into a dictionary mapping to the @@ -1717,13 +1722,8 @@ def setup_bathymetry( "yh": latitude_coordinate_name, "elevation": vertical_coordinate_name, } - if chunks != "auto": - chunks = { - coordinate_names["xh"]: chunks["longitude"], - coordinate_names["yh"]: chunks["latitude"], - } - bathymetry = xr.open_dataset(bathymetry_path, chunks=chunks)[ + bathymetry = xr.open_dataset(bathymetry_path, chunks="auto")[ coordinate_names["elevation"] ] @@ -1792,18 +1792,6 @@ def setup_bathymetry( self.mom_input_dir / "bathymetry_original.nc", mode="w", engine="netcdf4" ) - tgrid = xr.Dataset( - { - "lon": ( - ["lon"], - self.hgrid.x.isel(nxp=slice(1, None, 2), nyp=1).values, - ), - "lat": ( - ["lat"], - self.hgrid.y.isel(nxp=1, nyp=slice(1, None, 2)).values, - ), - } - ) tgrid = xr.Dataset( data_vars={ "elevation": ( @@ -1828,13 +1816,6 @@ def setup_bathymetry( ) # rewrite chunks to use lat/lon now for use with xesmf - if chunks != "auto": - chunks = { - "lon": chunks[coordinate_names["xh"]], - "lat": chunks[coordinate_names["yh"]], - } - - tgrid = tgrid.chunk(chunks) tgrid.lon.attrs["units"] = "degrees_east" tgrid.lon.attrs["_FillValue"] = 1e20 tgrid.lat.attrs["units"] = "degrees_north" @@ -1846,39 +1827,33 @@ def setup_bathymetry( ) tgrid.close() - ## Replace subprocess run with regular regridder + bathymetry_output = bathymetry_output.load() + print( "Begin regridding bathymetry...\n\n" - + "If this process hangs it means that the chosen domain might be too big to handle this way. " - + "After ensuring access to appropriate computational resources, try calling ESMF " - + "directly from a terminal in the input directory via\n\n" - + "mpirun -np `NUMBER_OF_CPUS` ESMF_Regrid -s bathymetry_original.nc -d bathymetry_unfinished.nc -m bilinear --src_var elevation --dst_var elevation --netcdf4 --src_regional --dst_regional\n\n" + + f"Original bathymetry size: {bathymetry_output.nbytes/1e6:.2f} Mb\n" + + f"Regridded size: {tgrid.nbytes/1e6:.2f} Mb\n" + + "Automatic regridding may fail if your domain is too big! If this process hangs or crashes," + + "open a terminal with appropriate computational and resources try calling ESMF " + + f"directly in the input directory {self.mom_input_dir} via\n\n" + + "`mpirun -np NUMBER_OF_CPUS ESMF_Regrid -s bathymetry_original.nc -d bathymetry_unfinished.nc -m bilinear --src_var elevation --dst_var elevation --netcdf4 --src_regional --dst_regional`\n\n" + "For details see https://xesmf.readthedocs.io/en/latest/large_problems_on_HPC.html\n\n" - + "Afterwards, we run 'tidy_bathymetry' method to skip the expensive interpolation step, and finishing metadata, encoding and cleanup." - ) - - # If we have a domain large enough for chunks, we'll run regridder with parallel=True - parallel = True - if len(tgrid.chunks) != 2: - parallel = False - print(f"Regridding in parallel: {parallel}") - bathymetry_output = bathymetry_output.chunk(chunks) - # return - regridder = xe.Regridder( - bathymetry_output, tgrid, "bilinear", parallel=parallel + + "Afterwards, run the 'expt.tidy_bathymetry' method to skip the expensive interpolation step, and finishing metadata, encoding and cleanup.\n\n\n" ) - + regridder = xe.Regridder(bathymetry_output, tgrid, "bilinear", parallel=False) bathymetry = regridder(bathymetry_output) bathymetry.to_netcdf( self.mom_input_dir / "bathymetry_unfinished.nc", mode="w", engine="netcdf4" ) print( - "Regridding finished. Now calling `tidy_bathymetry` method for some finishing touches..." + "Regridding successful! Now calling `tidy_bathymetry` method for some finishing touches..." ) self.tidy_bathymetry(fill_channels, positive_down) + print("setup bathymetry has finished successfully.") + return - def tidy_bathymetry(self, fill_channels=False, positive_down=True): + def tidy_bathymetry(self, fill_channels=False, positive_down=False): """ An auxiliary function for bathymetry used to fix up the metadata and remove inland lakes after regridding the bathymetry. Having `tidy_bathymetry` as a separate @@ -1893,12 +1868,15 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): fill_channels (Optional[bool]): Whether to fill in diagonal channels. This removes more narrow inlets, but can also connect extra islands to land. Default: ``False``. - positive_down (Optional[bool]): If ``True`` (default), assume that - bathymetry vertical coordinate is positive down. + positive_down (Optional[bool]): If ``False`` (default), assume that + bathymetry vertical coordinate is positive down, as is the case in GEBCO for example. """ ## reopen bathymetry to modify - print("Reading in regridded bathymetry to fix up metadata...", end="") + print( + "Tidy bathymetry: Reading in regridded bathymetry to fix up metadata...", + end="", + ) bathymetry = xr.open_dataset( self.mom_input_dir / "bathymetry_unfinished.nc", engine="netcdf4" ) @@ -1917,11 +1895,13 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): ## Ensure that coordinate is positive down! bathymetry["depth"] *= -1 - ## REMOVE INLAND LAKES - - ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= self.min_depth, 0, 1) + ## Make a land mask based on the bathymetry + ocean_mask = xr.where(bathymetry.copy(deep=True).depth <= 0, 0, 1) land_mask = np.abs(ocean_mask - 1) + ## REMOVE INLAND LAKES + print("done. Filling in inland lakes and channels... ", end="") + changed = True ## keeps track of whether solution has converged or not forward = True ## only useful for iterating through diagonal channel removal. Means iteration goes SW -> NE @@ -2054,6 +2034,18 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): bathymetry["depth"] *= self.ocean_mask + ## Now, any points in the bathymetry that are shallower than minimum depth are set to minimum depth. + ## This preserves the true land/ocean mask. + bathymetry["depth"] = bathymetry["depth"].where( + bathymetry["depth"] > self.minimum_depth, self.minimum_depth + ) + + bathymetry["depth"] = bathymetry["depth"].where( + (bathymetry["depth"] > self.minimum_depth) + (bathymetry["depth"] == 0), + self.minimum_depth, + ) + + ## Finally, set all land points to nan bathymetry["depth"] = bathymetry["depth"].where( bathymetry["depth"] != 0, np.nan ) @@ -2065,7 +2057,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=True): ) print("done.") - self.bathymetry = bathymetry + return def run_FRE_tools(self, layout=None): """A wrapper for FRE Tools ``check_mask``, ``make_solo_mosaic``, and ``make_quick_mosaic``. @@ -2308,7 +2300,7 @@ def setup_run_directory( # Therefore, we can use expt.segments to determine how many segments we need for MOM_input. We can fill the empty segments with a empty string to make sure it is overriden correctly. # Others - MOM_override_dict["MINIMUM_DEPTH"]["value"] = float(self.min_depth) + MOM_override_dict["MINIMUM_DEPTH"]["value"] = float(self.minimum_depth) MOM_override_dict["NK"]["value"] = len(self.vgrid.zl.values) # OBC Adjustments From 4d17aec7e5030a42e0dbf920e09bb904845c7688 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Sat, 5 Oct 2024 08:33:46 -0600 Subject: [PATCH 66/81] update test to match --- regional_mom6/regional_mom6.py | 9 +++++++-- tests/test_expt_class.py | 4 +--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 767bf0ea..56c30968 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -2771,8 +2771,13 @@ def __init__( ): ## Store coordinate names if arakawa_grid == "A" and infile is not None: - self.x = varnames["x"] - self.y = varnames["y"] + try: + self.x = varnames["x"] + self.y = varnames["y"] + ## In case user continues using T point names for A grid + except: + self.x = varnames["xh"] + self.y = varnames["yh"] elif arakawa_grid in ("B", "C"): self.xq = varnames["xq"] diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index d03c3bf5..40cd4e86 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -92,9 +92,7 @@ def test_setup_bathymetry( bathymetry_path=str(bathymetry_file), longitude_coordinate_name="silly_lon", latitude_coordinate_name="silly_lat", - vertical_coordinate_name="silly_depth", - chunks={"longitude": 10, "latitude": 10}, - ) + vertical_coordinate_name="silly_depth" ) bathymetry_file.unlink() From 5060e8d7d372e73701cbd4c828b9c09cddc46415 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Sat, 5 Oct 2024 08:34:15 -0600 Subject: [PATCH 67/81] black --- regional_mom6/regional_mom6.py | 4 ++-- tests/test_expt_class.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 56c30968..0c592cbb 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -2775,9 +2775,9 @@ def __init__( self.x = varnames["x"] self.y = varnames["y"] ## In case user continues using T point names for A grid - except: + except: self.x = varnames["xh"] - self.y = varnames["yh"] + self.y = varnames["yh"] elif arakawa_grid in ("B", "C"): self.xq = varnames["xq"] diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index 40cd4e86..62f6038f 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -92,7 +92,8 @@ def test_setup_bathymetry( bathymetry_path=str(bathymetry_file), longitude_coordinate_name="silly_lon", latitude_coordinate_name="silly_lat", - vertical_coordinate_name="silly_depth" ) + vertical_coordinate_name="silly_depth", + ) bathymetry_file.unlink() From 903d329c0c4cb2af92a660768045f73c751d4896 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Sat, 5 Oct 2024 09:43:24 -0600 Subject: [PATCH 68/81] allow reading of lon lat extents from pre existing hgrid --- .../common_files/MOM_input | 6 +- regional_mom6/regional_mom6.py | 97 ++++++++++--------- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/demos/premade_run_directories/common_files/MOM_input b/demos/premade_run_directories/common_files/MOM_input index 79ea05a3..d259b265 100755 --- a/demos/premade_run_directories/common_files/MOM_input +++ b/demos/premade_run_directories/common_files/MOM_input @@ -208,7 +208,7 @@ INIT_LAYERS_FROM_Z_FILE = True ! [Boolean] default = False ! Z-space file on a latitude-longitude grid. ! === module MOM_initialize_layers_from_Z === -TEMP_SALT_Z_INIT_FILE = "forcing/init_tracers.nc" ! default = "temp_salt_z.nc" +TEMP_SALT_Z_INIT_FILE = "init_tracers.nc" ! default = "temp_salt_z.nc" ! The name of the z-space input file used to initialize temperatures (T) and ! salinities (S). If T and S are not in the same file, TEMP_Z_INIT_FILE and ! SALT_Z_INIT_FILE must be set. @@ -223,7 +223,7 @@ TEMP_SALT_INIT_VERTICAL_REMAP_ONLY = True ! [Boolean] default = False DEPRESS_INITIAL_SURFACE = True ! [Boolean] default = False ! If true, depress the initial surface to avoid huge tsunamis when a large ! surface pressure is applied. -SURFACE_HEIGHT_IC_FILE = "forcing/init_eta.nc" ! +SURFACE_HEIGHT_IC_FILE = "init_eta.nc" ! ! The initial condition file for the surface height. SURFACE_HEIGHT_IC_VAR = "eta_t" ! default = "SSH" ! The initial condition variable for the surface height. @@ -238,7 +238,7 @@ VELOCITY_CONFIG = "file" ! default = "zero" ! rossby_front - a mixed layer front in thermal wind balance. ! soliton - Equatorial Rossby soliton. ! USER - call a user modified routine. -VELOCITY_FILE = "forcing/init_vel.nc" ! +VELOCITY_FILE = "init_vel.nc" ! ! The name of the velocity initial condition file. ! === module MOM_diag_mediator === NUM_DIAG_COORDS = 1 ! default = 1 diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 0c592cbb..d93689dd 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -349,14 +349,14 @@ def get_glorys_data( path = Path(download_path) if modify_existing: - file = open(Path(path / "get_glorysdata.sh"), "r") + file = open(Path(path / "get_glorys_data.sh"), "r") lines = file.readlines() file.close() else: lines = ["#!/bin/bash\n"] - file = open(Path(path / "get_glorysdata.sh"), "w") + file = open(Path(path / "get_glorys_data.sh"), "w") lines.append( f""" @@ -690,8 +690,6 @@ def create_empty( def __init__( self, *, - longitude_extent, - latitude_extent, date_range, resolution, number_vertical_layers, @@ -700,6 +698,8 @@ def __init__( mom_run_dir, mom_input_dir, toolpath_dir, + longitude_extent = None, + latitude_extent = None, grid_type="even_spacing", repeat_year_forcing=False, read_existing_grids=False, @@ -719,8 +719,6 @@ def __init__( # ## Set up the experiment with no config file ## in case list was given, convert to tuples self.expt_name = name - self.longitude_extent = tuple(longitude_extent) - self.latitude_extent = tuple(latitude_extent) self.date_range = tuple(date_range) self.mom_run_dir = Path(mom_run_dir) @@ -751,6 +749,15 @@ def __init__( try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") self.vgrid = xr.open_dataset(self.mom_input_dir / "vcoord.nc") + self.longitude_extent = ( + float(self.hgrid.x.min()), + float(self.hgrid.x.max()), + ) + self.latitude_extent = ( + float(self.hgrid.y.min()), + float(self.hgrid.y.max()), + ) + except: print( "Error while reading in existing grids!\n\n" @@ -758,6 +765,8 @@ def __init__( ) raise ValueError else: + self.longitude_extent = tuple(longitude_extent) + self.latitude_extent = tuple(latitude_extent) self.hgrid = self._make_hgrid() self.vgrid = self._make_vgrid() @@ -1363,7 +1372,7 @@ def setup_initial_condition( print("Saving outputs... ", end="") vel_out.fillna(0).to_netcdf( - self.mom_input_dir / "forcing/init_vel.nc", + self.mom_input_dir / "init_vel.nc", mode="w", encoding={ "u": {"_FillValue": netCDF4.default_fillvals["f4"]}, @@ -1372,7 +1381,7 @@ def setup_initial_condition( ) tracers_out.to_netcdf( - self.mom_input_dir / "forcing/init_tracers.nc", + self.mom_input_dir / "init_tracers.nc", mode="w", encoding={ "xh": {"_FillValue": None}, @@ -1383,7 +1392,7 @@ def setup_initial_condition( }, ) eta_out.to_netcdf( - self.mom_input_dir / "forcing/init_eta.nc", + self.mom_input_dir / "init_eta.nc", mode="w", encoding={ "xh": {"_FillValue": None}, @@ -1414,47 +1423,47 @@ def get_glorys_rectangular( # Initial Condition get_glorys_data( - self.longitude_extent, - self.latitude_extent, - [ + longitude_extent = [float(self.hgrid.x.min()), float(self.hgrid.x.max())], + latitude_extent = [float(self.hgrid.y.min()), float(self.hgrid.y.max())], + timerange = [ self.date_range[0], self.date_range[0] + datetime.timedelta(days=1), ], - "ic_unprocessed", - raw_boundaries_path, - modify_existing=False, + segment_name = "ic_unprocessed", + download_path = raw_boundaries_path, + modify_existing=False, # This is the first line, so start bash script anew ) if "east" in boundaries: get_glorys_data( - [self.longitude_extent[1], self.longitude_extent[1]], - [self.latitude_extent[0], self.latitude_extent[1]], - self.date_range, - "east_unprocessed", - raw_boundaries_path, + longitude_extent = [float(self.hgrid.x.isel(nxp = -1).min()), float(self.hgrid.x.isel(nxp = -1).max())], ## Collect from Eastern (x = -1) side + latitude_extent = [float(self.hgrid.y.isel(nxp = -1).min()), float(self.hgrid.y.isel(nxp = -1).max())], + timerange = self.date_range, + segment_name = "east_unprocessed", + download_path = raw_boundaries_path, ) if "west" in boundaries: get_glorys_data( - [self.longitude_extent[0], self.longitude_extent[0]], - [self.latitude_extent[0], self.latitude_extent[1]], - self.date_range, - "west_unprocessed", - raw_boundaries_path, + longitude_extent = [float(self.hgrid.x.isel(nxp = 0).min()), float(self.hgrid.x.isel(nxp = 0).max())], ## Collect from Western (x = 0) side + latitude_extent = [float(self.hgrid.y.isel(nxp = 0).min()), float(self.hgrid.y.isel(nxp = 0).max())], + timerange = self.date_range, + segment_name = "west_unprocessed", + download_path = raw_boundaries_path, ) - if "north" in boundaries: + if "south" in boundaries: get_glorys_data( - [self.longitude_extent[0], self.longitude_extent[1]], - [self.latitude_extent[1], self.latitude_extent[1]], - self.date_range, - "north_unprocessed", - raw_boundaries_path, + longitude_extent = [float(self.hgrid.x.isel(nyp = 0).min()), float(self.hgrid.x.isel(nyp = 0).max())], ## Collect from Southern (y = 0) side + latitude_extent = [float(self.hgrid.y.isel(nyp = 0).min()), float(self.hgrid.y.isel(nyp = 0).max())], + timerange = self.date_range, + segment_name = "south_unprocessed", + download_path = raw_boundaries_path, ) - if "south" in boundaries: + if "north" in boundaries: get_glorys_data( - [self.longitude_extent[0], self.longitude_extent[1]], - [self.latitude_extent[0], self.latitude_extent[0]], - self.date_range, - "south_unprocessed", - raw_boundaries_path, + longitude_extent = [float(self.hgrid.x.isel(nyp = -1).min()), float(self.hgrid.x.isel(nyp = -1).max())], ## Collect from Southern (y = -1) side + latitude_extent = [float(self.hgrid.y.isel(nyp = -1).min()), float(self.hgrid.y.isel(nyp = -1).max())], + timerange = self.date_range, + segment_name = "north_unprocessed", + download_path = raw_boundaries_path, ) print( @@ -2357,11 +2366,11 @@ def setup_run_directory( ) # 1,2,3,4 for rectangular boundaries, BUT if we have less than 4 segments we use the index to specific the number, but keep filenames as if we had four boundaries MOM_override_dict[key_DATA][ "value" - ] = f'"U=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(u),V=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(v),SSH=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(eta),TEMP=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(temp),SALT=file:forcing/forcing_obc_segment_00{file_num_obc}.nc(salt)' + ] = f'"U=file:forcing_obc_segment_00{file_num_obc}.nc(u),V=file:forcing_obc_segment_00{file_num_obc}.nc(v),SSH=file:forcing_obc_segment_00{file_num_obc}.nc(eta),TEMP=file:forcing_obc_segment_00{file_num_obc}.nc(temp),SALT=file:forcing_obc_segment_00{file_num_obc}.nc(salt)' if with_tides_rectangular: MOM_override_dict[key_DATA]["value"] = ( MOM_override_dict[key_DATA]["value"] - + f',Uamp=file:forcing/tu_segment_00{file_num_obc}.nc(uamp),Uphase=file:forcing/tu_segment_00{file_num_obc}.nc(uphase),Vamp=file:forcing/tu_segment_00{file_num_obc}.nc(vamp),Vphase=file:forcing/tu_segment_00{file_num_obc}.nc(vphase),SSHamp=file:forcing/tz_segment_00{file_num_obc}.nc(zamp),SSHphase=file:forcing/tz_segment_00{file_num_obc}.nc(zphase)"' + + f',Uamp=file:tu_segment_00{file_num_obc}.nc(uamp),Uphase=file:tu_segment_00{file_num_obc}.nc(uphase),Vamp=file:tu_segment_00{file_num_obc}.nc(vamp),Vphase=file:tu_segment_00{file_num_obc}.nc(vphase),SSHamp=file:tz_segment_00{file_num_obc}.nc(zamp),SSHphase=file:tz_segment_00{file_num_obc}.nc(zphase)"' ) else: MOM_override_dict[key_DATA]["value"] = ( @@ -2687,7 +2696,7 @@ def setup_era5(self, era5_path): q.q.attrs = {"long_name": "Specific Humidity", "units": "kg/kg"} q.to_netcdf( - f"{self.mom_input_dir}/forcing/q_ERA5.nc", + f"{self.mom_input_dir}/q_ERA5.nc", unlimited_dims="time", encoding={"q": {"dtype": "double"}}, ) @@ -2702,7 +2711,7 @@ def setup_era5(self, era5_path): "units": "kg m**-2 s**-1", } trr.to_netcdf( - f"{self.mom_input_dir}/forcing/trr_ERA5.nc", + f"{self.mom_input_dir}/trr_ERA5.nc", unlimited_dims="time", encoding={"trr": {"dtype": "double"}}, ) @@ -2712,7 +2721,7 @@ def setup_era5(self, era5_path): pass else: rawdata[fname].to_netcdf( - f"{self.mom_input_dir}/forcing/{fname}_ERA5.nc", + f"{self.mom_input_dir}/{fname}_ERA5.nc", unlimited_dims="time", encoding={vname: {"dtype": "double"}}, ) @@ -3227,7 +3236,7 @@ def regrid_velocity_tracers(self): with ProgressBar(): segment_out.load().to_netcdf( - self.outfolder / f"forcing/forcing_obc_{self.segment_name}.nc", + self.outfolder / f"forcing_obc_{self.segment_name}.nc", encoding=encoding_dict, unlimited_dims="time", ) @@ -3399,7 +3408,7 @@ def encode_tidal_files_and_output(self, ds, filename): dataset (xarray.Dataset): The processed tidal dataset filename (str): The output file name Returns: - *.nc files: Regridded [FILENAME] files in 'self.outfolder/forcing/[filename]_[segmentname].nc' + *.nc files: Regridded [FILENAME] files in 'self.outfolder/[filename]_[segmentname].nc' General Description: This tidal data functions are sourced from the GFDL NWA25 and changed in the following ways: From 69407fda5ab0320028722af25d520a14b1a552ec Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Sat, 5 Oct 2024 09:44:08 -0600 Subject: [PATCH 69/81] black --- regional_mom6/regional_mom6.py | 80 ++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d93689dd..f3a15828 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -698,8 +698,8 @@ def __init__( mom_run_dir, mom_input_dir, toolpath_dir, - longitude_extent = None, - latitude_extent = None, + longitude_extent=None, + latitude_extent=None, grid_type="even_spacing", repeat_year_forcing=False, read_existing_grids=False, @@ -1423,47 +1423,71 @@ def get_glorys_rectangular( # Initial Condition get_glorys_data( - longitude_extent = [float(self.hgrid.x.min()), float(self.hgrid.x.max())], - latitude_extent = [float(self.hgrid.y.min()), float(self.hgrid.y.max())], - timerange = [ + longitude_extent=[float(self.hgrid.x.min()), float(self.hgrid.x.max())], + latitude_extent=[float(self.hgrid.y.min()), float(self.hgrid.y.max())], + timerange=[ self.date_range[0], self.date_range[0] + datetime.timedelta(days=1), ], - segment_name = "ic_unprocessed", - download_path = raw_boundaries_path, - modify_existing=False, # This is the first line, so start bash script anew + segment_name="ic_unprocessed", + download_path=raw_boundaries_path, + modify_existing=False, # This is the first line, so start bash script anew ) if "east" in boundaries: get_glorys_data( - longitude_extent = [float(self.hgrid.x.isel(nxp = -1).min()), float(self.hgrid.x.isel(nxp = -1).max())], ## Collect from Eastern (x = -1) side - latitude_extent = [float(self.hgrid.y.isel(nxp = -1).min()), float(self.hgrid.y.isel(nxp = -1).max())], - timerange = self.date_range, - segment_name = "east_unprocessed", - download_path = raw_boundaries_path, + longitude_extent=[ + float(self.hgrid.x.isel(nxp=-1).min()), + float(self.hgrid.x.isel(nxp=-1).max()), + ], ## Collect from Eastern (x = -1) side + latitude_extent=[ + float(self.hgrid.y.isel(nxp=-1).min()), + float(self.hgrid.y.isel(nxp=-1).max()), + ], + timerange=self.date_range, + segment_name="east_unprocessed", + download_path=raw_boundaries_path, ) if "west" in boundaries: get_glorys_data( - longitude_extent = [float(self.hgrid.x.isel(nxp = 0).min()), float(self.hgrid.x.isel(nxp = 0).max())], ## Collect from Western (x = 0) side - latitude_extent = [float(self.hgrid.y.isel(nxp = 0).min()), float(self.hgrid.y.isel(nxp = 0).max())], - timerange = self.date_range, - segment_name = "west_unprocessed", - download_path = raw_boundaries_path, + longitude_extent=[ + float(self.hgrid.x.isel(nxp=0).min()), + float(self.hgrid.x.isel(nxp=0).max()), + ], ## Collect from Western (x = 0) side + latitude_extent=[ + float(self.hgrid.y.isel(nxp=0).min()), + float(self.hgrid.y.isel(nxp=0).max()), + ], + timerange=self.date_range, + segment_name="west_unprocessed", + download_path=raw_boundaries_path, ) if "south" in boundaries: get_glorys_data( - longitude_extent = [float(self.hgrid.x.isel(nyp = 0).min()), float(self.hgrid.x.isel(nyp = 0).max())], ## Collect from Southern (y = 0) side - latitude_extent = [float(self.hgrid.y.isel(nyp = 0).min()), float(self.hgrid.y.isel(nyp = 0).max())], - timerange = self.date_range, - segment_name = "south_unprocessed", - download_path = raw_boundaries_path, + longitude_extent=[ + float(self.hgrid.x.isel(nyp=0).min()), + float(self.hgrid.x.isel(nyp=0).max()), + ], ## Collect from Southern (y = 0) side + latitude_extent=[ + float(self.hgrid.y.isel(nyp=0).min()), + float(self.hgrid.y.isel(nyp=0).max()), + ], + timerange=self.date_range, + segment_name="south_unprocessed", + download_path=raw_boundaries_path, ) if "north" in boundaries: get_glorys_data( - longitude_extent = [float(self.hgrid.x.isel(nyp = -1).min()), float(self.hgrid.x.isel(nyp = -1).max())], ## Collect from Southern (y = -1) side - latitude_extent = [float(self.hgrid.y.isel(nyp = -1).min()), float(self.hgrid.y.isel(nyp = -1).max())], - timerange = self.date_range, - segment_name = "north_unprocessed", - download_path = raw_boundaries_path, + longitude_extent=[ + float(self.hgrid.x.isel(nyp=-1).min()), + float(self.hgrid.x.isel(nyp=-1).max()), + ], ## Collect from Southern (y = -1) side + latitude_extent=[ + float(self.hgrid.y.isel(nyp=-1).min()), + float(self.hgrid.y.isel(nyp=-1).max()), + ], + timerange=self.date_range, + segment_name="north_unprocessed", + download_path=raw_boundaries_path, ) print( From bb8b9ec081c2da558da9599ce9d36b7ba656a989 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 9 Oct 2024 14:22:51 -0600 Subject: [PATCH 70/81] bugfixing curved boundary regridding for tracer IC and bathymetry --- regional_mom6/regional_mom6.py | 148 ++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index f3a15828..4d201fa9 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -137,7 +137,7 @@ def load_experiment(config_file_path): expt.number_vertical_layers = config_dict["number_vertical_layers"] expt.layer_thickness_ratio = config_dict["layer_thickness_ratio"] expt.depth = config_dict["depth"] - expt.grid_type = config_dict["grid_type"] + expt.hgrid_type = config_dict["hgrid_type"] expt.repeat_year_forcing = config_dict["repeat_year_forcing"] expt.ocean_mask = None expt.layout = None @@ -616,7 +616,7 @@ class experiment: mom_input_dir (str): Path of the MOM6 input directory, to receive the forcing files. toolpath_dir (str): Path of GFDL's FRE tools (https://github.com/NOAA-GFDL/FRE-NCtools) binaries. - grid_type (Optional[str]): Type of horizontal grid to generate. + hgrid_type (Optional[str]): Type of horizontal grid to generate. Currently, only ``'even_spacing'`` is supported. repeat_year_forcing (Optional[bool]): When ``True`` the experiment runs with repeat-year forcing. When ``False`` (default) then inter-annual forcing is used. @@ -640,7 +640,7 @@ def create_empty( mom_run_dir=None, mom_input_dir=None, toolpath_dir=None, - grid_type="even_spacing", + hgrid_type="even_spacing", repeat_year_forcing=False, minimum_depth=4, tidal_constituents=["M2"], @@ -662,7 +662,7 @@ def create_empty( mom_input_dir=None, toolpath_dir=None, create_empty=True, - grid_type=None, + hgrid_type=None, repeat_year_forcing=None, tidal_constituents=None, name=None, @@ -671,7 +671,7 @@ def create_empty( expt.expt_name = name expt.tidal_constituents = tidal_constituents expt.repeat_year_forcing = repeat_year_forcing - expt.grid_type = grid_type + expt.hgrid_type = hgrid_type expt.toolpath_dir = toolpath_dir expt.mom_run_dir = mom_run_dir expt.mom_input_dir = mom_input_dir @@ -700,9 +700,9 @@ def __init__( toolpath_dir, longitude_extent=None, latitude_extent=None, - grid_type="even_spacing", + hgrid_type="even_spacing", + vgrid_type="hyperbolic_tangent", repeat_year_forcing=False, - read_existing_grids=False, minimum_depth=4, tidal_constituents=["M2"], create_empty=False, @@ -736,19 +736,17 @@ def __init__( self.number_vertical_layers = number_vertical_layers self.layer_thickness_ratio = layer_thickness_ratio self.depth = depth - self.grid_type = grid_type + self.hgrid_type = hgrid_type + self.vgrid_type = vgrid_type self.repeat_year_forcing = repeat_year_forcing self.ocean_mask = None self.layout = None # This should be a tuple. Leaving in a dummy 'None' makes it easy to remind the user to provide a value later on. - self.minimum_depth = ( - minimum_depth # Minimum depth. Shallower water will be masked out. - ) + self.minimum_depth = minimum_depth # Minimum depth allowed in bathy file self.tidal_constituents = tidal_constituents - if read_existing_grids: + if hgrid_type == "from_file": try: self.hgrid = xr.open_dataset(self.mom_input_dir / "hgrid.nc") - self.vgrid = xr.open_dataset(self.mom_input_dir / "vcoord.nc") self.longitude_extent = ( float(self.hgrid.x.min()), float(self.hgrid.x.max()), @@ -757,19 +755,31 @@ def __init__( float(self.hgrid.y.min()), float(self.hgrid.y.max()), ) - except: print( - "Error while reading in existing grids!\n\n" - + f"Make sure `hgrid.nc` and `vcoord.nc` exists in {self.mom_input_dir} directory." + "Error while reading in existing horizontal grid!\n\n" + + f"Make sure `hgrid.nc`exists in {self.mom_input_dir} directory." ) raise ValueError else: self.longitude_extent = tuple(longitude_extent) self.latitude_extent = tuple(latitude_extent) self.hgrid = self._make_hgrid() + + if vgrid_type == "from_file": + try: + self.vgrid = xr.open_dataset(self.mom_input_dir / "vcoord.nc") + + except: + print( + "Error while reading in existing vertical coordinates!\n\n" + + f"Make sure `vcoord.nc`exists in {self.mom_input_dir} directory." + ) + raise ValueError + else: self.vgrid = self._make_vgrid() + self.segments = ( {} ) # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) @@ -824,10 +834,10 @@ def _make_hgrid(self): """ assert ( - self.grid_type == "even_spacing" + self.hgrid_type == "even_spacing" ), "only even_spacing grid type is implemented" - if self.grid_type == "even_spacing": + if self.hgrid_type == "even_spacing": # longitudes are evenly spaced based on resolution and bounds nx = int( @@ -902,6 +912,8 @@ def _make_vgrid(self): return vcoord + + @property def ocean_state_boundaries(self): """ @@ -1049,7 +1061,7 @@ def write_config_file(self, path=None, export=True, quiet=False): "number_vertical_layers": self.number_vertical_layers, "layer_thickness_ratio": self.layer_thickness_ratio, "depth": self.depth, - "grid_type": self.grid_type, + "grid_type": self.hgrid_type, "repeat_year_forcing": self.repeat_year_forcing, "ocean_mask": self.ocean_mask, "layout": self.layout, @@ -1233,17 +1245,11 @@ def setup_initial_condition( ) ## Construct the cell centre grid for tracers (xh, yh). - tgrid = xr.Dataset( - { - "lon": ( - ["lon"], - self.hgrid.x.isel(nxp=slice(1, None, 2), nyp=1).values, - ), - "lat": ( - ["lat"], - self.hgrid.y.isel(nxp=1, nyp=slice(1, None, 2)).values, - ), - } + tgrid = ( + self.hgrid[["x", "y"]] + .isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)) + .rename({"x": "lon", "y": "lat","nxp":"nx","nyp":"ny"}) + .set_coords(["lat", "lon"]) ) # NaNs might be here from the land mask of the model that the IC has come from. @@ -1327,12 +1333,20 @@ def setup_initial_condition( regridder_t(ic_raw_tracers[varnames["tracers"][i]]).rename(i) for i in varnames["tracers"] ] - ).rename({"lon": "xh", "lat": "yh", varnames["zl"]: "zl"}) + ).rename({"lon":"xh","lat":"yh",varnames["zl"]: "zl"}).transpose("zl","ny","nx") + + # tracers_out = tracers_out.assign_coords( + # {"nx":np.arange(tracers_out.sizes["nx"]).astype(float), + # "ny":np.arange(tracers_out.sizes["ny"]).astype(float)}) + + tracers_out = tracers_out.assign_coords( + {"nx":np.arange(tracers_out.sizes["nx"]).astype(float), + "ny":np.arange(tracers_out.sizes["ny"]).astype(float)}) print("Done.\nRegridding Free surface... ", end="") eta_out = ( - regridder_t(ic_raw_eta).rename({"lon": "xh", "lat": "yh"}).rename("eta_t") + regridder_t(ic_raw_eta).rename({"lon":"xh","lat":"yh"}).rename("eta_t").transpose("ny","nx") ) ## eta_t is the name set in MOM_input by default print("Done.") @@ -1384,9 +1398,9 @@ def setup_initial_condition( self.mom_input_dir / "init_tracers.nc", mode="w", encoding={ - "xh": {"_FillValue": None}, - "yh": {"_FillValue": None}, - "zl": {"_FillValue": None}, + # "xh": {"_FillValue": None}, + # "yh": {"_FillValue": None}, + # "zl": {"_FillValue": None}, "temp": {"_FillValue": -1e20, "missing_value": -1e20}, "salt": {"_FillValue": -1e20, "missing_value": -1e20}, }, @@ -1395,8 +1409,8 @@ def setup_initial_condition( self.mom_input_dir / "init_eta.nc", mode="w", encoding={ - "xh": {"_FillValue": None}, - "yh": {"_FillValue": None}, + # "xh": {"_FillValue": None}, + # "yh": {"_FillValue": None}, "eta_t": {"_FillValue": None}, }, ) @@ -1720,7 +1734,7 @@ def setup_bathymetry( bathymetry_path, longitude_coordinate_name="lon", latitude_coordinate_name="lat", - vertical_coordinate_name="elevation", + vertical_coordinate_name="elevation", # This is to match GEBCO fill_channels=False, positive_down=False, ): @@ -1753,11 +1767,11 @@ def setup_bathymetry( coordinate_names = { "xh": longitude_coordinate_name, "yh": latitude_coordinate_name, - "elevation": vertical_coordinate_name, + "depth": vertical_coordinate_name, } bathymetry = xr.open_dataset(bathymetry_path, chunks="auto")[ - coordinate_names["elevation"] + coordinate_names["depth"] ] bathymetry = bathymetry.sel( @@ -1804,7 +1818,7 @@ def setup_bathymetry( ) bathymetry.attrs["missing_value"] = -1e20 # missing value expected by FRE tools - bathymetry_output = xr.Dataset({"elevation": bathymetry}) + bathymetry_output = xr.Dataset({"depth": bathymetry}) bathymetry.close() bathymetry_output = bathymetry_output.rename( @@ -1812,23 +1826,23 @@ def setup_bathymetry( ) bathymetry_output.lon.attrs["units"] = "degrees_east" bathymetry_output.lat.attrs["units"] = "degrees_north" - bathymetry_output.elevation.attrs["_FillValue"] = -1e20 - bathymetry_output.elevation.attrs["units"] = "meters" - bathymetry_output.elevation.attrs["standard_name"] = ( + bathymetry_output.depth.attrs["_FillValue"] = -1e20 + bathymetry_output.depth.attrs["units"] = "meters" + bathymetry_output.depth.attrs["standard_name"] = ( "height_above_reference_ellipsoid" ) - bathymetry_output.elevation.attrs["long_name"] = ( + bathymetry_output.depth.attrs["long_name"] = ( "Elevation relative to sea level" ) - bathymetry_output.elevation.attrs["coordinates"] = "lon lat" + bathymetry_output.depth.attrs["coordinates"] = "lon lat" bathymetry_output.to_netcdf( self.mom_input_dir / "bathymetry_original.nc", mode="w", engine="netcdf4" ) tgrid = xr.Dataset( data_vars={ - "elevation": ( - ["lat", "lon"], + "depth": ( + ["nx", "ny"], np.zeros( self.hgrid.x.isel( nxp=slice(1, None, 2), nyp=slice(1, None, 2) @@ -1838,12 +1852,12 @@ def setup_bathymetry( }, coords={ "lon": ( - ["lon"], - self.hgrid.x.isel(nxp=slice(1, None, 2), nyp=1).values, + ["nx","ny"], + self.hgrid.x.isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)).values, ), "lat": ( - ["lat"], - self.hgrid.y.isel(nxp=1, nyp=slice(1, None, 2)).values, + ["nx","ny"], + self.hgrid.y.isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)).values, ), }, ) @@ -1853,8 +1867,8 @@ def setup_bathymetry( tgrid.lon.attrs["_FillValue"] = 1e20 tgrid.lat.attrs["units"] = "degrees_north" tgrid.lat.attrs["_FillValue"] = 1e20 - tgrid.elevation.attrs["units"] = "meters" - tgrid.elevation.attrs["coordinates"] = "lon lat" + tgrid.depth.attrs["units"] = "meters" + tgrid.depth.attrs["coordinates"] = "lon lat" tgrid.to_netcdf( self.mom_input_dir / "bathymetry_unfinished.nc", mode="w", engine="netcdf4" ) @@ -1886,7 +1900,7 @@ def setup_bathymetry( print("setup bathymetry has finished successfully.") return - def tidy_bathymetry(self, fill_channels=False, positive_down=False): + def tidy_bathymetry(self, fill_channels=False, positive_down=False,vertical_coordinate_name = "depth"): """ An auxiliary function for bathymetry used to fix up the metadata and remove inland lakes after regridding the bathymetry. Having `tidy_bathymetry` as a separate @@ -1916,7 +1930,7 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=False): ## Ensure correct encoding bathymetry = xr.Dataset( - {"depth": (["ny", "nx"], bathymetry["elevation"].values)} + {"depth": (["ny", "nx"], bathymetry[vertical_coordinate_name].values)} ) bathymetry.attrs["depth"] = "meters" bathymetry.attrs["standard_name"] = "bathymetric depth at T-cell centers" @@ -2070,18 +2084,9 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=False): ## Now, any points in the bathymetry that are shallower than minimum depth are set to minimum depth. ## This preserves the true land/ocean mask. bathymetry["depth"] = bathymetry["depth"].where( - bathymetry["depth"] > self.minimum_depth, self.minimum_depth - ) - - bathymetry["depth"] = bathymetry["depth"].where( - (bathymetry["depth"] > self.minimum_depth) + (bathymetry["depth"] == 0), - self.minimum_depth, - ) - - ## Finally, set all land points to nan - bathymetry["depth"] = bathymetry["depth"].where( - bathymetry["depth"] != 0, np.nan + bathymetry["depth"] > 0, np.nan ) + bathymetry["depth"] = bathymetry["depth"].where(~(bathymetry.depth <= self.minimum_depth),self.minimum_depth + 0.1) bathymetry.expand_dims({"ntiles": 1}).to_netcdf( self.mom_input_dir / "bathymetry.nc", @@ -3246,14 +3251,19 @@ def regrid_velocity_tracers(self): ).data, ) - # Add units to the lat / lon to keep the `categorize_axis_from_units` checker happy + # Add units to the lat / lon to keep the `categorize_axis_from_units` checker from throwing warnings segment_out[f"lat_{self.segment_name}"].attrs = { "units": "degrees_north", } segment_out[f"lon_{self.segment_name}"].attrs = { "units": "degrees_east", } - + segment_out[f"ny_{self.segment_name}"].attrs = { + "units": "degrees_north", + } + segment_out[f"nx_{self.segment_name}"].attrs = { + "units": "degrees_east", + } # If repeat-year forcing, add modulo coordinate if self.repeat_year_forcing: segment_out["time"] = segment_out["time"].assign_attrs({"modulo": " "}) From 848a712dcd666b8dd3a42ac41b7bdff6caa9099e Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 9 Oct 2024 14:23:42 -0600 Subject: [PATCH 71/81] black --- regional_mom6/regional_mom6.py | 61 ++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 4d201fa9..5c7ac60e 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -769,7 +769,7 @@ def __init__( if vgrid_type == "from_file": try: self.vgrid = xr.open_dataset(self.mom_input_dir / "vcoord.nc") - + except: print( "Error while reading in existing vertical coordinates!\n\n" @@ -779,7 +779,6 @@ def __init__( else: self.vgrid = self._make_vgrid() - self.segments = ( {} ) # Holds segements for use in setting up the ocean state boundary conditions (GLORYS) and the tidal boundary conditions (TPXO) @@ -912,8 +911,6 @@ def _make_vgrid(self): return vcoord - - @property def ocean_state_boundaries(self): """ @@ -1248,7 +1245,7 @@ def setup_initial_condition( tgrid = ( self.hgrid[["x", "y"]] .isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)) - .rename({"x": "lon", "y": "lat","nxp":"nx","nyp":"ny"}) + .rename({"x": "lon", "y": "lat", "nxp": "nx", "nyp": "ny"}) .set_coords(["lat", "lon"]) ) @@ -1328,25 +1325,35 @@ def setup_initial_condition( print("Done.\nRegridding Tracers... ", end="") - tracers_out = xr.merge( - [ - regridder_t(ic_raw_tracers[varnames["tracers"][i]]).rename(i) - for i in varnames["tracers"] - ] - ).rename({"lon":"xh","lat":"yh",varnames["zl"]: "zl"}).transpose("zl","ny","nx") + tracers_out = ( + xr.merge( + [ + regridder_t(ic_raw_tracers[varnames["tracers"][i]]).rename(i) + for i in varnames["tracers"] + ] + ) + .rename({"lon": "xh", "lat": "yh", varnames["zl"]: "zl"}) + .transpose("zl", "ny", "nx") + ) # tracers_out = tracers_out.assign_coords( # {"nx":np.arange(tracers_out.sizes["nx"]).astype(float), # "ny":np.arange(tracers_out.sizes["ny"]).astype(float)}) tracers_out = tracers_out.assign_coords( - {"nx":np.arange(tracers_out.sizes["nx"]).astype(float), - "ny":np.arange(tracers_out.sizes["ny"]).astype(float)}) + { + "nx": np.arange(tracers_out.sizes["nx"]).astype(float), + "ny": np.arange(tracers_out.sizes["ny"]).astype(float), + } + ) print("Done.\nRegridding Free surface... ", end="") eta_out = ( - regridder_t(ic_raw_eta).rename({"lon":"xh","lat":"yh"}).rename("eta_t").transpose("ny","nx") + regridder_t(ic_raw_eta) + .rename({"lon": "xh", "lat": "yh"}) + .rename("eta_t") + .transpose("ny", "nx") ) ## eta_t is the name set in MOM_input by default print("Done.") @@ -1734,7 +1741,7 @@ def setup_bathymetry( bathymetry_path, longitude_coordinate_name="lon", latitude_coordinate_name="lat", - vertical_coordinate_name="elevation", # This is to match GEBCO + vertical_coordinate_name="elevation", # This is to match GEBCO fill_channels=False, positive_down=False, ): @@ -1831,9 +1838,7 @@ def setup_bathymetry( bathymetry_output.depth.attrs["standard_name"] = ( "height_above_reference_ellipsoid" ) - bathymetry_output.depth.attrs["long_name"] = ( - "Elevation relative to sea level" - ) + bathymetry_output.depth.attrs["long_name"] = "Elevation relative to sea level" bathymetry_output.depth.attrs["coordinates"] = "lon lat" bathymetry_output.to_netcdf( self.mom_input_dir / "bathymetry_original.nc", mode="w", engine="netcdf4" @@ -1852,12 +1857,16 @@ def setup_bathymetry( }, coords={ "lon": ( - ["nx","ny"], - self.hgrid.x.isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)).values, + ["nx", "ny"], + self.hgrid.x.isel( + nxp=slice(1, None, 2), nyp=slice(1, None, 2) + ).values, ), "lat": ( - ["nx","ny"], - self.hgrid.y.isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)).values, + ["nx", "ny"], + self.hgrid.y.isel( + nxp=slice(1, None, 2), nyp=slice(1, None, 2) + ).values, ), }, ) @@ -1900,7 +1909,9 @@ def setup_bathymetry( print("setup bathymetry has finished successfully.") return - def tidy_bathymetry(self, fill_channels=False, positive_down=False,vertical_coordinate_name = "depth"): + def tidy_bathymetry( + self, fill_channels=False, positive_down=False, vertical_coordinate_name="depth" + ): """ An auxiliary function for bathymetry used to fix up the metadata and remove inland lakes after regridding the bathymetry. Having `tidy_bathymetry` as a separate @@ -2083,10 +2094,10 @@ def tidy_bathymetry(self, fill_channels=False, positive_down=False,vertical_coor ## Now, any points in the bathymetry that are shallower than minimum depth are set to minimum depth. ## This preserves the true land/ocean mask. + bathymetry["depth"] = bathymetry["depth"].where(bathymetry["depth"] > 0, np.nan) bathymetry["depth"] = bathymetry["depth"].where( - bathymetry["depth"] > 0, np.nan + ~(bathymetry.depth <= self.minimum_depth), self.minimum_depth + 0.1 ) - bathymetry["depth"] = bathymetry["depth"].where(~(bathymetry.depth <= self.minimum_depth),self.minimum_depth + 0.1) bathymetry.expand_dims({"ntiles": 1}).to_netcdf( self.mom_input_dir / "bathymetry.nc", From 60c59b706e4ceb75b7a4a3a9c80135b17ff39027 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 9 Oct 2024 14:31:34 -0600 Subject: [PATCH 72/81] update tests --- regional_mom6/regional_mom6.py | 8 ++++---- tests/test_expt_class.py | 20 ++++++++++---------- tests/test_manish_branch.py | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 5c7ac60e..d2601d7a 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -2171,7 +2171,7 @@ def setup_run_directory( surface_forcing=None, using_payu=False, overwrite=False, - with_tides_rectangular=False, + with_tides=False, boundaries=["south", "north", "west", "east"], ): """ @@ -2234,7 +2234,7 @@ def setup_run_directory( overwrite_run_dir = False # Check if we can implement tides - if with_tides_rectangular: + if with_tides: tidal_files_exist = any( "tidal" in filename for filename in ( @@ -2407,7 +2407,7 @@ def setup_run_directory( MOM_override_dict[key_DATA][ "value" ] = f'"U=file:forcing_obc_segment_00{file_num_obc}.nc(u),V=file:forcing_obc_segment_00{file_num_obc}.nc(v),SSH=file:forcing_obc_segment_00{file_num_obc}.nc(eta),TEMP=file:forcing_obc_segment_00{file_num_obc}.nc(temp),SALT=file:forcing_obc_segment_00{file_num_obc}.nc(salt)' - if with_tides_rectangular: + if with_tides: MOM_override_dict[key_DATA]["value"] = ( MOM_override_dict[key_DATA]["value"] + f',Uamp=file:tu_segment_00{file_num_obc}.nc(uamp),Uphase=file:tu_segment_00{file_num_obc}.nc(uphase),Vamp=file:tu_segment_00{file_num_obc}.nc(vamp),Vphase=file:tu_segment_00{file_num_obc}.nc(vphase),SSHamp=file:tz_segment_00{file_num_obc}.nc(zamp),SSHphase=file:tz_segment_00{file_num_obc}.nc(zphase)"' @@ -2418,7 +2418,7 @@ def setup_run_directory( ) # Tides OBC adjustments - if with_tides_rectangular: + if with_tides: # Include internal tide forcing MOM_override_dict["TIDES"]["value"] = "True" diff --git a/tests/test_expt_class.py b/tests/test_expt_class.py index 62f6038f..2aa2465a 100644 --- a/tests/test_expt_class.py +++ b/tests/test_expt_class.py @@ -20,7 +20,7 @@ "mom_run_dir", "mom_input_dir", "toolpath_dir", - "grid_type", + "hgrid_type", ), [ ( @@ -49,7 +49,7 @@ def test_setup_bathymetry( mom_run_dir, mom_input_dir, toolpath_dir, - grid_type, + hgrid_type, tmp_path, ): expt = experiment( @@ -63,7 +63,7 @@ def test_setup_bathymetry( mom_run_dir=mom_run_dir, mom_input_dir=mom_input_dir, toolpath_dir=toolpath_dir, - grid_type=grid_type, + hgrid_type=hgrid_type, ) ## Generate a bathymetry to use in tests @@ -169,7 +169,7 @@ def generate_silly_coords( mom_run_dir = "rundir/" mom_input_dir = "inputdir/" toolpath_dir = "toolpath" -grid_type = "even_spacing" +hgrid_type = "even_spacing" nx, ny = number_of_gridpoints(longitude_extent, latitude_extent, resolution) @@ -201,7 +201,7 @@ def generate_silly_coords( "mom_run_dir", "mom_input_dir", "toolpath_dir", - "grid_type", + "hgrid_type", ), [ ( @@ -230,7 +230,7 @@ def test_ocean_forcing( mom_run_dir, mom_input_dir, toolpath_dir, - grid_type, + hgrid_type, temp_dataarray_initial_condition, tmp_path, ): @@ -258,7 +258,7 @@ def test_ocean_forcing( mom_run_dir=mom_run_dir, mom_input_dir=mom_input_dir, toolpath_dir=toolpath_dir, - grid_type=grid_type, + hgrid_type=hgrid_type, ) ## Generate some initial condition to test on @@ -335,7 +335,7 @@ def test_ocean_forcing( "mom_run_dir", "mom_input_dir", "toolpath_dir", - "grid_type", + "hgrid_type", ), [ ( @@ -364,7 +364,7 @@ def test_rectangular_boundaries( mom_run_dir, mom_input_dir, toolpath_dir, - grid_type, + hgrid_type, tmp_path, ): @@ -455,7 +455,7 @@ def test_rectangular_boundaries( mom_run_dir=mom_run_dir, mom_input_dir=mom_input_dir, toolpath_dir=toolpath_dir, - grid_type=grid_type, + hgrid_type=hgrid_type, ) varnames = { diff --git a/tests/test_manish_branch.py b/tests/test_manish_branch.py index e8d4e302..aa7ecb8f 100644 --- a/tests/test_manish_branch.py +++ b/tests/test_manish_branch.py @@ -243,7 +243,7 @@ def test_tides(self, dummy_tidal_data): self.expt.longitude_extent = (-5, 5) self.expt.latitude_extent = (0, 30) # Grid Type - self.expt.grid_type = "even_spacing" + self.expt.hgrid_type = "even_spacing" # Dates self.expt.date_range = ("2000-01-01", "2000-01-02") self.expt.segments = [] From dee8457f958c0629a244017f3b0cabe512a29017 Mon Sep 17 00:00:00 2001 From: ashjbarnes Date: Wed, 9 Oct 2024 14:38:57 -0600 Subject: [PATCH 73/81] typo --- regional_mom6/regional_mom6.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d2601d7a..2d2cf2f1 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1058,7 +1058,7 @@ def write_config_file(self, path=None, export=True, quiet=False): "number_vertical_layers": self.number_vertical_layers, "layer_thickness_ratio": self.layer_thickness_ratio, "depth": self.depth, - "grid_type": self.hgrid_type, + "hgrid_type": self.hgrid_type, "repeat_year_forcing": self.repeat_year_forcing, "ocean_mask": self.ocean_mask, "layout": self.layout, From 3f86a14d60acda441cda6a43eb40bd9ca530e808 Mon Sep 17 00:00:00 2001 From: Manish Venumuddula <80477243+manishvenu@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:56:40 -0600 Subject: [PATCH 74/81] Update regional_mom6/regional_mom6.py --- regional_mom6/regional_mom6.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 2d2cf2f1..8134e149 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1339,7 +1339,7 @@ def setup_initial_condition( # tracers_out = tracers_out.assign_coords( # {"nx":np.arange(tracers_out.sizes["nx"]).astype(float), # "ny":np.arange(tracers_out.sizes["ny"]).astype(float)}) - +# Add dummy values for the nx and ny dimensions. Otherwise MOM6 complains that it's missing data?? tracers_out = tracers_out.assign_coords( { "nx": np.arange(tracers_out.sizes["nx"]).astype(float), From ae5857336e61ece516a74f8908fb649eaacefd9a Mon Sep 17 00:00:00 2001 From: manishvenu Date: Wed, 9 Oct 2024 15:08:27 -0600 Subject: [PATCH 75/81] Black Format --- regional_mom6/regional_mom6.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 8134e149..b8880a48 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1339,7 +1339,7 @@ def setup_initial_condition( # tracers_out = tracers_out.assign_coords( # {"nx":np.arange(tracers_out.sizes["nx"]).astype(float), # "ny":np.arange(tracers_out.sizes["ny"]).astype(float)}) -# Add dummy values for the nx and ny dimensions. Otherwise MOM6 complains that it's missing data?? + # Add dummy values for the nx and ny dimensions. Otherwise MOM6 complains that it's missing data?? tracers_out = tracers_out.assign_coords( { "nx": np.arange(tracers_out.sizes["nx"]).astype(float), From f562b1f2612f1044fab437cb4ae019cf15eec275 Mon Sep 17 00:00:00 2001 From: Ashley Barnes <53282288+ashjbarnes@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:17:33 -0600 Subject: [PATCH 76/81] Update regional_mom6.py Update comment to reflect name change --- regional_mom6/regional_mom6.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index b8880a48..237a9b09 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -1892,7 +1892,7 @@ def setup_bathymetry( + "Automatic regridding may fail if your domain is too big! If this process hangs or crashes," + "open a terminal with appropriate computational and resources try calling ESMF " + f"directly in the input directory {self.mom_input_dir} via\n\n" - + "`mpirun -np NUMBER_OF_CPUS ESMF_Regrid -s bathymetry_original.nc -d bathymetry_unfinished.nc -m bilinear --src_var elevation --dst_var elevation --netcdf4 --src_regional --dst_regional`\n\n" + + "`mpirun -np NUMBER_OF_CPUS ESMF_Regrid -s bathymetry_original.nc -d bathymetry_unfinished.nc -m bilinear --src_var depth --dst_var depth --netcdf4 --src_regional --dst_regional`\n\n" + "For details see https://xesmf.readthedocs.io/en/latest/large_problems_on_HPC.html\n\n" + "Afterwards, run the 'expt.tidy_bathymetry' method to skip the expensive interpolation step, and finishing metadata, encoding and cleanup.\n\n\n" ) From cd987f590d9346ebcb232413ee78fd3e34b86c78 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Fri, 11 Oct 2024 14:08:34 -0600 Subject: [PATCH 77/81] Remove unused test --- tests/test_angled_grids.py | 77 -------------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 tests/test_angled_grids.py diff --git a/tests/test_angled_grids.py b/tests/test_angled_grids.py deleted file mode 100644 index 820ac181..00000000 --- a/tests/test_angled_grids.py +++ /dev/null @@ -1,77 +0,0 @@ -import regional_mom6 as rmom6 -import os -from pathlib import Path -import pytest - -IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" - - -@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") -def test_angled_grids(): - """ - Test that the angled grid is correctly read in. - """ - expt_name = "nwa12_read_grids" - - latitude_extent = [16.0, 27] - longitude_extent = [192, 209] - - date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] - - ## Place where all your input files go - input_dir = Path( - os.path.join( - "/", - "glade", - "u", - "home", - "manishrv", - "documents", - "nwa12_0.1", - "regional_mom_workflows", - "rm6", - expt_name, - "inputs", - ) - ) - - ## Directory where you'll run the experiment from - run_dir = Path( - os.path.join( - "/", - "glade", - "u", - "home", - "manishrv", - "documents", - "nwa12_0.1", - "regional_mom_workflows", - "rm6", - expt_name, - "run_files", - ) - ) - for path in (run_dir, input_dir): - os.makedirs(str(path), exist_ok=True) - - ## User-1st, test if we can even read the angled nc files. - expt = rmom6.experiment( - longitude_extent=longitude_extent, - latitude_extent=latitude_extent, - date_range=date_range, - resolution=0.05, - number_vertical_layers=75, - layer_thickness_ratio=10, - depth=4500, - minimum_depth=5, - mom_run_dir=run_dir, - mom_input_dir=input_dir, - toolpath_dir="", - read_existing_grids=True, - ) - - ## Dev-2nd, test if the segment.coords function can properly give us the angles from this grid, which is at least called by rectangular_boundaries. - - ## User-2nd, test our ocean state boundary conditions - - ## User-3rd, test our tides boundary conditions From 5b1c816f548d8700d2db9b1e7d4eaecc2954c210 Mon Sep 17 00:00:00 2001 From: Manish Venumuddula <80477243+manishvenu@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:29:05 -0600 Subject: [PATCH 78/81] Minor Bug Fixes/Improvements to create_empty, change_MOM_param, & setup_run_directory (#20) * Package type changes for CRR * Black Formatting * Change MOM Param * Change MOM_param to Work * black * Remvoe premade_run_dir_arg * Remove init * Black * Update MOM_param_test * Clean --- regional_mom6/regional_mom6.py | 23 +++++++++++++++++------ tests/test_manish_branch.py | 7 ++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index 237a9b09..d93db5a2 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -25,6 +25,7 @@ import glob from collections import defaultdict import json +import copy warnings.filterwarnings("ignore") @@ -685,6 +686,7 @@ def create_empty( expt.longitude_extent = longitude_extent expt.ocean_mask = None expt.layout = None + self.segments = {} return expt def __init__( @@ -2416,7 +2418,13 @@ def setup_run_directory( MOM_override_dict[key_DATA]["value"] = ( MOM_override_dict[key_DATA]["value"] + '"' ) - + if type(self.date_range[0]) == str: + self.date_range[0] = dt.datetime.strptime( + self.date_range[0], "%Y-%m-%d %H:%M:%S" + ) + self.date_range[1] = dt.datetime.strptime( + self.date_range[1], "%Y-%m-%d %H:%M:%S" + ) # Tides OBC adjustments if with_tides: @@ -2520,7 +2528,7 @@ def change_MOM_parameter( if param_name in MOM_override_dict.keys(): original_val = MOM_override_dict[param_name]["value"] print( - "This parameter {} is begin replaced from {} to {} in MOM_override".format( + "This parameter {} is being replaced from {} to {} in MOM_override".format( param_name, original_val, param_value ) ) @@ -2558,9 +2566,9 @@ def read_MOM_file_as_dict(self, filename): lines = file.readlines() # Set the default initialization for a new key - MOM_file_dict = defaultdict(lambda: default_layout.copy()) + MOM_file_dict = defaultdict(lambda: copy.deepcopy(default_layout)) MOM_file_dict["filename"] = filename - dlc = default_layout.copy() + dlc = copy.deepcopy(default_layout) for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: split = lines[jj].split("=", 1) @@ -2579,10 +2587,10 @@ def read_MOM_file_as_dict(self, filename): dlc["value"] = str(value.strip()) dlc["comment"] = None - MOM_file_dict[var.strip()] = dlc.copy() + MOM_file_dict[var.strip()] = copy.deepcopy(dlc) # Save a copy of the original dictionary - MOM_file_dict["original"] = MOM_file_dict.copy() + MOM_file_dict["original"] = copy.deepcopy(MOM_file_dict) return MOM_file_dict def write_MOM_file(self, MOM_file_dict): @@ -2596,6 +2604,9 @@ def write_MOM_file(self, MOM_file_dict): for jj in range(len(lines)): if "=" in lines[jj] and not "===" in lines[jj]: var = lines[jj].split("=", 1)[0].strip() + if "#override" in var: + var = var.replace("#override", "") + var = var.strip() if var in MOM_file_dict.keys() and ( str(MOM_file_dict[var]["value"]) ) != str(original_MOM_file_dict[var]["value"]): diff --git a/tests/test_manish_branch.py b/tests/test_manish_branch.py index aa7ecb8f..0c293e61 100644 --- a/tests/test_manish_branch.py +++ b/tests/test_manish_branch.py @@ -282,10 +282,11 @@ def test_change_MOM_parameter(self): shutil.copytree( base_run_dir / "common_files", self.expt.mom_run_dir, dirs_exist_ok=True ) - self.expt.change_MOM_parameter("OBC_SEGMENT_001", "adasd", "COOL COMMENT") + og = self.expt.change_MOM_parameter("MINIMUM_DEPTH", "adasd", "COOL COMMENT") MOM_override_dict = self.expt.read_MOM_file_as_dict("MOM_override") - assert MOM_override_dict["OBC_SEGMENT_001"]["value"] == "adasd" - assert MOM_override_dict["OBC_SEGMENT_001"]["comment"] == "COOL COMMENT\n" + assert MOM_override_dict["MINIMUM_DEPTH"]["value"] == "adasd" + assert MOM_override_dict["original"]["OBC_SEGMENT_001"]["value"] == og + assert MOM_override_dict["MINIMUM_DEPTH"]["comment"] == "COOL COMMENT\n" def test_properties_empty(self): """ From 2ee186d74e2fd3c38dbc2021cc8b7637bccad945 Mon Sep 17 00:00:00 2001 From: Ashley Barnes <53282288+ashjbarnes@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:44:34 -0600 Subject: [PATCH 79/81] allow __getattr__ to read in data arrays from disk (#23) * include DataReader class * within __getattr__ read data files * bug * update demo with plots * typo --- demos/reanalysis-forced.ipynb | 36 +++++++++++++++- regional_mom6/regional_mom6.py | 78 +++++++++++++++++++++++++++------- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index 35f7eabe..2fd34621 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -225,7 +225,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Check out your domain:" + "### Check out your domain!\n", + "\n", + "Calling `expt.bathymetry` returns an xarray dataset, which can be plotted as usual. If you haven't yet run setup_bathymetry, calling `expt.bathymetry` will return `None` and prompt you to do so!" ] }, { @@ -309,6 +311,38 @@ " )" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Check out your initial condition data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "expt.init_tracers.salt.isel(zl = 0).plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### You can plot your segment data too" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "expt.segment_001.u_segment_001.isel(time = 5).plot()" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d93db5a2..daccde3e 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,13 +14,8 @@ import os import importlib.resources import datetime -from .utils import ( - quadrilateral_areas, - ap2ep, - ep2ap, -) +from .utils import quadrilateral_areas, ap2ep, ep2ap import pandas as pd -import re from pathlib import Path import glob from collections import defaultdict @@ -699,7 +694,7 @@ def __init__( depth, mom_run_dir, mom_input_dir, - toolpath_dir, + toolpath_dir=None, longitude_extent=None, latitude_extent=None, hgrid_type="even_spacing", @@ -800,12 +795,66 @@ def __str__(self) -> str: return json.dumps(self.write_config_file(export=False, quiet=True), indent=4) def __getattr__(self, name): + + ## First, check whether the attribute is an input file + + if name == "bathymetry": + if (self.mom_input_dir / "bathymetry.nc").exists(): + return xr.open_dataset( + self.mom_input_dir / "bathymetry.nc", + decode_cf=False, + decode_times=False, + ) + else: + print( + f"bathymetry.nc file not found! Make sure you've successfully run the setup_bathmetry method, or copied your own bathymetry.nc file into {self.mom_input_dir}." + ) + return None + elif name == "init_velocities": + if (self.mom_input_dir / "init_vel.nc").exists(): + return xr.open_dataset( + self.mom_input_dir / "init_vel.nc", + decode_cf=False, + decode_times=False, + ) + else: + print( + f"init_vel.nc file not found! Make sure you've successfully run the setup_initial_condition method, or copied your own init_vel.nc file into {self.mom_input_dir}." + ) + return + + elif name == "init_tracers": + if (self.mom_input_dir / "init_tracers.nc").exists(): + return xr.open_dataset( + self.mom_input_dir / "init_tracers.nc", + decode_cf=False, + decode_times=False, + ) + else: + print( + f"init_tracers.nc file not found! Make sure you've successfully run the setup_initial_condition method, or copied your own init_tracers.nc file into {self.mom_input_dir}." + ) + return + + elif "segment" in name: + try: + xr.open_mfdataset( + str(self.mom_input_dir / f"*{name}*.nc"), + decode_times=False, + decode_cf=False, + ) + except: + print( + f"{name} files not found! Make sure you've successfully run the setup_ocean_state_boundaries method, or copied your own segment files file into {self.mom_input_dir}." + ) + return None + + ## If we get here, attribute wasn't found + available_methods = [ method for method in dir(self) if not method.startswith("__") ] - error_message = ( - f"{name} method not found. Available methods are: {available_methods}" - ) + error_message = f"{name} not found. Available methods and attributes are: {available_methods}" raise AttributeError(error_message) def _make_hgrid(self): @@ -1067,7 +1116,6 @@ def write_config_file(self, path=None, export=True, quiet=False): "minimum_depth": self.minimum_depth, "vgrid": str(vgrid_path), "hgrid": str(hgrid_path), - "bathymetry": self.bathymetry_property, "ocean_state": self.ocean_state_boundaries, "tides": self.tides_boundaries, "initial_conditions": self.initial_condition, @@ -1849,7 +1897,7 @@ def setup_bathymetry( tgrid = xr.Dataset( data_vars={ "depth": ( - ["nx", "ny"], + ["ny", "nx"], np.zeros( self.hgrid.x.isel( nxp=slice(1, None, 2), nyp=slice(1, None, 2) @@ -1859,13 +1907,13 @@ def setup_bathymetry( }, coords={ "lon": ( - ["nx", "ny"], + ["ny", "nx"], self.hgrid.x.isel( nxp=slice(1, None, 2), nyp=slice(1, None, 2) ).values, ), "lat": ( - ["nx", "ny"], + ["ny", "nx"], self.hgrid.y.isel( nxp=slice(1, None, 2), nyp=slice(1, None, 2) ).values, @@ -3268,7 +3316,7 @@ def regrid_velocity_tracers(self): ) segment_out[f"lat_{self.segment_name}"] = ( [f"ny_{self.segment_name}", f"nx_{self.segment_name}"], - self.coords.lon.expand_dims( + self.coords.lat.expand_dims( dim="blank", axis=self.coords.attrs["axis_to_expand"] - 2 ).data, ) From 61398c92f0549cc32c5f0e9c1df39611c79630e1 Mon Sep 17 00:00:00 2001 From: manishvenu Date: Mon, 14 Oct 2024 14:48:59 -0600 Subject: [PATCH 80/81] Remove the deletion of MOM_input params in change_MOM_param --- regional_mom6/regional_mom6.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d93db5a2..7d2fcc5e 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -2516,15 +2516,10 @@ def change_MOM_parameter( "If not deleting a parameter, you must specify a new value for it." ) - MOM_input_dict = self.read_MOM_file_as_dict("MOM_input") MOM_override_dict = self.read_MOM_file_as_dict("MOM_override") original_val = "No original val" if not delete: - # We don't want to keep any parameters in MOM_input that we change. We want to clearly list them in MOM_override. - if param_name in MOM_input_dict.keys(): - original_val = MOM_override_dict[param_name]["value"] - print("Removing original value {} from MOM_input".format(original_val)) - del MOM_input_dict[param_name] + if param_name in MOM_override_dict.keys(): original_val = MOM_override_dict[param_name]["value"] print( @@ -2546,7 +2541,6 @@ def change_MOM_parameter( param_name ) ) - self.write_MOM_file(MOM_input_dict) self.write_MOM_file(MOM_override_dict) return original_val From 7d7216e3df6e8c8da918aa4f75e8a81c97e64d1e Mon Sep 17 00:00:00 2001 From: Manish Venumuddula <80477243+manishvenu@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:42:03 -0600 Subject: [PATCH 81/81] Update Config for Computer Independence, fix change_MOM_param, and fix __getattr__ bug (#24) --- demos/reanalysis-forced.ipynb | 41 +++----- regional_mom6/regional_mom6.py | 171 ++++++++++++++------------------- tests/test_config.py | 130 +++++++++++++++++++++++++ tests/test_manish_branch.py | 22 ++--- 4 files changed, 221 insertions(+), 143 deletions(-) create mode 100644 tests/test_config.py diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index 2fd34621..a8195511 100644 --- a/demos/reanalysis-forced.ipynb +++ b/demos/reanalysis-forced.ipynb @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -106,7 +106,7 @@ "toolpath_dir = Path(\"PATH_TO_FRE_TOOLS\")\n", "\n", "## Path to where your raw ocean forcing files are stored\n", - "glorys_path = Path(\"PATH_TO_GLORYS_DATA\" )\n", + "glorys_path = Path(\"PATH_TO_GLORYS_DATA\")\n", "\n", "## if directories don't exist, create them\n", "for path in (run_dir, glorys_path, input_dir):\n", @@ -232,35 +232,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "tags": [ "nbval-ignore-output", "nbval-skip" ] }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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G5Lf5/I5VqtV+fjHPy7r1q2Tzs5OOf744+VTn/qU1FHMaq6d3w9/+EN561vfKqtWrZJUKiX//u//rn5v53xKpZJ88IMflKVLl0p3d7dcdtll8tJLL83iWewb+zrHSqUiH/3oR+X000+X7u5uWbVqlVxxxRXy8ssvq30c7ecYsIARBUR33313lMvlojvvvDN68skno2uuuSbq7u6OXnzxxSPdtf3Gm970pugrX/lK9Pjjj0cbN26M3vzmN0dr1qyJJiYmkja33npr1NPTE33rW9+KHnvsseid73xntHLlymhsbOwI9vzA8PDDD0fHHXdc9KpXvSq65pprkvVz/Rz37NkTrV27NvqDP/iD6KGHHoo2b94c3XvvvdGzzz6btJnL53jTTTdFS5Ysif7zP/8z2rx5c/Rv//Zv0aJFi6LbbrstaTPXzu973/tedMMNN0Tf+ta3IhGJvvOd76jf2zmfq666KjrmmGOi9evXR4888kj0ute9LjrjjDOiarU6y2fjxr7OcWRkJLr44oujb37zm9Evf/nL6Mc//nF07rnnRmeddZbax9F+jgELF8EgiqLonHPOia666iq17qSTToquv/76I9SjQ4fh4eFIRKL7778/iqIoqtfr0eDgYHTrrbcmbYrFYtTX1xf9wz/8w5Hq5gFhfHw8OuGEE6L169dHF154YWIQzYdz/OhHPxpdcMEF3t/n+jm++c1vjv7oj/5IrXv7298evfe9742iaO6f30xjoZ3zGRkZiXK5XHT33XcnbbZt2xal0+nonnvumbW+twuX0TcTDz/8cCQiyeRyrp1jwMLCgneZlctl2bBhg1xyySVq/SWXXCIPPvjgEerVocPo6KiIiAwMDIiIyObNm2VoaEidb6FQkAsvvHDOne9f/MVfyJvf/Ga5+OKL1fr5cI7f/e535eyzz5bf+73fk+XLl8uZZ54pd955Z/L7XD/HCy64QP7nf/5HnnnmGRER+fnPfy4PPPCA/NZv/ZaIzP3zm4l2zmfDhg1SqVRUm1WrVslpp502J89ZpDH+pFIp6e/vF5H5eY4B8wfzRqn6QLFr1y6p1WqyYsUKtX7FihUyNDR0hHp1aBBFkVx33XVywQUXyGmnnSYikpyT63xffPHFWe/jgeLuu++WRx55RH760582/TYfzvH555+XL33pS3LdddfJX//1X8vDDz8sV199tRQKBbniiivm/Dl+9KMfldHRUTnppJMkk8lIrVaTT3/60/Lud79bRObHPSTaOZ+hoSHJ5/OyePHipjZzcSwqFoty/fXXy3ve856kwOt8O8eA+YUFbxAZpFIp9XcURU3r5ho+8IEPyC9+8Qt54IEHmn6by+e7detWueaaa+T73//+PitBz+VzrNfrcvbZZ8vNN98sIiJnnnmmPPHEE/KlL31JrrjiiqTdXD3Hb37zm3LXXXfJN77xDTn11FNl48aNcu2118qqVavkyiuvTNrN1fPz4UDOZy6ec6VSkXe9611Sr9fl9ttvb9l+Lp5jwPzDgneZLV26VDKZTNPsZHh4uGk2N5fwwQ9+UL773e/KfffdJ8cee2yyfnBwUERkTp/vhg0bZHh4WM466yzJZrOSzWbl/vvvl7/7u7+TbDabnMdcPseVK1fKKaecotadfPLJsmXLFhGZ+/fxr/7qr+T666+Xd73rXXL66afL5ZdfLh/60IfklltuEZG5f34z0c75DA4OSrlclr1793rbzAVUKhV5xzveIZs3b5b169cn7JDI/DnHgPmJBW8Q5fN5Oeuss2T9+vVq/fr16+U1r3nNEerVgSOKIvnABz4g3/72t+V///d/Zd26der3devWyeDgoDrfcrks999//5w53ze84Q3y2GOPycaNG5N/Z599tvz+7/++bNy4UY4//vg5f46//uu/3iSX8Mwzz8jatWtFZO7fx6mpKUmn9fCTyWSStPu5fn4z0c75nHXWWZLL5VSb7du3y+OPPz5nztkYQ5s2bZJ7771XlixZon6fD+cYMI9xpKK5jyaYtPsvf/nL0ZNPPhlde+21UXd3d/TCCy8c6a7tN/78z/886uvri37wgx9E27dvT/5NTU0lbW699daor68v+va3vx099thj0bvf/e6jOp25HTDLLIrm/jk+/PDDUTabjT796U9HmzZtir7+9a9HXV1d0V133ZW0mcvneOWVV0bHHHNMknb/7W9/O1q6dGn0kY98JGkz185vfHw8evTRR6NHH300EpHo85//fPToo48mGVbtnM9VV10VHXvssdG9994bPfLII9HrX//6oyolfV/nWKlUossuuyw69thjo40bN6rxp1QqJfs42s8xYOEiGEQxvvjFL0Zr166N8vl89Gu/9mtJmvpcg4g4/33lK19J2tTr9egTn/hENDg4GBUKheg3fuM3oscee+zIdfoQYKZBNB/O8T/+4z+i0047LSoUCtFJJ50U3XHHHer3uXyOY2Nj0TXXXBOtWbMm6ujoiI4//vjohhtuUB/OuXZ+9913n/Pdu/LKK6Moau98pqenow984APRwMBA1NnZGb3lLW+JtmzZcgTOxo19nePmzZu94899992X7ONoP8eAhYtUFEXR7PFRAQEBAQEBAQFHHxZ8DFFAQEBAQEBAQDCIAgICAgICAhY8gkEUEBAQEBAQsOARDKKAgICAgICABY9gEAUEBAQEBAQseASDKCAgICAgIGDBIxhEAQEBAQEBAQsewSAKCAgICAgIWPAIBlFAQEBAQEDAgkcwiAICAgICAgIWPIJBFBCwwHHRRRfJ1VdfLR/5yEdkYGBABgcH5cYbbxQRkR/84AeSz+flRz/6UdL+c5/7nCxdulS2b99+hHocEBAQcOgRDKKAgAD52te+Jt3d3fLQQw/JZz7zGfnUpz4l69evl4suukiuvfZaufzyy2V0dFR+/vOfyw033CB33nmnrFy58kh3OyAgIOCQIRR3DQhY4LjoooukVqspFuicc86R17/+9XLrrbdKuVyW8847T0444QR54okn5Pzzz5c777zzCPY4ICAg4NAje6Q7EBAQcOTxqle9Sv29cuVKGR4eFhGRfD4vd911l7zqVa+StWvXym233XYEehgQEBBweBFcZgEBAZLL5dTfqVRK6vV68veDDz4oIiJ79uyRPXv2zGrfAgICAmYDwSAKCAjYJ5577jn50Ic+JHfeeaecd955csUVVyhjKSAgIGA+IBhEAQEBXtRqNbn88svlkksukT/8wz+Ur3zlK/L444/L5z73uSPdtYCAgIBDimAQBQQEePHpT39aXnjhBbnjjjtERGRwcFD+6Z/+ST7+8Y/Lxo0bj2znAgICAg4hQpZZQEBAQEBAwIJHYIgCAgICAgICFjyCQRQQEBAQEBCw4BEMooCAgICAgIAFj2AQBQQEBAQEBCx4BIMoICAgICAgYMEjGEQBAQEBAQEBCx7BIAoICAgICAhY8AgGUUBAQEBAQMCCRzCIAgICAgICAhY8gkEUEBAQEBAQsOARDKKAgICAgICABY9gEAUEBAQEBAQsePz/LOUhC/oBD7oAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "expt.bathymetry.depth.plot()" ] @@ -324,7 +303,8 @@ "metadata": {}, "outputs": [], "source": [ - "expt.init_tracers.salt.isel(zl = 0).plot()" + "# Depends on Matplotlib\n", + "# expt.init_tracers.salt.isel(zl = 0).plot()" ] }, { @@ -340,7 +320,8 @@ "metadata": {}, "outputs": [], "source": [ - "expt.segment_001.u_segment_001.isel(time = 5).plot()" + "# Depends on Matplotlib\n", + "#expt.segment_001.u_segment_001.isel(time = 5).plot()" ] }, { @@ -448,7 +429,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "vroom_clean_env", "language": "python", "name": "python3" }, @@ -462,7 +443,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.12.6" } }, "nbformat": 4, diff --git a/regional_mom6/regional_mom6.py b/regional_mom6/regional_mom6.py index d69e58ec..9b8735e6 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -30,7 +30,7 @@ "generate_rectangular_hgrid", "experiment", "segment", - "load_experiment", + "create_experiment_from_config", ] @@ -104,7 +104,25 @@ def find_MOM6_rectangular_orientation(input): ## Load Experiment Function -def load_experiment(config_file_path): +def create_experiment_from_config( + config_file_path, + mom_input_folder=None, + mom_run_folder=None, + create_hgrid_and_vgrid=True, +): + """ + Load an experiment variables from a config file and generate hgrid/vgrid. + Computer specific functionality eliminates the ability to pass file paths. + Basically another way to initialize. Sets a default folder of "mom_input/from_config" and "mom_run/from_config" unless specified + + Args: + config_file_path (str): Path to the config file. + mom_input_folder (str): Path to the MOM6 input folder. Default is "mom_input/from_config". + mom_run_folder (str): Path to the MOM6 run folder. Default is "mom_run/from_config". + create_hgrid_and_vgrid (bool): Whether to create the hgrid and vgrid. Default is True. + Returns: + experiment: An experiment object with the fields from the config loaded in. + """ print("Reading from config file....") with open(config_file_path, "r") as f: config_dict = json.load(f) @@ -113,7 +131,7 @@ def load_experiment(config_file_path): expt = experiment.create_empty() print("Setting Default Variables.....") - expt.expt_name = config_dict["name"] + expt.expt_name = config_dict["expt_name"] try: expt.longitude_extent = tuple(config_dict["longitude_extent"]) expt.latitude_extent = tuple(config_dict["latitude_extent"]) @@ -122,13 +140,24 @@ def load_experiment(config_file_path): expt.latitude_extent = None try: expt.date_range = config_dict["date_range"] - expt.date_range[0] = dt.datetime.strptime(expt.date_range[0], "%Y-%m-%d") - expt.date_range[1] = dt.datetime.strptime(expt.date_range[1], "%Y-%m-%d") + expt.date_range[0] = dt.datetime.strptime( + expt.date_range[0], "%Y-%m-%d %H:%M:%S" + ) + expt.date_range[1] = dt.datetime.strptime( + expt.date_range[1], "%Y-%m-%d %H:%M:%S" + ) except: expt.date_range = None - expt.mom_run_dir = Path(config_dict["run_dir"]) - expt.mom_input_dir = Path(config_dict["input_dir"]) - expt.toolpath_dir = Path(config_dict["toolpath_dir"]) + + if mom_input_folder is None: + mom_input_folder = Path(os.path.join("mom_run", "from_config")) + if mom_run_folder is None: + mom_run_folder = Path(os.path.join("mom_input", "from_config")) + expt.mom_run_dir = Path(mom_run_folder) + expt.mom_input_dir = Path(mom_input_folder) + os.makedirs(expt.mom_run_dir, exist_ok=True) + os.makedirs(expt.mom_input_dir, exist_ok=True) + expt.resolution = config_dict["resolution"] expt.number_vertical_layers = config_dict["number_vertical_layers"] expt.layer_thickness_ratio = config_dict["layer_thickness_ratio"] @@ -140,66 +169,14 @@ def load_experiment(config_file_path): expt.minimum_depth = config_dict["minimum_depth"] expt.tidal_constituents = config_dict["tidal_constituents"] - print("Checking for hgrid and vgrid....") - if os.path.exists(config_dict["hgrid"]): - print("Found") - expt.hgrid = xr.open_dataset(config_dict["hgrid"]) - else: - print("Hgrid not found, call _make_hgrid when you're ready.") - expt.hgrid = None - if os.path.exists(config_dict["vgrid"]): - print("Found") - expt.vgrid = xr.open_dataset(config_dict["vgrid"]) - else: - print("Vgrid not found, call _make_vgrid when ready") - expt.vgrid = None - - print("Checking for bathymetry...") - if config_dict["bathymetry"] is not None and os.path.exists( - config_dict["bathymetry"] - ): - print("Found") - expt.bathymetry = xr.open_dataset(config_dict["bathymetry"]) + if create_hgrid_and_vgrid: + print("Creating hgrid and vgrid....") + expt.hgrid = expt._make_hgrid() + expt.vgrid = expt._make_vgrid() else: - print( - "Bathymetry not found. Please provide bathymetry, or call setup_bathymetry method to set up bathymetry." - ) - - print("Checking for ocean state files....") - found = True - for path in config_dict["ocean_state"]: - if not os.path.exists(path): - found = False - print( - "At least one ocean state file not found. Please provide ocean state files, or call setup_ocean_state_boundaries method to set up ocean state." - ) - break - if found: - print("Found") - found = True - print("Checking for initial condition files....") - for path in config_dict["initial_conditions"]: - if not os.path.exists(path): - found = False - print( - "At least one initial condition file not found. Please provide initial condition files, or call setup_initial_condition method to set up initial condition." - ) - break - if found: - print("Found") - found = True - print("Checking for tides files....") - for path in config_dict["tides"]: - if not os.path.exists(path): - found = False - print( - "At least one tides file not found. If you would like tides, call setup_tides_boundaries method to set up tides" - ) - break - if found: - print("Found") - found = True + print("Skipping hgrid and vgrid creation....") + print("Done!") return expt @@ -640,7 +617,7 @@ def create_empty( repeat_year_forcing=False, minimum_depth=4, tidal_constituents=["M2"], - name=None, + expt_name=None, ): """ Substitute init method to creates an empty expirement object, with the opportunity to override whatever values wanted. @@ -661,10 +638,10 @@ def create_empty( hgrid_type=None, repeat_year_forcing=None, tidal_constituents=None, - name=None, + expt_name=None, ) - expt.expt_name = name + expt.expt_name = expt_name expt.tidal_constituents = tidal_constituents expt.repeat_year_forcing = repeat_year_forcing expt.hgrid_type = hgrid_type @@ -703,7 +680,7 @@ def __init__( minimum_depth=4, tidal_constituents=["M2"], create_empty=False, - name=None, + expt_name=None, ): # Creates empty experiment object for testing and experienced user manipulation. @@ -715,7 +692,7 @@ def __init__( # ## Set up the experiment with no config file ## in case list was given, convert to tuples - self.expt_name = name + self.expt_name = expt_name self.date_range = tuple(date_range) self.mom_run_dir = Path(mom_run_dir) @@ -838,7 +815,7 @@ def __getattr__(self, name): elif "segment" in name: try: - xr.open_mfdataset( + return xr.open_mfdataset( str(self.mom_input_dir / f"*{name}*.nc"), decode_times=False, decode_cf=False, @@ -1077,34 +1054,30 @@ def bathymetry_property(self): def write_config_file(self, path=None, export=True, quiet=False): """ Write a configuration file for the experiment. This is a simple json file - that contains the expirment object information to allow for reproducibility, to pick up where a user left off, and - to make information about the expirement readable. + that contains the expirment varuavke information to allow for easy pass off to other users, with a strict computer independence restriction. + It also makes information about the expirement readable, and is good for just printing out information about the experiment. + + Args: + path (Optional[str]): Path to write the config file to. If not provided, the file is written to the ``mom_run_dir`` directory. + export (Optional[bool]): If ``True`` (default), the configuration file is written to disk on the given path + quiet (Optional[bool]): If ``True``, no print statements are made. + Returns: + Dict: A dictionary containing the configuration information. """ if not quiet: print("Writing Config File.....") - ## check if files exist - vgrid_path = None - hgrid_path = None - if os.path.exists(self.mom_input_dir / "vcoord.nc"): - vgrid_path = self.mom_input_dir / "vcoord.nc" - if os.path.exists(self.mom_input_dir / "hgrid.nc"): - hgrid_path = self.mom_input_dir / "hgrid.nc" - try: date_range = [ - self.date_range[0].strftime("%Y-%m-%d"), - self.date_range[1].strftime("%Y-%m-%d"), + self.date_range[0].strftime("%Y-%m-%d %H:%M:%S"), + self.date_range[1].strftime("%Y-%m-%d %H:%M:%S"), ] except: date_range = None config_dict = { - "name": self.expt_name, + "expt_name": self.expt_name, "date_range": date_range, "latitude_extent": self.latitude_extent, "longitude_extent": self.longitude_extent, - "run_dir": str(self.mom_run_dir), - "input_dir": str(self.mom_input_dir), - "toolpath_dir": str(self.toolpath_dir), "resolution": self.resolution, "number_vertical_layers": self.number_vertical_layers, "layer_thickness_ratio": self.layer_thickness_ratio, @@ -1114,11 +1087,6 @@ def write_config_file(self, path=None, export=True, quiet=False): "ocean_mask": self.ocean_mask, "layout": self.layout, "minimum_depth": self.minimum_depth, - "vgrid": str(vgrid_path), - "hgrid": str(hgrid_path), - "ocean_state": self.ocean_state_boundaries, - "tides": self.tides_boundaries, - "initial_conditions": self.initial_condition, "tidal_constituents": self.tidal_constituents, } if export: @@ -2656,10 +2624,19 @@ def write_MOM_file(self, MOM_file_dict): str(original_MOM_file_dict[var]["value"]), str(MOM_file_dict[var]["value"]), ) - lines[jj] = lines[jj].replace( - original_MOM_file_dict[var]["comment"], - str(MOM_file_dict[var]["comment"]), - ) + if original_MOM_file_dict[var]["comment"] != None: + lines[jj] = lines[jj].replace( + original_MOM_file_dict[var]["comment"], + str(MOM_file_dict[var]["comment"]), + ) + else: + lines[jj] = ( + lines[jj].replace("\n", "") + + " !" + + str(MOM_file_dict[var]["comment"]) + + "\n" + ) + print( "Changed", var, diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..6c2f35c9 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,130 @@ +import pytest +import regional_mom6 as rmom6 +from pathlib import Path +import os +import json +import shutil + + +def test_write_config(): + expt_name = "testing" + + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + + date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] + + ## Place where all your input files go + input_dir = Path( + os.path.join( + expt_name, + "inputs", + ) + ) + + ## Directory where you'll run the experiment from + run_dir = Path( + os.path.join( + expt_name, + "run_files", + ) + ) + data_path = Path("data") + for path in (run_dir, input_dir, data_path): + os.makedirs(str(path), exist_ok=True) + + ## User-1st, test if we can even read the angled nc files. + expt = rmom6.experiment( + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=0.05, + number_vertical_layers=75, + layer_thickness_ratio=10, + depth=4500, + minimum_depth=25, + mom_run_dir=run_dir, + mom_input_dir=input_dir, + toolpath_dir="", + expt_name="test", + ) + config_dict = expt.write_config_file() + assert config_dict["longitude_extent"] == tuple(longitude_extent) + assert config_dict["latitude_extent"] == tuple(latitude_extent) + assert config_dict["date_range"] == date_range + assert config_dict["resolution"] == 0.05 + assert config_dict["number_vertical_layers"] == 75 + assert config_dict["layer_thickness_ratio"] == 10 + assert config_dict["depth"] == 4500 + assert config_dict["minimum_depth"] == 25 + assert config_dict["expt_name"] == "test" + assert config_dict["hgrid_type"] == "even_spacing" + assert config_dict["repeat_year_forcing"] == False + assert config_dict["tidal_constituents"] == ["M2"] + assert config_dict["expt_name"] == "test" + shutil.rmtree(run_dir) + shutil.rmtree(input_dir) + shutil.rmtree(data_path) + + +def test_load_config(): + + expt_name = "testing" + + latitude_extent = [16.0, 27] + longitude_extent = [192, 209] + + date_range = ["2005-01-01 00:00:00", "2005-02-01 00:00:00"] + + ## Place where all your input files go + input_dir = Path( + os.path.join( + expt_name, + "inputs", + ) + ) + + ## Directory where you'll run the experiment from + run_dir = Path( + os.path.join( + expt_name, + "run_files", + ) + ) + data_path = Path("data") + for path in (run_dir, input_dir, data_path): + os.makedirs(str(path), exist_ok=True) + + ## User-1st, test if we can even read the angled nc files. + expt = rmom6.experiment( + longitude_extent=longitude_extent, + latitude_extent=latitude_extent, + date_range=date_range, + resolution=0.05, + number_vertical_layers=75, + layer_thickness_ratio=10, + depth=4500, + minimum_depth=25, + mom_run_dir=run_dir, + mom_input_dir=input_dir, + toolpath_dir="", + ) + path = "testing_config.json" + config_expt = expt.write_config_file(path) + new_expt = rmom6.create_experiment_from_config(os.path.join(path)) + assert str(new_expt) == str(expt) + print(new_expt.vgrid) + print(expt.vgrid) + assert new_expt.hgrid == expt.hgrid + assert (new_expt.vgrid.zi == expt.vgrid.zi).all() & ( + new_expt.vgrid.zl == expt.vgrid.zl + ).all() + assert os.path.exists(new_expt.mom_run_dir) & os.path.exists(new_expt.mom_input_dir) + assert os.path.exists(new_expt.mom_input_dir / "hgrid.nc") & os.path.exists( + new_expt.mom_input_dir / "vcoord.nc" + ) + shutil.rmtree(run_dir) + shutil.rmtree(input_dir) + shutil.rmtree(data_path) + shutil.rmtree(new_expt.mom_run_dir) + shutil.rmtree(new_expt.mom_input_dir) diff --git a/tests/test_manish_branch.py b/tests/test_manish_branch.py index 0c293e61..63cc6de9 100644 --- a/tests/test_manish_branch.py +++ b/tests/test_manish_branch.py @@ -162,7 +162,7 @@ def setup_class(self): # tmp_path is a pytest fixture self.dump_files_dir = Path("testing_outputs") os.makedirs(self.dump_files_dir, exist_ok=True) self.expt = rmom6.experiment.create_empty( - name=expt_name, + expt_name=expt_name, mom_input_dir=self.dump_files_dir, mom_run_dir=self.dump_files_dir, ) @@ -255,17 +255,6 @@ def test_tides(self, dummy_tidal_data): self.expt.setup_boundary_tides(self.dump_files_dir, "fake_tidal_data.nc") - def test_read_write_config(self): - """ - Test the read and write config functions - """ - # Write the config - self.expt.write_config_file(path=self.dump_files_dir / "config.yaml") - # Read the config - expt = rmom6.load_experiment(self.dump_files_dir / "config.yaml") - # Check if the config is the same - assert str(self.expt) == str(expt) - def test_change_MOM_parameter(self): """ Test the change MOM parameter function, as well as read_MOM_file and write_MOM_file under the hood. @@ -282,11 +271,12 @@ def test_change_MOM_parameter(self): shutil.copytree( base_run_dir / "common_files", self.expt.mom_run_dir, dirs_exist_ok=True ) - og = self.expt.change_MOM_parameter("MINIMUM_DEPTH", "adasd", "COOL COMMENT") MOM_override_dict = self.expt.read_MOM_file_as_dict("MOM_override") - assert MOM_override_dict["MINIMUM_DEPTH"]["value"] == "adasd" - assert MOM_override_dict["original"]["OBC_SEGMENT_001"]["value"] == og - assert MOM_override_dict["MINIMUM_DEPTH"]["comment"] == "COOL COMMENT\n" + og = self.expt.change_MOM_parameter("DT", "30", "COOL COMMENT") + MOM_override_dict_new = self.expt.read_MOM_file_as_dict("MOM_override") + assert MOM_override_dict_new["DT"]["value"] == "30" + assert MOM_override_dict["DT"]["value"] == og + assert MOM_override_dict_new["DT"]["comment"] == "COOL COMMENT\n" def test_properties_empty(self): """