diff --git a/demos/premade_run_directories/common_files/MOM_input b/demos/premade_run_directories/common_files/MOM_input index 8b4ccc70..d259b265 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. @@ -232,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. @@ -247,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. @@ -262,17 +238,8 @@ 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. -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/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 diff --git a/demos/reanalysis-forced.ipynb b/demos/reanalysis-forced.ipynb index f66e8333..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", @@ -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", " )" ] }, @@ -225,40 +225,21 @@ "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!" ] }, { "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()" ] @@ -294,14 +275,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", @@ -309,6 +290,40 @@ " )" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Check out your initial condition data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Depends on Matplotlib\n", + "# 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": [ + "# Depends on Matplotlib\n", + "#expt.segment_001.u_segment_001.isel(time = 5).plot()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -324,7 +339,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" ] }, { @@ -414,7 +429,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "vroom_clean_env", "language": "python", "name": "python3" }, @@ -428,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 55d6120b..9b8735e6 100644 --- a/regional_mom6/regional_mom6.py +++ b/regional_mom6/regional_mom6.py @@ -14,20 +14,172 @@ import os import importlib.resources import datetime -from .utils import quadrilateral_areas - +from .utils import quadrilateral_areas, ap2ep, ep2ap +import pandas as pd +from pathlib import Path +import glob +from collections import defaultdict +import json +import copy warnings.filterwarnings("ignore") __all__ = [ "longitude_slicer", "hyperbolictan_thickness_profile", - "rectangular_hgrid", + "generate_rectangular_hgrid", "experiment", "segment", + "create_experiment_from_config", ] +## 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 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) + + print("Creating Empty Experiment Object....") + expt = experiment.create_empty() + + print("Setting Default Variables.....") + expt.expt_name = config_dict["expt_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 %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 + + 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"] + expt.depth = config_dict["depth"] + expt.hgrid_type = config_dict["hgrid_type"] + expt.repeat_year_forcing = config_dict["repeat_year_forcing"] + expt.ocean_mask = None + expt.layout = None + expt.minimum_depth = config_dict["minimum_depth"] + expt.tidal_constituents = config_dict["tidal_constituents"] + + if create_hgrid_and_vgrid: + print("Creating hgrid and vgrid....") + expt.hgrid = expt._make_hgrid() + expt.vgrid = expt._make_vgrid() + else: + print("Skipping hgrid and vgrid creation....") + + print("Done!") + return expt + + ## Auxiliary functions @@ -77,11 +229,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 +297,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 +322,14 @@ def get_glorys_data( path = Path(download_path) if modify_existing: - file = open(path / "get_glorysdata.sh", "r") + file = open(Path(path / "get_glorys_data.sh"), "r") lines = file.readlines() file.close() else: - lines = ["#!/bin/bash\ncopernicusmarine login"] + lines = ["#!/bin/bash\n"] - file = open(path / "get_glorysdata.sh", "w") + file = open(Path(path / "get_glorys_data.sh"), "w") lines.append( f""" @@ -308,10 +457,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 +470,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) @@ -434,7 +589,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. @@ -442,13 +597,73 @@ 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. """ + @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, + hgrid_type="even_spacing", + repeat_year_forcing=False, + minimum_depth=4, + tidal_constituents=["M2"], + expt_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, + hgrid_type=None, + repeat_year_forcing=None, + tidal_constituents=None, + expt_name=None, + ) + + expt.expt_name = expt_name + expt.tidal_constituents = tidal_constituents + expt.repeat_year_forcing = repeat_year_forcing + expt.hgrid_type = hgrid_type + expt.toolpath_dir = toolpath_dir + expt.mom_run_dir = mom_run_dir + expt.mom_input_dir = mom_input_dir + expt.minimum_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 + self.segments = {} + return expt + def __init__( self, *, - longitude_extent, - latitude_extent, date_range, resolution, number_vertical_layers, @@ -456,14 +671,28 @@ def __init__( depth, mom_run_dir, mom_input_dir, - toolpath_dir, - grid_type="even_spacing", + toolpath_dir=None, + longitude_extent=None, + latitude_extent=None, + 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, + expt_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.longitude_extent = tuple(longitude_extent) - self.latitude_extent = tuple(latitude_extent) + self.expt_name = expt_name self.date_range = tuple(date_range) self.mom_run_dir = Path(mom_run_dir) @@ -481,23 +710,53 @@ 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. - if read_existing_grids: + self.minimum_depth = minimum_depth # Minimum depth allowed in bathy file + self.tidal_constituents = tidal_constituents + + 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()), + ) + self.latitude_extent = ( + 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) + # create additional directories and links (self.mom_input_dir / "weights").mkdir(exist_ok=True) (self.mom_input_dir / "forcing").mkdir(exist_ok=True) @@ -509,13 +768,70 @@ 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): + + ## 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: + return 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): @@ -525,14 +841,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: @@ -545,10 +861,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( @@ -558,7 +874,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 @@ -579,11 +895,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 @@ -607,6 +923,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"} @@ -614,7 +939,172 @@ 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 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.....") + try: + date_range = [ + 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 = { + "expt_name": self.expt_name, + "date_range": date_range, + "latitude_extent": self.latitude_extent, + "longitude_extent": self.longitude_extent, + "resolution": self.resolution, + "number_vertical_layers": self.number_vertical_layers, + "layer_thickness_ratio": self.layer_thickness_ratio, + "depth": self.depth, + "hgrid_type": self.hgrid_type, + "repeat_year_forcing": self.repeat_year_forcing, + "ocean_mask": self.ocean_mask, + "layout": self.layout, + "minimum_depth": self.minimum_depth, + "tidal_constituents": self.tidal_constituents, + } + if export: + if path is not None: + export_path = path + else: + export_path = self.mom_run_dir / "rmom6_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, @@ -626,7 +1116,7 @@ def initial_condition( 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. @@ -638,7 +1128,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: @@ -770,17 +1260,11 @@ def 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. @@ -859,17 +1343,35 @@ def 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"}) + 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)}) + # 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), + "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.") @@ -909,7 +1411,7 @@ def 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"]}, @@ -918,22 +1420,22 @@ def 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}, - "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}, }, ) 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}, - "yh": {"_FillValue": None}, + # "xh": {"_FillValue": None}, + # "yh": {"_FillValue": None}, "eta_t": {"_FillValue": None}, }, ) @@ -950,7 +1452,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. @@ -960,60 +1462,85 @@ 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( - 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, @@ -1028,6 +1555,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"]: @@ -1044,18 +1572,34 @@ 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_single_boundary( + Path( + os.path.join( + (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_single_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. @@ -1074,6 +1618,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="") @@ -1081,6 +1626,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 @@ -1093,21 +1640,128 @@ def 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_boundary_tides( + 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, *, 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, - minimum_layers=3, positive_down=False, - chunks="auto", ): """ Cut out and interpolate the chosen bathymetry and then fill inland lakes. @@ -1129,15 +1783,8 @@ 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., - ``{"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 @@ -1145,16 +1792,11 @@ def setup_bathymetry( coordinate_names = { "xh": longitude_coordinate_name, "yh": latitude_coordinate_name, - "elevation": vertical_coordinate_name, + "depth": 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)[ - coordinate_names["elevation"] + bathymetry = xr.open_dataset(bathymetry_path, chunks="auto")[ + coordinate_names["depth"] ] bathymetry = bathymetry.sel( @@ -1201,7 +1843,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( @@ -1209,35 +1851,21 @@ 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"] = ( - "Elevation relative to sea level" - ) - bathymetry_output.elevation.attrs["coordinates"] = "lon lat" + 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" ) - 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": ( - ["lat", "lon"], + "depth": ( + ["ny", "nx"], np.zeros( self.hgrid.x.isel( nxp=slice(1, None, 2), nyp=slice(1, None, 2) @@ -1247,69 +1875,60 @@ def setup_bathymetry( }, coords={ "lon": ( - ["lon"], - self.hgrid.x.isel(nxp=slice(1, None, 2), nyp=1).values, + ["ny", "nx"], + 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, + ["ny", "nx"], + self.hgrid.y.isel( + nxp=slice(1, None, 2), nyp=slice(1, None, 2) + ).values, ), }, ) # 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" 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" ) 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 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 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, 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, minimum_layers, positive_down) + self.tidy_bathymetry(fill_channels, positive_down) + print("setup bathymetry has finished successfully.") + return def tidy_bathymetry( - self, fill_channels=False, minimum_layers=3, positive_down=True + 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 @@ -1325,23 +1944,22 @@ 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. + 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" ) ## 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" @@ -1353,15 +1971,13 @@ def tidy_bathymetry( ## Ensure that coordinate is positive down! bathymetry["depth"] *= -1 - ## REMOVE INLAND LAKES - - min_depth = self.vgrid.zi[minimum_layers] - - ocean_mask = bathymetry.copy(deep=True).depth.where( - bathymetry.depth <= min_depth, 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 @@ -1494,8 +2110,11 @@ def tidy_bathymetry( 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"] > 0, np.nan) bathymetry["depth"] = bathymetry["depth"].where( - bathymetry["depth"] != 0, np.nan + ~(bathymetry.depth <= self.minimum_depth), self.minimum_depth + 0.1 ) bathymetry.expand_dims({"ntiles": 1}).to_netcdf( @@ -1505,9 +2124,9 @@ def tidy_bathymetry( ) print("done.") - self.bathymetry = bathymetry + return - 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. """ @@ -1545,9 +2164,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. @@ -1570,6 +2189,8 @@ def setup_run_directory( surface_forcing=None, using_payu=False, overwrite=False, + with_tides=False, + boundaries=["south", "north", "west", "east"], ): """ Set up the run directory for MOM6. Either copy a pre-made set of files, or modify @@ -1588,7 +2209,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) @@ -1598,23 +2221,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( @@ -1624,6 +2251,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: + 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. @@ -1632,7 +2273,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 ( @@ -1645,7 +2286,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" @@ -1678,52 +2319,163 @@ 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: - 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( - 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] + if layout == None: + print( + "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( + 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." + ) - print("Number of CPUs required: ", ncpus) + ncpus = layout[0] * layout[1] + 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() - 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]: - 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.minimum_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_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: + 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)"' + ) + else: + 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: - if "NJGLOBAL" in lines[jj]: - lines[jj] = f"NJGLOBAL = {self.hgrid.ny.shape[0]//2}\n" + # Include internal tide forcing + MOM_override_dict["TIDES"]["value"] = "True" - with open(self.mom_run_dir / "MOM_layout", "w") as f: - f.writelines(lines) + # 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"): 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() @@ -1754,6 +2506,192 @@ 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_override_dict = self.read_MOM_file_as_dict("MOM_override") + original_val = "No original val" + if not delete: + + if param_name in MOM_override_dict.keys(): + original_val = MOM_override_dict[param_name]["value"] + print( + "This parameter {} is being 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_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: copy.deepcopy(default_layout)) + MOM_file_dict["filename"] = filename + 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) + 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()] = copy.deepcopy(dlc) + + # Save a copy of the original dictionary + MOM_file_dict["original"] = copy.deepcopy(MOM_file_dict) + 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 "#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"]): + lines[jj] = lines[jj].replace( + str(original_MOM_file_dict[var]["value"]), + str(MOM_file_dict[var]["value"]), + ) + 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, + "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): """ @@ -1778,7 +2716,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 @@ -1828,7 +2766,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"}}, ) @@ -1843,7 +2781,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"}}, ) @@ -1853,7 +2791,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"}}, ) @@ -1861,8 +2799,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 @@ -1892,11 +2830,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. """ @@ -1913,13 +2846,17 @@ def __init__( startdate, arakawa_grid="A", time_units="days", - tidal_constituents=None, repeat_year_forcing=False, ): ## Store coordinate names - if arakawa_grid == "A": - self.x = varnames["x"] - self.y = varnames["y"] + if arakawa_grid == "A" and infile is not None: + 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"] @@ -1928,15 +2865,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 @@ -1955,53 +2894,123 @@ 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): """ - 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" + + 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) + + + Code adapted 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": - self.hgrid_seg = self.hgrid.isel(nyp=[0]) - self.perpendicular = "ny" - self.parallel = "nx" + 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": 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 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( + { + "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": 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( + { + "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": 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( + { + "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": 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}" - if self.orientation == "east": - self.hgrid_seg = self.hgrid.isel(nxp=[-1]) - self.perpendicular = "nx" - self.parallel = "ny" + # Make lat and lon coordinates + rcoord = rcoord.assign_coords(lat=rcoord["lat"], lon=rcoord["lon"]) - if self.orientation == "west": - self.hgrid_seg = self.hgrid.isel(nxp=[0]) - self.perpendicular = "nx" - self.parallel = "ny" + return rcoord - ## 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 + def rotate(self, u, v): + # Make docstring - ## 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"]) + """ + 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. + """ + + 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") @@ -2010,7 +3019,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, @@ -2033,7 +3042,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, @@ -2043,7 +3052,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, @@ -2051,13 +3060,17 @@ def rectangular_brushcut(self): / f"weights/bilinear_tracer_weights_{self.orientation}.nc", ) + 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( [ - regridder_velocity( - rawseg[[self.u, self.v]].rename( - {self.xq: "lon", self.yq: "lat"} - ) - ), + velocities_out, regridder_tracer( rawseg[ [self.eta] + [self.tracers[i] for i in self.tracers] @@ -2070,7 +3083,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, @@ -2080,7 +3093,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, @@ -2090,7 +3103,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, @@ -2124,9 +3137,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( @@ -2190,7 +3203,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.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 @@ -2235,42 +3249,307 @@ 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.lat.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 + # 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": " "}) 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", ) 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=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 + + # 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", 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", self.coords.attrs["locations_name"] + ) + + 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=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 + 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", self.coords.attrs["locations_name"]), + up, + ) # radians + ds_ap[f"vphase_{self.segment_name}"] = ( + ("constituent", self.coords.attrs["locations_name"]), + 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", 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) + + 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/[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( + {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}"} + ) + + ## 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_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_expt_class.py b/tests/test_expt_class.py index d0713c2f..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 @@ -93,8 +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}, ) bathymetry_file.unlink() @@ -171,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) @@ -203,7 +201,7 @@ def generate_silly_coords( "mom_run_dir", "mom_input_dir", "toolpath_dir", - "grid_type", + "hgrid_type", ), [ ( @@ -232,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, ): @@ -260,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 @@ -312,7 +310,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", @@ -337,7 +335,7 @@ def test_ocean_forcing( "mom_run_dir", "mom_input_dir", "toolpath_dir", - "grid_type", + "hgrid_type", ), [ ( @@ -366,7 +364,7 @@ def test_rectangular_boundaries( mom_run_dir, mom_input_dir, toolpath_dir, - grid_type, + hgrid_type, tmp_path, ): @@ -457,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 = { @@ -471,4 +469,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..63cc6de9 --- /dev/null +++ b/tests/test_manish_branch.py @@ -0,0 +1,290 @@ +""" +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( + expt_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.hgrid_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_boundary_tides(self.dump_files_dir, "fake_tidal_data.nc") + + 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 + ) + MOM_override_dict = self.expt.read_MOM_file_as_dict("MOM_override") + 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): + """ + 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)