diff --git a/amescap/Script_utils.py b/amescap/Script_utils.py index 8419736..166b0eb 100644 --- a/amescap/Script_utils.py +++ b/amescap/Script_utils.py @@ -42,23 +42,17 @@ Green = "\033[92m" Purple = "\033[95m" -# Old definitions for colored text, delete after all autodocs are merged -def prRed(skk): print("\033[91m{}\033[00m".format(skk)) -def prGreen(skk): print("\033[92m{}\033[00m".format(skk)) -def prCyan(skk): print("\033[96m{}\033[00m".format(skk)) -def prYellow(skk): print("\033[93m{}\033[00m".format(skk)) -def prPurple(skk): print("\033[95m{}\033[00m".format(skk)) -def prLightPurple(skk): print("\033[94m{}\033[00m".format(skk)) - - def MY_func(Ls_cont): """ - Returns the Mars Year + Returns the Mars Year. :param Ls_cont: solar longitude (continuous) - :type Ls_cont: array + :type Ls_cont: array + :return: the Mars year - :rtype: int + :rtype: int + + :raises ValueError: if Ls_cont is not in the range [0, 360) """ return (Ls_cont)//(360.) + 1 @@ -66,14 +60,16 @@ def MY_func(Ls_cont): def find_tod_in_diurn(fNcdf): """ - Returns the variable for the local time axis in diurn files - (e.g., time_of_day_24). - Original implementation by Victoria H. + Returns the variable for the local time axis in diurn files. + + (e.g., time_of_day_24). Original implementation by Victoria H. :param fNcdf: a netCDF file - :type fNcdf: netCDF file object + :type fNcdf: netCDF file object + :return: the name of the time of day dimension - :rtype: str + :rtype: str + :raises ValueError: if the time of day variable is not found """ regex = re.compile("time_of_day.") @@ -84,11 +80,13 @@ def find_tod_in_diurn(fNcdf): def print_fileContent(fileNcdf): """ - Prints the contents of a netCDF file to the screen. Variables sorted - by dimension. + Prints the contents of a netCDF file to the screen. + + Variables sorted by dimension. :param fileNcdf: full path to the netCDF file - :type fileNcdf: str + :type fileNcdf: str + :return: None """ @@ -149,18 +147,25 @@ def print_fileContent(fileNcdf): def print_varContent(fileNcdf, list_varfull, print_stat=False): """ - Print variable contents from a variable in a netCDF file. Requires - a XXXXX.fixed.nc file in the current directory. + Print variable contents from a variable in a netCDF file. + + Requires a XXXXX.fixed.nc file in the current directory. :param fileNcdf: full path to a netcdf file - :type fileNcdf: str + :type fileNcdf: str :param list_varfull: list of variable names and optional slices (e.g., ``["lon", "ps[:, 10, 20]"]``) - :type list_varfull: list + :type list_varfull: list :param print_stat: If True, print min, mean, and max. If False, print values. Defaults to False - :type print_stat: bool, optional + :type print_stat: bool, optional + :return: None + + :raises ValueError: if the variable is not found in the file + :raises FileNotFoundError: if the file is not found + :raises Exception: if the variable is not found in the file + :raises Exception: if the file is not found """ if not os.path.isfile(fileNcdf.name): @@ -231,6 +236,14 @@ def print_varContent(fileNcdf, list_varfull, print_stat=False): def give_permission(filename): """ Sets group file permissions for the NAS system + + :param filename: full path to the netCDF file + :type filename: str + + :return: None + + :raises subprocess.CalledProcessError: if the setfacl command fails + :raises FileNotFoundError: if the file is not found """ try: @@ -249,11 +262,19 @@ def give_permission(filename): def check_file_tape(fileNcdf): """ - Checks whether a file exists on the disk. If on a NAS system, - also checks if the file needs to be migrated from tape. + Checks whether a file exists on the disk. + + If on a NAS system, also checks if the file needs to be migrated + from tape. :param fileNcdf: full path to a netcdf file or a file object with a name attribute - :type fileNcdf: str or file object + :type fileNcdf: str or file object + + :return: None + + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises subprocess.CalledProcessError: if the dmls command fails """ # Get the filename, whether passed as string or as file object @@ -321,15 +342,20 @@ def get_Ncdf_path(fNcdf): """ Returns the full path for a netCDF file object. - .. note:: - ``Dataset`` and multi-file dataset (``MFDataset``) have - different attributes for the path, hence the need for this - function. + ``Dataset`` and multi-file dataset (``MFDataset``) have different + attributes for the path, hence the need for this function. :param fNcdf: Dataset or MFDataset object - :type fNcdf: netCDF file object + :type fNcdf: netCDF file object + :return: string list for the Dataset (MFDataset) - :rtype: str(list) + :rtype: str(list) + + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist """ # Only MFDataset has the_files attribute @@ -342,16 +368,23 @@ def get_Ncdf_path(fNcdf): def extract_path_basename(filename): """ - Returns the path and basename of a file. If only the filename is - provided, assume it is in the current directory. + Returns the path and basename of a file. + + If only the filename is provided, assume it is in the current + directory. :param filename: name of the netCDF file (may include full path) - :type filename: str + :type filename: str :return: full file path & name of file + :rtype: tuple + :raises ValueError: if the filename is not a string + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the filename is not a string + :raises OSError: if the filename is not a valid path .. note:: - This routine does not confirm that the file exists. - It operates on the provided input string. + This routine does not confirm that the file exists. It operates + on the provided input string. """ # Get the filename without the path @@ -370,15 +403,23 @@ def extract_path_basename(filename): def FV3_file_type(fNcdf): """ - Return the type of the netCDF file (i.e., ``fixed``, ``diurn``, - ``average``, ``daily``) and the format of the Ls array ``areo`` - (i.e., ``fixed``, ``continuous``, or ``diurn``). + Return the type of the netCDF file. + + Returns netCDF file type (i.e., ``fixed``, ``diurn``, ``average``, + ``daily``) and the format of the Ls array ``areo`` (i.e., ``fixed``, + ``continuous``, or ``diurn``). :param fNcdf: an open Netcdf file - :type fNcdf: Netcdf file object + :type fNcdf: Netcdf file object :return: The Ls array type (string, ``fixed``, ``continuous``, or ``diurn``) and the netCDF file type (string ``fixed``, ``diurn``, ``average``, or ``daily``) + :rtype: tuple + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute """ # Get the full path from the file @@ -424,21 +465,24 @@ def FV3_file_type(fNcdf): def alt_FV3path(fullpaths, alt, test_exist=True): """ - Returns the original or fixed file given an interpolated daily, - diurn or average file. + Returns the original or fixed file for a given path. :param fullpaths: full path to a file or a list of full paths to more than one file - :type fullpaths: str + :type fullpaths: str :param alt: type of file to return (i.e., original or fixed) - :type alt: str + :type alt: str :param test_exist: Whether file exists on the disk, defaults to True - :type test_exist: bool, optional - :raises ValueError: _description_ - :return: path to original or fixed file - (e.g., "/u/path/00010.atmos_average.nc" or - "/u/path/00010.fixed.nc") - :rtype: str + :type test_exist: bool, optional + :return: path to original or fixed file (e.g., + /u/path/00010.atmos_average.nc or /u/path/00010.fixed.nc) + :rtype: str + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises OSError: if the file is not a valid path """ out_list = [] @@ -484,21 +528,32 @@ def alt_FV3path(fullpaths, alt, test_exist=True): def smart_reader(fNcdf, var_list, suppress_warning=False): """ + Reads a variable from a netCDF file. + + If the variable is not found in the file, it checks for the variable + in the original file (e.g., atmos_average.nc) or the fixed file + (e.g., 00010.fixed.nc). + Alternative to ``var = fNcdf.variables["var"][:]`` for handling - *processed* files that also checks for a matching average or daily - and XXXXX.fixed.nc file. + *processed* files. :param fNcdf: an open netCDF file - :type fNcdf: netCDF file object + :type fNcdf: netCDF file object :param var_list: a variable or list of variables (e.g., ``areo`` or [``pk``, ``bk``, ``areo``]) - :type var_list: _type_ + :type var_list: _type_ :param suppress_warning: suppress debug statement. Useful if a variable is not expected to be in the file anyway. Defaults to False - :type suppress_warning: bool, optional + :type suppress_warning: bool, optional :return: variable content (single or values to unpack) - :rtype: list + :rtype: list + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises OSError: if the file is not a valid path Example:: @@ -588,21 +643,28 @@ def smart_reader(fNcdf, var_list, suppress_warning=False): def regrid_Ncfile(VAR_Ncdf, file_Nc_in, file_Nc_target): """ Regrid a netCDF variable from one file structure to another. + Requires a file with the desired file structure to mimic. [Alex Kling, May 2021] :param VAR_Ncdf: a netCDF variable object to regrid (e.g., ``f_in.variable["temp"]``) - :type VAR_Ncdf: netCDF file variable + :type VAR_Ncdf: netCDF file variable :param file_Nc_in: an open netCDF file to source for the variable (e.g., ``f_in = Dataset("filename", "r")``) - :type file_Nc_in: netCDF file object + :type file_Nc_in: netCDF file object :param file_Nc_target: an open netCDF file with the desired file structure (e.g., ``f_out = Dataset("filename", "r")``) - :type file_Nc_target: netCDF file object + :type file_Nc_target: netCDF file object :return: the values of the variable interpolated to the target file grid. - :rtype: array + :rtype: array + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises OSError: if the file is not a valid path .. note:: While the KDTree interpolation can handle a 3D dataset @@ -741,9 +803,11 @@ def progress(k, Nmax): Displays a progress bar to monitor heavy calculations. :param k: current iteration of the outer loop - :type k: int + :type k: int :param Nmax: max iteration of the outer loop - :type Nmax: int + :type Nmax: int + :return: None + :raises ValueError: if k or Nmax are not integers, k > Nmax, or k < 0 """ # For rounding to the 2nd digit @@ -773,13 +837,17 @@ def progress(k, Nmax): def section_content_amescap_profile(section_ID): """ - Executes first code section in ``~/.amescap_profile`` to read in - user-defined plot & interpolation settings. + Executes first code section in ``~/.amescap_profile``. + + Reads in user-defined plot & interpolation settings. + [Alex Kling, Nov 2022] :param section_ID: the section to load (e.g., Pressure definitions for pstd) - :type section_ID: str + :type section_ID: str :return: the relevant line with Python syntax + :rtype: str + :raises FileNotFoundError: if the file is not found """ import os @@ -822,20 +890,29 @@ def section_content_amescap_profile(section_ID): def filter_vars(fNcdf, include_list=None, giveExclude=False): """ - Filters the variable names in a netCDF file for processing. Returns - all dimensions (``lon``, ``lat``, etc.), the ``areo`` variable, and - any other variable listed in ``include_list``. + Filters the variable names in a netCDF file for processing. + + Returns all dimensions (``lon``, ``lat``, etc.), the ``areo`` + variable, and any other variable listed in ``include_list``. :param fNcdf: an open netCDF object for a diurn, daily, or average file - :type fNcdf: netCDF file object + :type fNcdf: netCDF file object :param include_list:list of variables to include (e.g., [``ucomp``, ``vcomp``], defaults to None - :type include_list: list or None, optional + :type include_list: list or None, optional :param giveExclude: if True, returns variables to be excluded from the file, defaults to False - :type giveExclude: bool, optional + :type giveExclude: bool, optional :return: list of variable names to include in the processed file + :rtype: list + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises OSError: if the file is not a valid path + :raises KeyError: if the variable is not found in the file """ var_list = fNcdf.variables.keys() @@ -873,24 +950,30 @@ def filter_vars(fNcdf, include_list=None, giveExclude=False): def find_fixedfile(filename): """ - Finds the relevant fixed file for a given average, daily, or diurn - file. - [Batterson, Updated by Alex Nov 29 2022] + Finds the relevant fixed file for an average, daily, or diurn file. + + [Courtney Batterson, updated by Alex Nov 29 2022] :param filename: an average, daily, or diurn netCDF file - :type filename: str + :type filename: str :return: full path to the correspnding fixed file - :rtype: str + :rtype: str + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises OSError: if the file is not a valid path Compatible file types:: - DDDDD.atmos_average.nc -> DDDDD.fixed.nc - atmos_average.tileX.nc -> fixed.tileX.nc - DDDDD.atmos_average_plevs.nc -> DDDDD.fixed.nc - DDDDD.atmos_average_plevs_custom.nc -> DDDDD.fixed.nc - atmos_average.tileX_plevs.nc -> fixed.tileX.nc - atmos_average.tileX_plevs_custom.nc -> fixed.tileX.nc - atmos_average_custom.tileX_plevs.nc -> fixed.tileX.nc + DDDDD.atmos_average.nc -> DDDDD.fixed.nc + atmos_average.tileX.nc -> fixed.tileX.nc + DDDDD.atmos_average_plevs.nc -> DDDDD.fixed.nc + DDDDD.atmos_average_plevs_custom.nc -> DDDDD.fixed.nc + atmos_average.tileX_plevs.nc -> fixed.tileX.nc + atmos_average.tileX_plevs_custom.nc -> fixed.tileX.nc + atmos_average_custom.tileX_plevs.nc -> fixed.tileX.nc """ filepath, fname = extract_path_basename(filename) @@ -909,16 +992,24 @@ def find_fixedfile(filename): def get_longname_unit(fNcdf, varname): """ - Returns the longname and unit attributes of a variable in a netCDF - file. If the attributes are unavailable, returns blank strings to - avoid an error. + Returns the longname and unit of a variable. + + If the attributes are unavailable, returns blank strings to avoid + an error. :param fNcdf: an open netCDF file - :type fNcdf: netCDF file object + :type fNcdf: netCDF file object :param varname: variable to extract attribute from - :type varname: str + :type varname: str :return: longname and unit attributes - :rtype: str + :rtype: str + :raises ValueError: if the file is not a netCDF file + :raises FileNotFoundError: if the file does not exist + :raises TypeError: if the file is not a Dataset or MFDataset + :raises AttributeError: if the file does not have the _files or + filepath attribute + :raises OSError: if the file is not a valid path + :raises KeyError: if the variable is not found in the file .. note:: Some functions in MarsVars edit the units @@ -933,8 +1024,13 @@ def get_longname_unit(fNcdf, varname): def wbr_cmap(): """ - Returns a color map that goes from - white -> blue -> green -> yellow -> red + Returns a color map (From R. John Wilson). + + Color map goes from white -> blue -> green -> yellow -> red + [R. John Wilson, Nov 2022] + + :return: color map + :rtype: array """ tmp_cmap = np.zeros((254, 4)) @@ -1009,8 +1105,13 @@ def wbr_cmap(): def rjw_cmap(): """ - Returns John Wilson's preferred color map - (red -> jade -> wisteria) + Returns a color map (From R. John Wilson). + + Color map goes from red -> jade -> wisteria. + [R. John Wilson, Nov 2022] + + :return: color map + :rtype: array """ tmp_cmap = np.zeros((55, 4)) @@ -1035,8 +1136,14 @@ def rjw_cmap(): def hot_cold_cmap(): """ - Returns Dark blue > light blue>white>yellow>red colormap - Based on Matlab's bipolar colormap + Returns a color map (From Alex Kling, based on bipolar cmap). + + Color map goes from dark blue -> light blue -> white -> yellow -> red. + Based on Matlab's bipolar colormap. + [Alex Kling, Nov 2022] + + :return: color map + :rtype: array """ tmp_cmap = np.zeros((128,4)) @@ -1080,9 +1187,15 @@ def hot_cold_cmap(): def dkass_dust_cmap(): """ - Returns a color map useful for dust cross-sections. - (yellow -> orange -> red -> purple) - Provided by Courtney Batterson. + Color map (From Courtney Batterson). + + Returns a color map useful for dust cross-sections that highlight + dust mixing ratios > 4 ppm. The color map goes from + white -> yellow -> orange -> red -> purple. + [Courtney Batterson, Nov 2022] + + :return: color map + :rtype: array """ tmp_cmap = np.zeros((256, 4)) @@ -1138,9 +1251,15 @@ def dkass_dust_cmap(): def dkass_temp_cmap(): """ - Returns a color map that highlights the 200K temperatures. - (black -> purple -> blue -> green -> yellow -> orange -> red) - Provided by Courtney Batterson. + Color map (From Courtney Batterson). + + Returns a color map useful for highlighting the 200 K temperature + level. The color map goes from + black -> purple -> blue -> green -> yellow -> orange -> red. + [Courtney Batterson, Nov 2022] + + :return: color map + :rtype: array """ tmp_cmap = np.zeros((256, 4)) @@ -1196,16 +1315,22 @@ def dkass_temp_cmap(): def pretty_print_to_fv_eta(var, varname, nperline=6): """ - Print the ``ak`` or ``bk`` coefficients for copying to - ``fv_eta.f90``. + Print the ``ak`` or ``bk`` coefficients for copying to ``fv_eta.f90``. + + The ``ak`` and ``bk`` coefficients are used to calculate the + vertical coordinate transformation. :param var: ak or bk data - :type var: array + :type var: array :param varname: the variable name ("a" or "b") - :type varname: str + :type varname: str :param nperline: the number of elements per line, defaults to 6 - :type nperline: int, optional + :type nperline: int, optional :return: a print statement for copying into ``fv_eta.f90`` + :rtype: None + :raises ValueError: if varname is not "a" or "b" + :raises ValueError: if nperline is not a positive integer + :raises ValueError: if var is not a 1D array of length NLAY+1 """ NLAY = len(var) - 1 @@ -1249,17 +1374,23 @@ def pretty_print_to_fv_eta(var, varname, nperline=6): def replace_dims(Ncvar_dim, vert_dim_name=None): """ + Replaces the dimensions of a variable in a netCDF file. + Updates the name of the variable dimension to match the format of the new NASA Ames Mars GCM output files. :param Ncvar_dim: netCDF variable dimensions (e.g., ``f_Ncdf.variables["temp"].dimensions``) - :type Ncvar_dim: str + :type Ncvar_dim: str :param vert_dim_name: the vertical dimension if it is ambiguous (``pstd``, ``zstd``, or ``zagl``). Defaults to None - :type vert_dim_name: str, optional + :type vert_dim_name: str, optional :return: updated dimensions - :rtype: str + :rtype: str + :raises ValueError: if Ncvar_dim is not a list + :raises ValueError: if vert_dim_name is not a string + :raises ValueError: if vert_dim_name is not in the list of + recognized vertical dimensions """ # Set input dictionary options recognizable as MGCM variables @@ -1287,13 +1418,20 @@ def replace_dims(Ncvar_dim, vert_dim_name=None): def ak_bk_loader(fNcdf): """ - Return ``ak`` and ``bk`` arrays from the current netCDF file. If - these are not found in the current file, search the fixed file in - the same directory. If not there, then search the tiled fixed files. + Loads the ak and bk variables from a netCDF file. + + This function will first check the current netCDF file for the + ``ak`` and ``bk`` variables. If they are not found, it will + search the fixed file in the same directory. If they are still + not found, it will search the tiled fixed files. The function + will return the ``ak`` and ``bk`` arrays. :param fNcdf: an open netCDF file - :type fNcdf: a netCDF file object + :type fNcdf: a netCDF file object :return: the ``ak`` and ``bk`` arrays + :rtype: tuple + :raises ValueError: if the ``ak`` and ``bk`` variables are not + found in the netCDF file, the fixed file, or the tiled fixed files .. note:: This routine will look for both ``ak`` and ``bk``. There @@ -1368,31 +1506,24 @@ def ak_bk_loader(fNcdf): def read_variable_dict_amescap_profile(f_Ncdf=None): """ - Inspect a Netcdf file and return the name of the variables and - dimensions based on the content of ~/.amescap_profile. + Reads a variable dictionary from the ``amescap_profile`` file. - Calling this function allows to remove hard-coded calls in CAP. - For example, to f.variables['ucomp'] is replaced by - f.variables["ucomp"], with "ucomp" taking the values of'ucomp', 'U' + This function will read the variable dictionary from the + ``amescap_profile`` file and return a dictionary with the + variable names and dimensions. The function will also check + the opened netCDF file for the variable names and dimensions. :param f_Ncdf: An opened Netcdf file object - :type f_Ncdf: File object - :return: Model, a dictionary with the dimensions and variables, - e.g. "ucomp"='U' or "dim_lat"='latitudes' - - .. note:: - The defaut names for variables are defined in () - parenthesis in ~/.amescap_profile:: - - 'X direction wind [m/s] (ucomp)>' - - The defaut names for dimensions are defined in {} parenthesis in - ~/.amescap_profile:: - - Ncdf Y latitude dimension [integer] {lat}>lats - - The dimensions (lon, lat, pfull, pstd) are loaded in the dictionary - as "dim_lon", "dim_lat" + :type f_Ncdf: File object + :return: MOD, a class object with the variable names and dimensions + (e.g., ``MOD.ucomp`` = 'U' or ``MOD.dim_lat`` = 'latitudes') + :rtype MOD: class object + :raises ValueError: if the ``amescap_profile`` file is not found + or if the variable dictionary is not found + :raises ValueError: if the variable or dimension name is not found + in the netCDF file + :raises ValueError: if the variable or dimension name is not found + in the``amescap_profile`` """ if f_Ncdf is not None: @@ -1455,10 +1586,8 @@ class model(object): setattr(MOD,FV3_var, found_list[0]) else: setattr(MOD,FV3_var, found_list[0]) - prYellow( - f"***Warning*** more than one possible variable " - f"'{FV3_var}' found in file: {found_list}" - ) + print(f'{Yellow}***Warning*** more than one possible ' + f'variable "{FV3_var}" found in file: {found_list}') if type_input == 'dimension': for ivar in var_list: @@ -1469,25 +1598,28 @@ class model(object): setattr(MOD, f"dim_{FV3_var}", found_list[0]) else: setattr(MOD, f"dim_{FV3_var}", found_list[0]) - prYellow( - f"***Warning*** more than one possible dimension " - f"'{FV3_var}' found in file: {found_list}" - ) + print(f'{Yellow}***Warning*** more than one possible ' + f'dimension "{FV3_var}" found in file: {found_list}') return MOD def reset_FV3_names(MOD): """ - This function reset the model dictionary to the native FV3's - variables, e.g.:: + Resets the FV3 variable names in a netCDF file. + + This function resets the model dictionary to the native FV3 + variable names, e.g.:: model.dim_lat = 'latitude' > model.dim_lat = 'lat' model.ucomp = 'U' > model.ucomp = 'ucomp' :param MOD: Generated with read_variable_dict_amescap_profile() - :type MOD: class object + :type MOD: class object :return: same object with updated names for the dimensions and variables + :rtype: class object + :raises ValueError: if the MOD object is not a class object or does + not contain the expected attributes """ atts_list = dir(MOD) # Get all attributes @@ -1505,22 +1637,28 @@ def reset_FV3_names(MOD): def except_message(debug, exception, varname, ifile, pre="", ext=""): """ - This function prints an error message in the case of an exception. - It also contains a special error in the case of an already existing + Prints an error message if a variable is not found. + + It also contains a special error in the case of a pre-existing variable. :param debug: Flag for debug mode - :type debug: logical + :type debug: logical :param exception: Exception from try statement - :type exception: class object + :type exception: class object :param varname: Name of variable causing exception - :type varname: string + :type varname: string :param ifile: Name of input file - :type ifile: string + :type ifile: string :param pre: Prefix to new variable - :type pre: string + :type pre: string :param ext: Extension to new variable - :type ext: string + :type ext: string + :return: None + :rtype: None + :raises ValueError: if debug is True, exception is not a class + object or string, varname is not a string, ifile is not a + string, pre is not a string, or ext is not a string """ if debug: @@ -1536,18 +1674,24 @@ def except_message(debug, exception, varname, ifile, pre="", ext=""): def check_bounds(values, min_val, max_val, dx): """ - Check if all values in an array are within specified bounds. - Exits program if any value is out of bounds. + Checks the bounds of a variable in a netCDF file. + + This function checks if the values in a netCDF file are within + the specified bounds. If any value is out of bounds, it will + print an error message and exit the program. + The function can handle both single values and arrays. Parameters: :param values: Single value or array of values to check - :type values: array-like + :type values: array-like :param min_val: Minimum allowed value - :type min_val: float + :type min_val: float :param max_val: Maximum allowed value - :type max_val: float + :type max_val: float :return values: The validated value(s) - :rtype: array or float + :rtype: array or float + :raises ValueError: if values is out of bounds or if values is not + a number, array, or list """ try: diff --git a/bin/MarsCalendar.py b/bin/MarsCalendar.py index f971b31..aebb8b5 100755 --- a/bin/MarsCalendar.py +++ b/bin/MarsCalendar.py @@ -5,13 +5,13 @@ The executable requires 1 of the following arguments: - * ``[-sol --sol]`` The sol to convert to Ls, OR - * ``[-ls --ls]`` The Ls to convert to sol + * ``[-sol --sol]`` The sol to convert to Ls, OR + * ``[-ls --ls]`` The Ls to convert to sol and optionally accepts: - * ``[-my --marsyear]`` The Mars Year of the simulation to compute sol or Ls from, AND/OR - * ``[-c --continuous]`` Returns Ls in continuous form + * ``[-my --marsyear]`` The Mars Year of the simulation to compute sol or Ls from, AND/OR + * ``[-c --continuous]`` Returns Ls in continuous form Third-party Requirements: @@ -41,8 +41,39 @@ def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -164,13 +195,24 @@ def parse_array(len_input): :param len_input: The input Ls or sol to convert. Can either be one number (e.g., 300) or start stop step (e.g., 300 310 2). - :type len_input: float or list of floats + :type len_input: float or list of floats :raises: Error if neither ``[-ls --ls]`` or ``[-sol --sol]`` are provided. :return: ``input_as_arr`` An array formatted for input into ``ls2sol`` or ``sol2ls``. If ``len_input = 300``, then ``input_as_arr=[300]``. If ``len_input = 300 310 2``, then - ``input_as_arr = [300, 302, 304, 306, 308]``.\n + ``input_as_arr = [300, 302, 304, 306, 308]``. + :rtype: list of floats + :raises ValueError: If the input is not a valid number or + range. + :raises TypeError: If the input is not a valid type. + :raises IndexError: If the input is not a valid index. + :raises KeyError: If the input is not a valid key. + :raises AttributeError: If the input is not a valid attribute. + :raises ImportError: If the input is not a valid import. + :raises RuntimeError: If the input is not a valid runtime. + :raises OverflowError: If the input is not a valid overflow. + :raises MemoryError: If the input is not a valid memory. """ if len(len_input) == 1: @@ -197,6 +239,36 @@ def parse_array(len_input): @debug_wrapper def main(): + """ + Main function for MarsCalendar command-line tool. + + This function processes user-specified arguments to convert between + Mars solar longitude (Ls) and sol (Martian day) values for a given + Mars year. It supports both continuous and discrete sol + calculations, and can handle input in either Ls or sol, returning + the corresponding converted values. The results are printed in a + formatted table. + + Arguments are expected to be provided via the `args` namespace: + + - args.marsyear: Mars year (default is 0) + - args.continuous: If set, enables continuous sol calculation + - args.ls: List of Ls values to convert to sol + - args.sol: List of sol values to convert to Ls + + :param args: Command-line arguments parsed using argparse. + :type args: argparse.Namespace + :raises ValueError: If the input is not a valid number or + range. + :returns: 0 if the program runs successfully, 1 if an error occurs. + :rtype: int + :raises RuntimeError: If the input is not a valid runtime. + :raises TypeError: If the input is not a valid type. + :raises IndexError: If the input is not a valid index. + :raises KeyError: If the input is not a valid key. + :raises AttributeError: If the input is not a valid attribute. + :raises ImportError: If the input is not a valid import. + """ # Load in user-specified Mars year, if any. Default = 0 MY = np.squeeze(args.marsyear) print(f"MARS YEAR = {MY}") diff --git a/bin/MarsFiles.py b/bin/MarsFiles.py index b8db80c..93d3871 100755 --- a/bin/MarsFiles.py +++ b/bin/MarsFiles.py @@ -79,6 +79,38 @@ # ------------------------------------------------------ class ExtAction(argparse.Action): + """ + Custom action for argparse to handle file extensions. + + This action is used to add an extension to the output file. + + :param ext_content: The content to be added to the file name. + :type ext_content: str + :param parser: The parser instance to which this action belongs. + :type parser: argparse.ArgumentParser + :param args: Additional positional arguments. + :type args: tuple + :param kwargs: Additional keyword arguments. + :type kwargs: dict + :param ext_content: The content to be added to the file name + :type ext_content: str + :return: None + :rtype: None + :raises ValueError: If ext_content is not provided. + :raises TypeError: If parser is not an instance of argparse.ArgumentParser. + :raises Exception: If an error occurs during the action. + :raises AttributeError: If the parser does not have the specified attribute. + :raises ImportError: If the parser cannot be imported. + :raises RuntimeError: If the parser cannot be run. + :raises KeyError: If the parser does not have the specified key. + :raises IndexError: If the parser does not have the specified index. + :raises IOError: If the parser cannot be opened. + :raises OSError: If the parser cannot be accessed. + :raises EOFError: If the parser cannot be read. + :raises MemoryError: If the parser cannot be allocated. + :raises OverflowError: If the parser cannot be overflowed. + """ + def __init__(self, *args, ext_content=None, parser=None, **kwargs): self.parser = parser # Store the extension content that's specific to this argument @@ -119,6 +151,36 @@ def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, ext_attr, self.ext_content) class ExtArgumentParser(argparse.ArgumentParser): + """ + Custom ArgumentParser that handles file extensions for output files. + + This class extends the argparse.ArgumentParser to add functionality + for handling file extensions in the command-line arguments. + + :param args: Additional positional arguments. + :type args: tuple + :param kwargs: Additional keyword arguments. + :type kwargs: dict + :param ext_content: The content to be added to the file name. + :type ext_content: str + :param parser: The parser instance to which this action belongs. + :type parser: argparse.ArgumentParser + :return: None + :rtype: None + :raises ValueError: If ext_content is not provided. + :raises TypeError: If parser is not an instance of argparse.ArgumentParser. + :raises Exception: If an error occurs during the action. + :raises AttributeError: If the parser does not have the specified attribute. + :raises ImportError: If the parser cannot be imported. + :raises RuntimeError: If the parser cannot be run. + :raises KeyError: If the parser does not have the specified key. + :raises IndexError: If the parser does not have the specified index. + :raises IOError: If the parser cannot be opened. + :raises OSError: If the parser cannot be accessed. + :raises EOFError: If the parser cannot be read. + :raises MemoryError: If the parser cannot be allocated. + :raises OverflowError: If the parser cannot be overflowed. + """ def parse_args(self, *args, **kwargs): namespace = super().parse_args(*args, **kwargs) @@ -134,8 +196,39 @@ def parse_args(self, *args, **kwargs): def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -655,9 +748,18 @@ def concatenate_files(file_list, full_file_list): Concatenates sequential output files in chronological order. :param file_list: list of file names - :type file_list: list + :type file_list: list :param full_file_list: list of file names and full paths - :type full_file_list: list + :type full_file_list: list + :returns: None + :rtype: None + :raises OSError: If the file cannot be removed. + :raises IOError: If the file cannot be moved. + :raises Exception: If the file cannot be opened. + :raises ValueError: If the file cannot be accessed. + :raises TypeError: If the file is not of the correct type. + :raises IndexError: If the file does not have the correct index. + :raises KeyError: If the file does not have the correct key. """ print(f"{Yellow}Using internal method for concatenation{Nclr}") @@ -729,12 +831,38 @@ def concatenate_files(file_list, full_file_list): def split_files(file_list, split_dim): """ - Extracts variables in the file along the time dimension, unless - other dimension is specified (lev, lat, or lon). + Extracts variables in the file along the specified dimension. + + The function creates a new file with the same name as the input + file, but with the specified dimension sliced to the requested + value or range of values. The new file is saved in the same + directory as the input file. + + The function also checks the bounds of the requested dimension + against the actual values in the file. If the requested value + is outside the bounds of the dimension, an error message is + printed and the function exits. + + The function also checks if the requested dimension is valid + (i.e., time, lev, lat, or lon). If the requested dimension is + invalid, an error message is printed and the function exits. + + The function also checks if the requested dimension is a + single dimension (i.e., areo). If the requested dimension is + a single dimension, the function removes all single dimensions + from the areo dimension (i.e., scalar_axis) before performing + the extraction. :param file_list: list of file names - :type split_dim: dimension along which to perform extraction + :type split_dim: dimension along which to perform extraction :returns: new file with sliced dimensions + :rtype: None + :raises OSError: If the file cannot be removed. + :raises IOError: If the file cannot be moved. + :raises Exception: If the file cannot be opened. + :raises ValueError: If the file cannot be accessed. + :raises TypeError: If the file is not of the correct type. + :raises IndexError: If the file does not have the correct index. """ if split_dim not in ['time','areo', 'lev', 'lat', 'lon']: @@ -1034,11 +1162,20 @@ def split_files(file_list, split_dim): # ------------------------------------------------------ def process_time_shift(file_list): """ - This function converts the data in diurn files with a time_of_day_XX - dimension to universal local time. + Converts diurn files to local time. + + This function is used to convert diurn files to local time. :param file_list: list of file names - :type file_list: list + :type file_list: list + :returns: None + :rtype: None + :raises OSError: If the file cannot be removed. + :raises IOError: If the file cannot be moved. + :raises Exception: If the file cannot be opened. + :raises ValueError: If the file cannot be accessed. + :raises TypeError: If the file is not of the correct type. + :raises IndexError: If the file does not have the correct index. """ if args.time_shift == 999: @@ -1189,6 +1326,41 @@ def process_time_shift(file_list): @debug_wrapper def main(): + """ + Main entry point for MarsFiles data processing utility. + + This function processes input NetCDF or legacy MGCM files according + to command-line arguments. It supports a variety of operations, + including: + + - Conversion of legacy MGCM files to FV3 format. + - Concatenation and splitting of NetCDF files along specified + dimensions. + - Temporal binning of daily files into average or diurnal files. + - Temporal filtering (high-pass, low-pass, band-pass) using spectral + methods. + - Spatial (zonal) filtering and decomposition using spherical + harmonics. + - Tidal analysis and harmonic decomposition of diurnal files. + - Regridding of data to match a target NetCDF file's grid. + - Zonal averaging of variables over longitude. + + The function handles file path resolution, argument validation, and + orchestrates the appropriate processing routines based on + user-specified options. Output files are written in NetCDF format, + with new variables and dimensions created as needed. + + Global Variables: + data_dir (str): The working directory for input/output files. + Arguments: + None directly. Uses global 'args' parsed from command-line. + Returns: + None. Outputs are written to disk. + Raises: + SystemExit: For invalid argument combinations or processing + errors. + """ + global data_dir file_list = [f.name for f in args.input_file] data_dir = os.getcwd() @@ -2038,19 +2210,29 @@ def main(): def make_FV3_files(fpath, typelistfv3, renameFV3=True): """ Make MGCM-like ``average``, ``daily``, and ``diurn`` files. - Used if call to [``-bin --bin_files``] is made AND Legacy files are in - netCDFformat (not fort.11). + + Used if call to [``-bin --bin_files``] is made AND Legacy files are + in netCDF format (not fort.11). :param fpath: Full path to the Legacy netcdf files - :type fpath: str + :type fpath: str :param typelistfv3: MGCM-like file type: ``average``, ``daily``, or ``diurn`` - :type typelistfv3: list + :type typelistfv3: list :param renameFV3: Rename the files from Legacy_LsXXX_LsYYY.nc to ``XXXXX.atmos_average.nc`` following MGCM output conventions - :type renameFV3: bool + :type renameFV3: bool :return: The MGCM-like files: ``XXXXX.atmos_average.nc``, ``XXXXX.atmos_daily.nc``, ``XXXXX.atmos_diurn.nc``. + :rtype: None + + :note:: + The ``average`` and ``daily`` files are created by + averaging over the ``diurn`` file. The ``diurn`` file is + created by binning the Legacy files. + + :note:: + The ``diurn`` file is created by binning the Legacy files. """ historyDir = os.getcwd() @@ -2064,17 +2246,35 @@ def make_FV3_files(fpath, typelistfv3, renameFV3=True): def proccess_file(newf, typefv3): """ - Creates required variables and inputs them into the new - files. Required variables include ``latitude``, - ``longitude``, ``time``, ``time-of-day`` (if diurn file), - and vertical layers (``phalf`` and ``pfull``). + Process the new file. + + This function is called by ``make_FV3_files`` to create the + required variables and dimensions for the new file. The new file + is created in the same directory as the Legacy file. New file + is named ``XXXXX.atmos_average.nc``, ``XXXXX.atmos_daily.nc``, + or ``XXXXX.atmos_diurn.nc``. + + Requires variables ``latitude``, ``longitude``, ``time``, + ``time-of-day`` (if diurn file), and vertical layers (``phalf`` + and ``pfull``). :param newf: path to target file - :type newf: str + :type newf: str :param typefv3: identifies type of file: ``average``, ``daily``, or ``diurn`` - :type typefv3: str + :type typefv3: str :return: netCDF file with minimum required variables + ``latitude``, ``longitude``, ``time``, ``time-of-day``, + ``phalf``, and ``pfull``. + :rtype: None + :raises KeyError: if the required variables are not found + :raises ValueError: if the required dimensions are not found + :raises AttributeError: if the required attributes are not found + + :note:: + The ``diurn`` file is created by binning the Legacy files. + The ``average`` and ``daily`` files are created by + averaging over the ``diurn`` file. """ for dname in histdims: @@ -2219,19 +2419,28 @@ def do_avg_vars(histfile, newf, avgtime, avgtod, bin_period=5): Performs a time average over all fields in a file. :param histfile: file to perform time average on - :type histfile: str + :type histfile: str :param newf: path to target file - :type newf: str + :type newf: str :param avgtime: whether ``histfile`` has averaged fields (e.g., ``atmos_average``) - :type avgtime: bool + :type avgtime: bool :param avgtod: whether ``histfile`` has a diurnal time dimenion (e.g., ``atmos_diurn``) - :type avgtod: bool + :type avgtod: bool :param bin_period: the time binning period if `histfile` has averaged fields (i.e., if ``avgtime==True``), defaults to 5 - :type bin_period: int, optional + :type bin_period: int, optional :return: a time-averaged file + :rtype: None + :raises KeyError: if the required variables are not found + :raises ValueError: if the required dimensions are not found + :raises AttributeError: if the required attributes are not found + + :note:: + The ``diurn`` file is created by binning the Legacy files. + The ``average`` and ``daily`` files are created by + averaging over the ``diurn`` file. """ histvars = histfile.variables.keys() @@ -2400,12 +2609,22 @@ def change_vname_longname_unit(vname, longname_txt, units_txt): designed to work specifically with LegacyCGM.nc files. :param vname: variable name - :type vname: str + :type vname: str :param longname_txt: variable description - :type longname_txt: str + :type longname_txt: str :param units_txt: variable units - :type units_txt: str + :type units_txt: str :return: variable name and corresponding description and unit + (e.g. ``vname = "ps"``) + :rtype: tuple + :raises KeyError: if the required variables are not found + :raises ValueError: if the required dimensions are not found + :raises AttributeError: if the required attributes are not found + + :note:: + The ``diurn`` file is created by binning the Legacy files. + The ``average`` and ``daily`` files are created by + averaging over the ``diurn`` file. """ if vname == "psurf": @@ -2460,11 +2679,15 @@ def replace_dims(dims, todflag): This is designed to work specifically with LegacyCGM.nc files. :param dims: dimensions of the variable - :type dims: str + :type dims: str :param todflag: indicates whether there exists a ``time_of_day`` dimension - :type todflag: bool + :type todflag: bool :return: new dimension names for the variable + :rtype: tuple + :raises KeyError: if the required variables are not found + :raises ValueError: if the required dimensions are not found + :raises AttributeError: if the required attributes are not found """ newdims = dims @@ -2486,17 +2709,24 @@ def replace_dims(dims, todflag): def replace_at_index(tuple_dims, idx, new_name): """ - Updates variable dimensions. + Replaces the dimension at the given index with a new name. + + If ``new_name`` is None, the dimension is removed. + This is designed to work specifically with LegacyCGM.nc files. :param tuple_dims: the dimensions as tuples e.g. (``pfull``, ``nlat``, ``nlon``) - :type tuple_dims: tuple + :type tuple_dims: tuple :param idx: index indicating axis with the dimensions to update (e.g. ``idx = 1`` for ``nlat``) - :type idx: int + :type idx: int :param new_name: new dimension name (e.g. ``latitude``) - :type new_name: str + :type new_name: str :return: updated dimensions + :rtype: tuple + :raises KeyError: if the required variables are not found + :raises ValueError: if the required dimensions are not found + :raises AttributeError: if the required attributes are not found """ if new_name is None: @@ -2509,17 +2739,25 @@ def ls2sol_1year(Ls_deg, offset=True, round10=True): """ Returns a sol number from the solar longitude. + This is consistent with the MGCM model. The Ls is the solar + longitude in degrees. The sol number is the number of sols since + the perihelion (Ls = 250.99 degrees). + :param Ls_deg: solar longitude [°] - :type Ls_deg: float + :type Ls_deg: float :param offset: if True, force year to start at Ls 0 - :type offset: bool + :type offset: bool :param round10: if True, round to the nearest 10 sols - :type round10: bool + :type round10: bool :returns: ``Ds`` the sol number + :rtype: float + :raises ValueError: if the required variables are not found + :raises KeyError: if the required variables are not found + :raises AttributeError: if the required attributes are not found ..note:: - For the moment, this is consistent with 0 <= Ls <= 359.99, but - not for monotically increasing Ls. + This is consistent with 0 <= Ls <= 359.99, but not for + monotically increasing Ls. """ Ls_perihelion = 250.99 # Ls at perihelion diff --git a/bin/MarsFormat.py b/bin/MarsFormat.py index 3ba4f74..8775012 100755 --- a/bin/MarsFormat.py +++ b/bin/MarsFormat.py @@ -29,10 +29,6 @@ * ``traceback`` * ``xarray`` * ``amescap`` - -List of Functions: - - * download - Queries the requested file from the NAS Data Portal. """ # Make print statements appear in color @@ -62,8 +58,38 @@ def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -183,14 +209,21 @@ def wrapper(*args, **kwargs): def get_time_dimension_name(DS, model): """ Find the time dimension name in the dataset. + Updates the model object with the correct dimension name. :param DS: The xarray Dataset - :type DS: xarray.Dataset + :type DS: xarray.Dataset :param model: Model object with dimension information - :type model: object + :type model: object :return: The actual time dimension name found - :rtype: str + :rtype: str + :raises KeyError: If no time dimension is found + :raises ValueError: If the model object is not defined + :raises TypeError: If the dataset is not an xarray Dataset + :raises AttributeError: If the model object does not have the + specified attribute + :raises ImportError: If the xarray module cannot be imported """ # First try the expected dimension name @@ -212,6 +245,57 @@ def get_time_dimension_name(DS, model): @debug_wrapper def main(): + """ + Main processing function for MarsFormat. + + This function processes NetCDF files from various Mars General + Circulation Models (GCMs) + including MarsWRF, OpenMars, PCM, and EMARS, and reformats them for + use in the AmesCAP + framework. + + It performs the following operations: + - Validates the selected GCM type and input files. + - Loads NetCDF files and reads model-specific variable and + dimension mappings. + - Applies model-specific post-processing, including: + - Unstaggering variables (for MarsWRF and EMARS). + - Creating and orienting pressure coordinates (pfull, phalf, + ak, bk). + - Standardizing variable and dimension names. + - Converting longitude ranges to 0-360 degrees east. + - Adding scalar axes where required. + - Handling vertical dimension orientation, especially for + PCM files. + - Optionally performs time binning: + - Daily, average (over N sols), or diurnal binning. + - Ensures correct time units and bin sizes. + - Preserves or corrects vertical orientation after binning. + - Writes processed datasets to new NetCDF files with appropriate + naming conventions. + + Args: + None. Uses global `args` for configuration and file selection. + + Raises: + KeyError: If required dimensions or variables are missing in + the input files. + ValueError: If dimension swapping fails for PCM files. + SystemExit: If no valid GCM type is specified. + + Outputs: + Writes processed NetCDF files to disk, with suffixes indicating + the type of processing + (e.g., _daily, _average, _diurn, _nat). + + Note: + This function assumes the presence of several helper functions + and global variables, + such as `read_variable_dict_amescap_profile`, + `get_time_dimension_name`, `reset_FV3_names`, and color + constants for printing. + """ + ext = '' # Initialize empty extension if args.gcm_name not in ['marswrf', 'openmars', 'pcm', 'emars']: diff --git a/bin/MarsInterp.py b/bin/MarsInterp.py index 644c814..f4c3dee 100755 --- a/bin/MarsInterp.py +++ b/bin/MarsInterp.py @@ -17,7 +17,6 @@ * ``[-ext --extension]`` Custom extension for the new file * ``[-print --print_grid]`` Print the vertical grid to the screen - Third-party Requirements: * ``numpy`` @@ -67,8 +66,38 @@ def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -219,8 +248,55 @@ def wrapper(*args, **kwargs): @debug_wrapper def main(): + """ + Main function for performing vertical interpolation on Mars + atmospheric model NetCDF files. + + This function processes one or more input NetCDF files, + interpolating variables from their native vertical coordinate + (e.g., model pressure levels) to a user-specified standard vertical + grid (pressure, altitude, or altitude above ground level). + The interpolation type and grid can be customized via command-line + arguments. + + Workflow: + 1. Parses command-line arguments for input files, interpolation + type, custom vertical grid, and other options. + 2. Loads standard vertical grid definitions (pressure, altitude, + or altitude above ground level) or uses a custom grid. + 3. Optionally prints the vertical grid and exits if requested. + 4. For each input file: + - Checks file existence. + - Loads necessary variables (e.g., pk, bk, ps, temperature). + - Computes the 3D vertical coordinate field for + interpolation. + - Creates a new NetCDF output file with updated vertical + dimension. + - Interpolates selected variables to the new vertical grid. + - Copies or interpolates other variables as appropriate. + 5. Handles both regular and diurnal-cycle files, as well as + FV3-tiled and lat/lon grids. + + Command-line arguments (via `args`): + - input_file: List of input NetCDF files to process. + - interp_type: Type of vertical interpolation ('pstd', 'zstd', + or 'zagl'). + - vertical_grid: Custom vertical grid definition (optional). + - print_grid: If True, prints the vertical grid and exits. + - extension: Optional string to append to output filenames. + - include: List of variable names to include in interpolation. + - debug: Enable debug output. + + Notes: + - Requires several helper functions and classes (e.g., + section_content_amescap_profile, find_fixedfile, Dataset, + Ncdf, vinterp). + - Handles both FV3-tiled and regular lat/lon NetCDF files. + - Exits with an error message if required files or variables are + missing. + """ + start_time = time.time() - debug = args.debug # Load all of the netcdf files file_list = file_list = [f.name for f in args.input_file] interp_type = args.interp_type # e.g. pstd diff --git a/bin/MarsPlot.py b/bin/MarsPlot.py index 9437006..c10e686 100755 --- a/bin/MarsPlot.py +++ b/bin/MarsPlot.py @@ -79,8 +79,38 @@ def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -347,6 +377,28 @@ def wrapper(*args, **kwargs): # ====================================================================== @debug_wrapper def main(): + """ + Main entry point for the MarsPlot script. + + Handles argument parsing, global variable setup, figure object + initialization, and execution of the main plotting workflow. + Depending on the provided arguments, this function can: + + - Inspect the contents of a NetCDF file and print variable + information or statistics. + - Generate a template configuration file. + - Parse a provided template file, select data based on optional + date bounds, and generate + diagnostic plots as individual files or as a merged + multipage PDF. + - Manage output directories and file naming conventions. + - Display progress and handle debug output. + + Global variables are set for configuration and figure formatting. + The function also manages error handling and user feedback for + invalid arguments or file operations. + """ + global output_path, input_paths, out_format, debug output_path = os.getcwd() out_format = 'pdf' if args.figure_filetype is None else args.figure_filetype @@ -593,15 +645,23 @@ def main(): def mean_func(arr, axis): """ + Calculate the mean of an array along a specified axis. + This function calculates a mean over the selected axis, ignoring or including NaN values as specified by ``show_NaN_in_slice`` in ``amescap_profile``. :param arr: the array to be averaged - :type arr: array + :type arr: array :param axis: the axis over which to average the array - :type axis: int + :type axis: int :return: the mean over the time axis + :rtype: array + :raises ValueError: If the array is empty or the axis is out of bounds. + :raises RuntimeWarning: If the mean calculation encounters NaN values. + :raises TypeError: If the input array is not a valid type for mean + calculation. + :raises Exception: If the mean calculation fails for any reason. """ with warnings.catch_warnings(): @@ -617,14 +677,18 @@ def shift_data(lon, data): Shifts the longitude data from 0-360 to -180/180 and vice versa. :param lon: 1D array of longitude - :type lon: array [lon] + :type lon: array [lon] :param data: 2D array with last dimension = longitude - :type data: array [1,lon] - :raises ValueError: Longitude coordinate type is not recognized. - :return: longitude (-180/180) - :rtype: array [lon] - :return: shifted data - :rtype: array [1,lon] + :type data: array [1,lon] + :return: 1D array of longitude in -180/180 or 0-360 + :rtype: array [lon] + :return: 2D array with last dimension = longitude + :rtype: array [1,lon] + :raises ValueError: If the longitude coordinate type is invalid. + :raises TypeError: If the input data is not a valid type for + shifting. + :raises Exception: If the shifting operation fails for any reason. + .. note:: Use ``np.ma.hstack`` instead of ``np.hstack`` to keep the @@ -653,12 +717,14 @@ def shift_data(lon, data): def MY_func(Ls_cont): """ - Returns the Mars Year + Returns the Mars Year. :param Ls_cont: solar longitude (``areo``; continuous) - :type Ls_cont: array [areo] + :type Ls_cont: array [areo] :return: the Mars year - :rtype: int + :rtype: int + :raises ValueError: If the input Ls_cont is not a valid type for + year calculation. """ return (Ls_cont)//(360.)+1 @@ -666,18 +732,19 @@ def MY_func(Ls_cont): def get_lon_index(lon_query_180, lons): """ - Returns the indices that will extract data from the netCDF file - according to a range of *longitudes*. + Returns the indices for a range of longitudes in a file. :param lon_query_180: longitudes in -180/180: value, ``[min, max]``, or `None` - :type lon_query_180: list + :type lon_query_180: list :param lons: longitude in 0-360 - :type lons: array [lon] + :type lons: array [lon] :return: 1D array of file indices - :rtype: array + :rtype: array :return: text descriptor for the extracted longitudes - :rtype: str + :rtype: str + :raises ValueError: If the input lon_query_180 is not a valid type + for longitude calculation. .. note:: The keyword ``all`` passed as ``-99999`` by the rT() functions @@ -766,15 +833,17 @@ def get_lon_index(lon_query_180, lons): def get_lat_index(lat_query, lats): """ - Returns the indices that will extract data from the netCDF file - according to a range of *latitudes*. + Returns the indices for a range of latitudes in a file. :param lat_query: requested latitudes (-90/+90) - :type lat_query: list + :type lat_query: list :param lats: latitude - :type lats: array [lat] + :type lats: array [lat] :return: 1d array of file indices - :rtype: text descriptor for the extracted longitudes + :rtype: text descriptor for the extracted longitudes + :rtype: str + :raises ValueError: If the input lat_query is not a valid type for + latitude calculation. .. note::T The keyword ``all`` passed as ``-99999`` by the ``rt()`` @@ -814,17 +883,18 @@ def get_lat_index(lat_query, lats): def get_tod_index(tod_query, tods): """ - Returns the indices that will extract data from the netCDF file - according to a range of *times of day*. + Returns the indices for a range of times of day in a file. :param tod_query: requested time of day (0-24) - :type tod_query: list + :type tod_query: list :param tods: times of day - :type tods: array [tod] + :type tods: array [tod] :return: file indices - :rtype: array [tod] + :rtype: array [tod] :return: descriptor for the extracted time of day - :rtype: str + :rtype: str + :raises ValueError: If the input tod_query is not a valid type for + time of day calculation. .. note:: The keyword ``all`` is passed as ``-99999`` by the ``rT()`` @@ -870,17 +940,18 @@ def get_tod_index(tod_query, tods): def get_level_index(level_query, levs): """ - Returns the indices that will extract data from the netCDF file - according to a range of *pressures* (resp. depth for ``zgrid``). + Returns the indices for a range of pressures in a file. :param level_query: requested pressure [Pa] (depth [m]) - :type level_query: float + :type level_query: float :param levs: levels (in the native coordinates) - :type levs: array [lev] + :type levs: array [lev] :return: file indices - :rtype: array + :rtype: array :return: descriptor for the extracted pressure (depth) - :rtype: str + :rtype: str + :raises ValueError: If the input level_query is not a valid type for + level calculation. .. note:: The keyword ``all`` is passed as ``-99999`` by the ``rT()`` @@ -931,21 +1002,26 @@ def get_level_index(level_query, levs): def get_time_index(Ls_query_360, LsDay): """ - Returns the indices that will extract data from the netCDF file - according to a range of solar longitudes [0-360]. + Returns the indices for a range of solar longitudes in a file. First try the Mars Year of the last timestep, then try the year before that. Use whichever Ls period is closest to the requested date. :param Ls_query_360: requested solar longitudes - :type Ls_query_360: list + :type Ls_query_360: list :param LsDay: continuous solar longitudes - :type LsDay: array [areo] + :type LsDay: array [areo] :return: file indices - :rtype: array + :rtype: array :return: descriptor for the extracted solar longitudes - :rtype: str + :rtype: str + :raises ValueError: If the input Ls_query_360 is not a valid type + for solar longitude calculation. + :raises TypeError: If the input LsDay is not a valid type for + solar longitude calculation. + :raises Exception: If the time index calculation fails for any + reason. .. note:: The keyword ``all`` is passed as ``-99999`` by the ``rT()`` @@ -1031,16 +1107,23 @@ def get_time_index(Ls_query_360, LsDay): # ====================================================================== def filter_input(txt, typeIn="char"): """ - Read template for the type of data expected + Read template for the type of data expected. + + Returns value to ``rT()``. :param txt: text input into ``Custom.in`` to the right of an equal sign - :type txt: str + :type txt: str :param typeIn: type of data expected: ``char``, ``float``, ``int``, ``bool``, defaults to ``char`` - :type typeIn: str, optional + :type typeIn: str, optional :return: text input reformatted to ``[val1, val2]`` - :rtype: float or array + :rtype: float or array + :raises ValueError: If the input txt is not a valid type for + filtering. + :raises TypeError: If the input typeIn is not a valid type for + filtering. + :raises Exception: If the filtering operation fails for any reason. """ if txt == "None" or not txt: @@ -1087,14 +1170,21 @@ def filter_input(txt, typeIn="char"): def rT(typeIn="char"): """ - Read template for the type of data expected. Returns value to + Read template for the type of data expected. + + Returns value to ``filter_input()``. :param typeIn: type of data expected: ``char``, ``float``, ``int``, ``bool``, defaults to ``char`` - :type typeIn: str, optional + :type typeIn: str, optional :return: text input reformatted to ``[val1, val2]`` - :rtype: float or array + :rtype: float or array + :raises ValueError: If the input typeIn is not a valid type for + filtering. + :raises TypeError: If the input typeIn is not a valid type for + filtering. + :raises Exception: If the filtering operation fails for any reason. """ global customFileIN @@ -1126,18 +1216,20 @@ def read_axis_options(axis_options_txt): :param axis_options_txt: a copy of the last line ``Axis Options`` in ``Custom.in`` templates - :type axis_options_txt: str + :type axis_options_txt: str :return: X-axis bounds as a numpy array or ``None`` if undedefined - :rtype: array or None + :rtype: array or None :return: Y-axis bounds as a numpy array or ``None`` if undedefined - :rtype: array or None + :rtype: array or None :return: colormap (e.g., ``jet``, ``nipy_spectral``) or line options (e.g., ``--r`` for dashed red) - :rtype: str + :rtype: str :return: linear (``lin``) or logarithmic (``log``) color scale - :rtype: str + :rtype: str :return: projection (e.g., ``ortho -125,45``) - :rtype: str + :rtype: str + :raises ValueError: If the input axis_options_txt is not a valid + type for axis options. """ list_txt = axis_options_txt.split(":")[1].split("|") @@ -1188,15 +1280,17 @@ def split_varfull(varfull): :param varfull: a ``varfull`` object (e.g, ``atmos_average@2.zsurf``, ``02400.atmos_average@2.zsurf``) - :type varfull: str + :type varfull: str :return: (sol_array) a sol number or ``None`` (if none provided) - :rtype: int or None + :rtype: int or None :return: (filetype) file type (e.g, ``atmos_average``) - :rtype: str + :rtype: str :return: (var) variable of interest (e.g, ``zsurf``) - :rtype: str + :rtype: str :return: (``simuID``) simulation ID (Python indexing starts at 0) - :rtype: int + :rtype: int + :raises ValueError: If the input varfull is not a valid type for + splitting. """ if varfull.count(".") == 1: @@ -1238,10 +1332,12 @@ def remove_whitespace(raw_input): :param raw_input: user input for variable, (e.g., ``[atmos_average.temp] + 2)`` - :type raw_input: str + :type raw_input: str :return: raw_input without whitespaces (e.g., ``[atmos_average.temp]+2)`` - :rtype: str + :rtype: str + :raises ValueError: If the input raw_input is not a valid type for + whitespace removal. """ processed_input = "" for i in range(0, len(raw_input)): @@ -1257,10 +1353,10 @@ def clean_comma_whitespace(raw_input): :param raw_input: dimensions specified by user input to Variable (e.g., ``lat=3. , lon=2 , lev = 10.``) - :type raw_input: str + :type raw_input: str :return: raw_input without whitespaces (e.g., ``lat=3.,lon=2,lev=10.``) - :rtype: str + :rtype: str """ processed_input = "" @@ -1276,10 +1372,12 @@ def get_list_varfull(raw_input): :param raw_input: complex user input to Variable (e.g., ``2*[atmos_average.temp]+[atmos_average2.ucomp]*1000``) - :type raw_input: str + :type raw_input: str :return: list required variables (e.g., [``atmos_average.temp``, ``atmos_average2.ucomp``]) - :rtype: str + :rtype: str + :raises ValueError: If the input raw_input is not a valid type for + variable extraction. """ var_list = [] @@ -1301,8 +1399,17 @@ def get_list_varfull(raw_input): def get_overwrite_dim_2D(varfull_bracket, plot_type, fdim1, fdim2, ftod): """ - Return new dimensions that will overwrite default dimensions for a - varfull object with ``{}`` on a 2D plot. + 2D plot: overwrite dimensions in ``varfull`` object with ``{}``. + + (e.g., ``atmos_average.temp{lev=10;ls=350;lon=155;lat=25}``) + + This function is used to overwrite the default dimensions in a + ``varfull`` object with ``{}`` (e.g., ``atmos_average.temp{lev=10; + ls=350;lon=155;lat=25}``) for a 2D plot. The function will return + the new dimensions that will overwrite the default dimensions for + the ``varfull`` object. The function will also return the required + file and variable (e.g., ``atmos_average.temp``) and the X and Y + axis dimensions for the plot. ``2D_lon_lat: fdim1 = ls, fdim2 = lev`` ``2D_lat_lev: fdim1 = ls, fdim2 = lon`` @@ -1313,18 +1420,24 @@ def get_overwrite_dim_2D(varfull_bracket, plot_type, fdim1, fdim2, ftod): :param varfull_bracket: a ``varfull`` object with ``{}`` (e.g., ``atmos_average.temp{lev=10;ls=350;lon=155;lat=25}``) - :type varfull_bracket: str + :type varfull_bracket: str :param plot_type: the type of the plot template - :type plot_type: str + :type plot_type: str :param fdim1: X axis dimension for plot - :type fdim1: str + :type fdim1: str :param fdim2: Y axis dimension for plot - :type fdim2: str + :type fdim2: str :return: (varfull) required file and variable (e.g., ``atmos_average.temp``); (fdim_out1) X axis dimension for plot; (fdim_out2) Y axis dimension for plot; (ftod_out) if X or Y axis dimension is time of day + :rtype: str + :raises ValueError: If the input varfull_bracket is not a valid + type for variable extraction. + :raises TypeError: If the input plot_type is not a valid type for + variable extraction. + :raises Exception: If the variable extraction fails for any reason. """ # Initialization: use dimension provided in template @@ -1405,29 +1518,50 @@ def get_overwrite_dim_2D(varfull_bracket, plot_type, fdim1, fdim2, ftod): def get_overwrite_dim_1D(varfull_bracket, t_in, lat_in, lon_in, lev_in, ftod_in): """ - Return new dimensions that will overwrite default dimensions for a - varfull object with ``{}`` for a 1D plot. + 1D plot: overwrite dimensions in ``varfull`` object with ``{}``. + + (e.g., ``atmos_average.temp{lev=10;ls=350;lon=155;lat=25}``) + This function is used to overwrite the default dimensions in a + ``varfull`` object with ``{}`` (e.g., ``atmos_average.temp{lev=10; + ls=350;lon=155;lat=25}``) for a 1D plot. The function will return + the new dimensions that will overwrite the default dimensions for + the ``varfull`` object. The function will also return the required + file and variable (e.g., ``atmos_average.temp``) and the X and Y + axis dimensions for the plot. :param varfull_bracket: a ``varfull`` object with ``{}`` (e.g., ``atmos_average.temp{lev=10;ls=350;lon=155;lat=25}``) - :type varfull_bracket: str + :type varfull_bracket: str :param t_in: self.t variable - :type t_in: array [time] + :type t_in: array [time] :param lat_in: self.lat variable - :type lat_in: array [lat] + :type lat_in: array [lat] :param lon_in: self.lon variable - :type lon_in: array [lon] + :type lon_in: array [lon] :param lev_in: self.lev variable - :type lev_in: array [lev] + :type lev_in: array [lev] :param ftod_in: self.ftod variable - :type ftod_in: array [tod] + :type ftod_in: array [tod] :return: ``varfull`` object without brackets (e.g., ``atmos_average.temp``); - :return: (t_out) dimension to update; - :return: (lat_out) dimension to update; - :return: (lon_out) dimension to update; - :return: (lev_out) dimension to update; - :return: (ftod_out) dimension to update; + :return: (t_out) dimension to update; + :return: (lat_out) dimension to update; + :return: (lon_out) dimension to update; + :return: (lev_out) dimension to update; + :return: (ftod_out) dimension to update; + :rtype: str + :raises ValueError: If the input varfull_bracket is not a valid + type for variable extraction. + :raises TypeError: If the input t_in, lat_in, lon_in, lev_in, + ftod_in are not valid types for variable extraction. + :raises Exception: If the variable extraction fails for any reason. + + .. note:: This function is used for 1D plots only. The function + will return the new dimensions that will overwrite the default + dimensions for the ``varfull`` object. The function will also + return the required file and variable (e.g., + ``atmos_average.temp``) and the X and Y axis dimensions for the + plot. """ # Initialization: Use dimension provided in template @@ -1487,15 +1621,23 @@ def fig_layout(subID, nPan, vertical_page=False): Return figure layout. :param subID: current subplot number - :type subID: int + :type subID: int :param nPan: number of panels desired on page (max = 64, 8x8) - :type nPan: int + :type nPan: int :param vertical_page: reverse the tuple for portrait format if ``True`` - :type vertical_page: bool + :type vertical_page: bool :return: plot layout (e.g., ``plt.subplot(nrows = out[0], ncols = out[1], plot_number = out[2])``) - :rtype: tuple + :rtype: tuple + :raises ValueError: If the input subID is not a valid type for + subplot number. + :raises TypeError: If the input nPan is not a valid type for + subplot number. + :raises Exception: If the input vertical_page is not a valid type + for subplot number. + :raises Exception: If the figure layout calculation fails for any + reason. """ out = list((0, 0, 0)) @@ -1548,6 +1690,13 @@ def make_template(): Generate the ``Custom.in`` template file. :return: Custom.in blank template + :rtype: file + :raises ValueError: If the input customFileIN is not a valid type + for template generation. + :raises TypeError: If the input customFileIN is not a valid type + for template generation. + :raises Exception: If the template generation fails for any + reason. """ global customFileIN # Will be modified @@ -1672,7 +1821,9 @@ def give_permission(filename): Sets group permissions for files created on NAS. :param filename: name of the file - :type filename: str + :type filename: str + :raises ValueError: If the input filename is not a valid type + for file name. """ # NAS system only: set group permissions to file @@ -1692,8 +1843,12 @@ def namelist_parser(Custom_file): Parse a ``Custom.in`` template. :param Custom_file: full path to ``Custom.in`` file - :type Custom_file: str + :type Custom_file: str :return: updated global variables, ``FigLayout``, ``objectList`` + ``panelList``, ``subplotList``, ``addLineList``, ``layoutList`` + :rtype: list + :raises ValueError: If the input Custom_file is not a valid type + for file name. """ global objectList @@ -1874,12 +2029,18 @@ def get_figure_header(line_txt): :param line_txt: template header from Custom.in (e.g., ``<<<<<<<<<| Plot 2D lon X lat = True |>>>>>>>>``) - :type line_txt: str + :type line_txt: str :return: (figtype) figure type (e.g., ``Plot 2D lon X lat``) - :rtype: str + :rtype: str :return: (boolPlot) whether to plot (``True``) or skip (``False``) figure - :rtype: bool + :rtype: bool + :raises ValueError: If the input line_txt is not a valid type for + figure header. + :raises TypeError: If the input line_txt is not a valid type for + figure header. + :raises Exception: If the figure header parsing fails for any + reason. """ # Plot 2D lon X lat = True @@ -1897,11 +2058,16 @@ def format_lon_lat(lon_lat, type): 45°E) :param lon_lat: latitude or longitude (+180/-180) - :type lon_lat: float + :type lon_lat: float :param type: ``lat`` or ``lon`` - :type type: str + :type type: str :return: formatted label - :rtype: str + :rtype: str + :raises ValueError: If the input lon_lat is not a valid type for + latitude or longitude. + :raises TypeError: If the input type is not a valid type for + latitude or longitude. + :raises Exception: If the formatting fails for any reason. """ letter = "" @@ -1929,7 +2095,9 @@ def get_Ncdf_num(): Requires at least one ``fixed`` file in the directory. :return: a sorted array of sols - :rtype: array + :rtype: array + :raises ValueError: If the input input_paths is not a valid type + for file name. """ # e.g., 00350.fixed.nc @@ -1950,11 +2118,16 @@ def select_range(Ncdf_num, bound): within the user-defined range. :param Ncdf_num: a sorted array of sols - :type Ncdf_num: array + :type Ncdf_num: array :param bound: a sol (e.g., 0350) or range of sols ``[min max]`` - :type bound: int or array + :type bound: int or array :return: a sorted array of sols within the bounds - :rtype: array + :rtype: array + :raises ValueError: If the input Ncdf_num is not a valid type for + file name. + :raises TypeError: If the input bound is not a valid type for + file name. + :raises Exception: If the range selection fails for any reason. """ bound = np.array(bound) @@ -1981,10 +2154,16 @@ def create_name(root_name): :param root_name: path + default name for the file type (e.g., ``/path/custom.in`` or ``/path/figure.png``) - :type root_name: str + :type root_name: str :return: the modified name if the file already exists (e.g., ``/path/custom_01.in`` or ``/path/figure_01.png``) - :rtype: str + :rtype: str + :raises ValueError: If the input root_name is not a valid type + for file name. + :raises TypeError: If the input root_name is not a valid type + for file name. + :raises Exception: If the file name creation fails for any + reason. """ n = 1 @@ -2008,10 +2187,17 @@ def progress(k, Nmax, txt="", success=True): Display a progress bar when performing heavy calculations. :param k: current iteration of the outer loop - :type k: float + :type k: float :param Nmax: max iteration of the outer loop - :type Nmax: float + :type Nmax: float :return: progress bar (EX: ``Running... [#---------] 10.64 %``) + :rtype: str + :raises ValueError: If the input k is not a valid type for + progress bar. + :raises TypeError: If the input Nmax is not a valid type for + progress bar. + :raises Exception: If the progress bar creation fails for any + reason. """ progress = float(k)/Nmax @@ -2033,23 +2219,31 @@ def progress(k, Nmax, txt="", success=True): def prep_file(var_name, file_type, simuID, sol_array): """ Open the file as a Dataset or MFDataset object depending on its - status on Lou. Note that the input arguments are typically - extracted from a ``varfull`` object (e.g., - ``03340.atmos_average.ucomp``) and not from a file whose disk - status is known beforehand. + status on Lou. Note that the input arguments are typically + extracted from a ``varfull`` object (e.g., + ``03340.atmos_average.ucomp``) and not from a file whose disk status + is known beforehand. :param var_name: variable to extract (e.g., ``ucomp``) - :type var_name: str + :type var_name: str :param file_type: MGCM output file type (e.g., ``average``) - :type file_name: str + :type file_name: str :param simuID: simulation ID number (e.g., 2 for 2nd simulation) - :type simuID: int + :type simuID: int :param sol_array: date in file name (e.g., [3340,4008]) - :type sol_array: list + :type sol_array: list :return: Dataset or MFDataset object; - (var_info) longname and units; - (dim_info) dimensions e.g., (``time``, ``lat``,``lon``); - (dims) shape of the array e.g., [133,48,96] + :return: (var_info) longname and units; + :return: (dim_info) dimensions e.g., (``time``, ``lat``,``lon``); + :return: (dims) shape of the array e.g., [133,48,96] + :rtype: Dataset or MFDataset object, str, tuple, list + :raises ValueError: If the input var_name is not a valid type + for variable name. + :raises TypeError: If the input file_type is not a valid type + for file type. + :raises Exception: If the file preparation fails for any + reason. + :raises IOError: If the file is not found or cannot be opened. """ global input_paths @@ -2116,6 +2310,34 @@ def __call__(self, x, pos=None): # FIGURE DEFINITIONS # ====================================================================== class Fig_2D(object): + """ + Base class for 2D figures. This class is not intended to be + instantiated directly. Instead, it is used as a base class for + specific 2D figure classes (e.g., ``Fig_2D_lon_lat``, ``Fig_2D_time_lat``, + ``Fig_2D_lat_lev``, etc.). It provides common attributes and methods + for all 2D figures, such as the variable name, file type, simulation + ID, and plotting options. The class also includes methods for + creating a template for the figure, reading the template from a + file, and loading data for 2D plots. The class is designed to be + extended by subclasses that implement specific plotting + functionality for different types of 2D figures. + + :param varfull: full variable name (e.g., ``fileYYY.XXX``) + :type varfull: str + :param doPlot: whether to plot the figure (default: ``False``) + :type doPlot: bool + :param varfull2: second variable name (default: ``None``) + :type varfull2: str + :return: None + :rtype: None + :raises ValueError: If the input varfull is not a valid type + for variable name. + :raises TypeError: If the input doPlot is not a valid type + for plotting. + :raises Exception: If the input varfull2 is not a valid type + for variable name. + """ + def __init__(self, varfull="fileYYY.XXX", doPlot=False, varfull2=None): self.title = None @@ -2682,9 +2904,75 @@ def solid_contour(self, xdata, ydata, var, contours): class Fig_2D_lon_lat(Fig_2D): + """ + Fig_2D_lon_lat is a class for creating 2D longitude-latitude plots. + + Fig_2D_lon_lat is a subclass of Fig_2D designed for generating 2D + plots of longitude versus latitude, primarily for visualizing Mars + climate data. It provides methods for figure creation, data loading, + plotting, and overlaying topography contours, with support for + various map projections and customization options. + + Attributes: + varfull (str): Full variable name (e.g., "fileYYY.XXX") to plot. + doPlot (bool): Whether to plot the figure (default: False). + varfull2 (str, optional): Second variable name for overlaying + contours (default: None). + plot_type (str): Type of plot (default: "2D_lon_lat"). + fdim1 (str, optional): First free dimension (default: None). + fdim2 (str, optional): Second free dimension (default: None). + ftod (str, optional): Time of day (default: None). + axis_opt1 (str, optional): First axis option, e.g., colormap + (default: None). + axis_opt2 (str, optional): Second axis option (default: None). + axis_opt3 (str, optional): Projection type (e.g., "cart", + "robin", "moll", "Npole", "Spole", "ortho"). + Xlim (tuple, optional): Longitude axis limits. + Ylim (tuple, optional): Latitude axis limits. + range (bool, optional): Whether to use a specified range for + color levels. + contour2 (float or list, optional): Contour levels for the + second variable. + title (str, optional): Custom plot title. + nPan (int): Number of panels (for multi-panel plots). + fdim_txt (str): Text describing free dimensions. + success (bool): Status flag indicating if plotting succeeded. + + Methods: + make_template(): + Sets up the plot template with appropriate axis labels and + titles. + + get_topo_2D(varfull, plot_type): + Loads and returns topography data (zsurf) for overlaying as + contours, matching the simulation and file type of the main variable. + + do_plot(): + Main plotting routine. Loads data, applies projection, + overlays topography and optional second variable contours, + customizes axes, and saves the figure. Handles both + standard and special map projections (cartesian, Robinson, + Mollweide, polar, orthographic). + + Usage: + This class is intended to be used within the MarsPlot software + for visualizing Mars climate model outputs as longitude-latitude + maps, with optional overlays and advanced projection support. + """ # Make_template calls method from parent class def make_template(self): + """ + Creates and configures a plot template for 2D longitude vs latitude data. + This method calls the parent class's `make_template` method with predefined + parameters to set up the plot title and axis labels specific to a 2D longitude-latitude plot. + The template includes: + - Title: "Plot 2D lon X lat" + - X-axis label: "Ls 0-360" + - Y-axis label: "Level Pa/m" + - Additional axis labels: "Lon" (longitude), "Lat" (latitude) + """ + super(Fig_2D_lon_lat, self).make_template( "Plot 2D lon X lat", "Ls 0-360", "Level Pa/m", "Lon", "Lat") @@ -2702,10 +2990,10 @@ def get_topo_2D(self, varfull, plot_type): :param varfull: variable input to main_variable in Custom.in (e.g., ``03340.atmos_average.ucomp``) - :type varfull: str + :type varfull: str :param plot_type: plot type (e.g., ``Plot 2D lon X time``) - :type plot_type: str + :type plot_type: str :return: topography or ``None`` if no matching ``fixed`` file """ @@ -2740,6 +3028,34 @@ def get_topo_2D(self, varfull, plot_type): def do_plot(self): + """ + Generate a 2D longitude-latitude plot with various projection options and optional overlays. + + This method creates a 2D plot of a variable (and optionally a second variable as contours) + on a longitude-latitude grid. It supports multiple map projections, including cartesian, + Robinson, Mollweide, and azimuthal (north pole, south pole, orthographic) projections. + Topography contours can be added if available. The method handles axis formatting, + colorbars, titles, and annotation of meridians and parallels. + + The plotting behavior is controlled by instance attributes such as: + - self.varfull: Main variable to plot. + - self.varfull2: Optional second variable for contour overlay. + - self.plot_type: Type of plot to generate. + - self.axis_opt1: Colormap or colormap option. + - self.axis_opt3: Projection type. + - self.contour2: Contour levels for the second variable. + - self.Xlim, self.Ylim: Axis limits for cartesian projection. + - self.range: Whether to use a specific range for color levels. + - self.title: Custom plot title. + - self.fdim_txt: Additional dimension text for the title. + - self.nPan: Panel index for multi-panel plots. + + The method handles exceptions and saves the figure upon completion. + + Raises: + Exception: Any error encountered during plotting is handled and reported. + """ + # Create figure ax = super(Fig_2D_lon_lat, self).fig_init() try: @@ -3090,9 +3406,54 @@ def do_plot(self): class Fig_2D_time_lat(Fig_2D): + """ + A 2D plotting class for visualizing data as a function of time (Ls) + and latitude. Inherits from: Fig_2D + + Methods: + make_template(): + Sets up the plot template with appropriate titles and axis + labels for a 2D time vs latitude plot. + do_plot(): + Loads 2D data (time and latitude), creates a filled contour + plot of the primary variable, and optionally overlays a + solid contour of a secondary variable. + Formats axes, customizes tick labels to show both Ls and + sol time (if enabled), and applies axis limits if specified. + Handles exceptions during plotting and saves the resulting + figure. + + Attributes (inherited and used): + varfull : str + Name of the primary variable to plot. + varfull2 : str or None + Name of the secondary variable to overlay as contours + (optional). + plot_type : str + Type of plot/data to load. + Xlim : tuple or None + Limits for the x-axis (sol time). + Ylim : tuple or None + Limits for the y-axis (latitude). + contour2 : list or None + Contour levels for the secondary variable. + nPan : int + Number of panels (used for label sizing). + success : bool + Indicates if the plot was successfully created. + """ def make_template(self): - # Calls method from parent class + """ + Creates and configures a plot template for a 2D time versus latitude figure. + This method calls the superclass's `make_template` method with predefined + titles and axis labels suitable for a plot displaying data across longitude, + level, solar longitude (Ls), and latitude. + + Returns: + None + """ + super(Fig_2D_time_lat, self).make_template("Plot 2D time X lat", "Lon +/-180", "Level [Pa/m]", @@ -3100,7 +3461,20 @@ def make_template(self): def do_plot(self): - # Create figure + """ + Generates a 2D time-latitude plot for the specified variable(s). + This method initializes the figure, loads the required 2D data arrays (time and latitude), + and creates a filled contour plot of the primary variable. If a secondary variable is specified, + it overlays solid contours for that variable. The method also formats the axes, including + custom tick labels for solar longitude (Ls) and optionally sol time, and applies axis limits + if specified. Additional plot formatting such as tick intervals and font sizes are set. + The plot is saved at the end of the method. Any exceptions encountered during plotting + are handled and reported. + + Raises: + Exception: If any error occurs during the plotting process, it is handled and reported. + """ + ax = super(Fig_2D_time_lat, self).fig_init() try: # Try to create figure, else return error @@ -3166,16 +3540,84 @@ def do_plot(self): class Fig_2D_lat_lev(Fig_2D): + """ + A subclass of Fig_2D for generating 2D plots with latitude and + vertical level (pressure or altitude) axes. + + This class customizes the plotting template and plotting logic for + visualizing data as a function of latitude and vertical level. + It supports filled contour plots for a primary variable, and + optionally overlays solid contour lines for a secondary variable. + + Methods: + make_template(): + Sets up the plot template with appropriate titles and axis + labels for latitude vs. level plots. + + do_plot(): + Loads data, creates a filled contour plot of the primary + variable, optionally overlays contours of a secondary + variable, configures axis scaling and formatting (including + logarithmic pressure axis if needed), sets axis limits and + tick formatting, handles exceptions, and saves the resulting + figure. + + Attributes (inherited and/or used): + varfull : str + Name of the primary variable to plot. + varfull2 : str or None + Name of the secondary variable to overlay as contours, if + any. + plot_type : str + Type of plot or data selection. + vert_unit : str + Unit for the vertical axis ("Pa" for pressure, otherwise + altitude in meters). + Xlim : tuple or None + Limits for the x-axis (latitude). + Ylim : tuple or None + Limits for the y-axis (level). + contour2 : list or None + Contour levels for the secondary variable. + nPan : int + Number of panels in the plot (affects tick label size). + success : bool + Indicates if the plot was successfully created. + """ def make_template(self): - # Calls method from parent class - super(Fig_2D_lat_lev, self).make_template("Plot 2D lat X lev", - "Ls 0-360 ", "Lon +/-180", - "Lat", "Level[Pa/m]") + """ + Creates and configures a plot template for a 2D latitude versus level plot. + This method calls the parent class's `make_template` method with predefined + titles and axis labels suitable for a plot displaying latitude against atmospheric + level data. + The plot is labeled as "Plot 2D lat X lev" with the following axis labels: + - X-axis: "Ls 0-360 " + - Y-axis: "Lon +/-180" + - Additional axes: "Lat", "Level[Pa/m]" + Returns: + None + """ + + super(Fig_2D_lat_lev, self).make_template( + "Plot 2D lat X lev", "Ls 0-360 ", "Lon +/-180", "Lat", "Level[Pa/m]" + ) def do_plot(self): - # Create figure + """ + Generates a 2D latitude-level plot for the specified variable(s). + This method initializes the figure, loads the required data, and creates a filled contour plot + of the primary variable. If a secondary variable is specified, it overlays solid contours for + that variable. The y-axis is set to logarithmic scale and inverted if the vertical unit is pressure. + Axis limits, labels, and tick formatting are applied as specified by the instance attributes. + The plot title is generated based on the variable information. Handles exceptions during plotting + and saves the resulting figure. + + Raises: + Exception: Any exception encountered during plotting is handled and logged. + """ + ax = super(Fig_2D_lat_lev, self).fig_init() try: # Try to create figure, else return error @@ -3224,10 +3666,42 @@ def do_plot(self): class Fig_2D_lon_lev(Fig_2D): + """ + A subclass of Fig_2D for generating 2D plots with longitude and + vertical level (pressure or altitude) axes. + + This class customizes the template and plotting routines to + visualize data as a function of longitude and vertical level. + It supports plotting filled contours for a primary variable and + optional solid contours for a secondary variable. + The vertical axis can be displayed in pressure (Pa, logarithmic + scale) or altitude (m). + + Methods: + make_template(): + Sets up the plot template with appropriate titles and axis + labels for longitude vs. level plots. + + do_plot(): + Loads data, applies longitude shifting, creates filled and + optional solid contour plots, + configures axis scales and labels, and handles exceptions + during plotting. + """ def make_template(self): """ - Calls method from parent class + Creates and configures a plot template for 2D lon x lev data. + + This method sets up the plot with predefined titles and axis + labels: + - Title: "Plot 2D lon X lev" + - X-axis: "Ls 0-360" + - Y-axis: "Latitude" + - Additional labels: "Lon +/-180" and "Level[Pa/m]" + + Overrides the base class method to provide specific + configuration for this plot type. """ super(Fig_2D_lon_lev, self).make_template("Plot 2D lon X lev", @@ -3237,9 +3711,25 @@ def make_template(self): def do_plot(self): """ - Create figure + Generates a 2D plot of a variable as a function of longitude + and vertical level (pressure or altitude). + + This method initializes the figure, loads the required data, + applies longitude shifting, and creates filled and/or solid + contour plots. + + It handles plotting of a secondary variable if specified, sets + axis scales and labels based on the vertical coordinate unit, + applies axis limits if provided, customizes tick formatting and + font sizes, and manages exceptions during plotting. + The resulting figure is saved to file. + + Raises: + Exception: If any error occurs during the plotting process, + it is handled and logged by the exception handler. """ + ax = super(Fig_2D_lon_lev, self).fig_init() try: # Try to create figure, else return error @@ -3291,15 +3781,91 @@ def do_plot(self): class Fig_2D_time_lev(Fig_2D): + """ + A specialized 2D plotting class for visualizing data as a function + of time (Ls) and vertical level (pressure or altitude). + + Inherits from: Fig_2D + + Methods: + make_template(): + Sets up the plot template with appropriate axis labels and + titles for 2D time vs. level plots. + + do_plot(): + Loads data and generates a filled contour plot of the + primary variable as a function of solar longitude (Ls) and + vertical level. Optionally overlays a solid contour of a + secondary variable. + Handles axis formatting, tick labeling (including optional + sol time axis), and y-axis scaling (logarithmic for + pressure). Sets plot titles and saves the figure. Catches + and handles exceptions during plotting. + + Attributes (inherited and/or used): + varfull : str + Name of the primary variable to plot. + varfull2 : str or None + Name of the secondary variable to overlay as contours + (optional). + plot_type : str + Type of plot/data selection. + Xlim : tuple or None + Limits for the x-axis (solar day). + Ylim : tuple or None + Limits for the y-axis (vertical level). + vert_unit : str + Unit for the vertical axis ("Pa" for pressure or other for + altitude). + nPan : int + Number of panels/subplots (affects label size). + contour2 : list or None + Contour levels for the secondary variable. + success : bool + Indicates if the plot was successfully generated. + """ def make_template(self): - # Calls method from parent class + """ + Creates and configures a plot template for 2D time versus level visualization. + This method calls the superclass's `make_template` method with predefined + titles and axis labels suitable for plotting data with latitude, longitude, + solar longitude (Ls), and atmospheric level (in Pa/m). + + Returns: + None + """ + super(Fig_2D_time_lev, self).make_template("Plot 2D time X lev", "Latitude", "Lon +/-180", "Ls", "Level[Pa/m]") def do_plot(self): - # Create figure + """ + Generates a 2D time-level plot for Mars atmospheric data. + + This method initializes the figure, loads the required data, and creates a filled contour plot + of the primary variable over solar longitude (Ls) and pressure or altitude. If a secondary variable + is specified, it overlays solid contours for that variable. The method also formats axes, applies + custom tick labels (optionally including sol time), and adjusts axis scales and labels based on + the vertical unit (pressure or altitude). The plot is titled and saved to file. + Handles exceptions by invoking a custom exception handler and always attempts to save the figure. + + Attributes used: + varfull (str): Name of the primary variable to plot. + plot_type (str): Type of plot/data to load. + varfull2 (str, optional): Name of the secondary variable for contour overlay. + contour2 (list, optional): Contour levels for the secondary variable. + Xlim (tuple, optional): Limits for the x-axis (solar day). + Ylim (tuple, optional): Limits for the y-axis (pressure or altitude). + vert_unit (str): Vertical axis unit, either "Pa" for pressure or other for altitude. + nPan (int): Number of panels (affects label size). + success (bool): Set to True if plotting succeeds. + + Raises: + Handles all exceptions internally and logs them via a custom handler. + """ + ax = super(Fig_2D_time_lev, self).fig_init() try: # Try to create figure, else return error @@ -3370,6 +3936,26 @@ def do_plot(self): class Fig_2D_lon_time(Fig_2D): + """ + A specialized 2D plotting class for visualizing data as a function + of longitude and time (Ls). + + Inherits from: Fig_2D + + Methods: + make_template(): + Sets up the plot template with appropriate titles and axis + labels for longitude vs. time plots. + + do_plot(): + Generates a 2D plot with longitude on the x-axis and solar + longitude (Ls) on the y-axis. + Loads and processes data, applies shifting if necessary, + and creates filled and/or solid contours. + Handles axis formatting, tick labeling (including optional + sol time annotation), and plot saving. + Catches and handles exceptions during plotting. + """ def make_template(self): # Calls method from parent class @@ -3444,6 +4030,92 @@ def do_plot(self): class Fig_1D(object): + """ + Fig_1D is a parent class for generating and handling 1D plots of + Mars atmospheric data. + + Attributes: + title : str + Title of the plot. + legend : str + Legend label for the plot. + varfull : str + Full variable specification, including file and variable + name. + t : str or float + Time axis or identifier for the varying dimension. + lat : float or str + Latitude value or identifier. + lon : float or str + Longitude value or identifier. + lev : float or str + Vertical level value or identifier. + ftod : float or str + Time of day requested. + hour : float or str + Hour of day, used for diurnal plots. + doPlot : bool + Whether to generate the plot. + plot_type : str + Type of 1D plot (e.g., "1D_time", "1D_lat"). + sol_array : str + Sol array extracted from varfull. + filetype : str + File type extracted from varfull. + var : str + Variable name extracted from varfull. + simuID : str + Simulation ID extracted from varfull. + nPan : int + Number of panels in the plot. + subID : int + Subplot ID. + addLine : bool + Whether to add a line to an existing plot. + layout : list or None + Page layout for multipanel plots. + fdim_txt : str + Annotation for free dimensions. + success : bool + Indicates if the plot was successfully created. + vert_unit : str + Vertical unit, either "m" or "Pa". + Dlim : list or None + Dimension limits for the axis. + Vlim : list or None + Variable limits for the axis. + axis_opt1 : str + Line style or axis option. + axis_opt2 : str + Additional axis option (optional). + + Methods: + make_template(): + Writes a template for the plot configuration to a file. + read_template(): + Reads plot configuration from a template file. + get_plot_type(): + Determines the type of 1D plot to create based on which + dimension is set to "AXIS" or -88888. + data_loader_1D(varfull, plot_type): + Loads 1D data for plotting, handling variable expressions + and dimension overwrites. + read_NCDF_1D(var_name, file_type, simuID, sol_array, plot_type, + t_req, lat_req, lon_req, lev_req, ftod_req): + Reads and processes 1D data from a NetCDF file for the + specified variable and dimensions. + exception_handler(e, ax): + Handles exceptions during plotting, displaying an error + message on the plot. + fig_init(): + Initializes the figure and subplot for plotting. + fig_save(): + Saves the generated figure to disk. + do_plot(): + Main method to generate the 1D plot, handling all plotting + logic and exceptions. + """ + # Parent class for 1D figure def __init__(self, varfull="atmos_average.ts", doPlot=True): @@ -3648,28 +4320,28 @@ def read_NCDF_1D(self, var_name, file_type, simuID, sol_array, plot. :param var_name: variable name (e.g., ``temp``) - :type var_name: str + :type var_name: str :param file_type: MGCM output file type. Must be ``fixed`` or ``average`` - :type file_type: str + :type file_type: str :param simuID: number identifier for netCDF file directory - :type simuID: str + :type simuID: str :param sol_array: sol if different from default (e.g., ``02400``) - :type sol_array: str + :type sol_array: str :param plot_type: ``1D_lon``, ``1D_lat``, ``1D_lev``, or ``1D_time`` - :type plot_type: str + :type plot_type: str :param t_req: Ls requested - :type t_req: str + :type t_req: str :param lat_req: lat requested - :type lat_req: str + :type lat_req: str :param lon_req: lon requested - :type lon_req: str + :type lon_req: str :param lev_req: level [Pa/m] requested - :type lev_req: str + :type lev_req: str :param ftod_req: time of day requested - :type ftod_req: str + :type ftod_req: str :return: (dim_array) the axis (e.g., an array of longitudes), (var_array) the variable extracted """ diff --git a/bin/MarsPull.py b/bin/MarsPull.py index 4f5d4f1..4b3b674 100755 --- a/bin/MarsPull.py +++ b/bin/MarsPull.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -The MarsPull executable is for querying data from the Mars Climate \ -Modeling Center (MCMC) Mars Global Climate Model (MGCM) repository on \ +The MarsPull executable is for querying data from the Mars Climate +Modeling Center (MCMC) Mars Global Climate Model (MGCM) repository on the NASA NAS Data Portal at data.nas.nasa.gov/mcmc. The executable requires 2 arguments: @@ -20,15 +20,11 @@ * ``functools`` * ``traceback`` * ``requests`` - -List of Functions: - - * download - Queries the requested file from the NAS Data Portal. """ # make print statements appear in color from amescap.Script_utils import ( - prYellow, prCyan, Green, Yellow, Nclr, Cyan, Blue, Red + Green, Yellow, Nclr, Cyan, Blue, Red ) # Load generic Python modules @@ -44,8 +40,38 @@ def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -56,12 +82,12 @@ def wrapper(*args, **kwargs): except Exception as e: if debug: # In debug mode, show the full traceback - print(f"{Red}ERROR in {func.__name__}: {str(e)}{Nclr}") + print(f'{Red}ERROR in {func.__name__}: {str(e)}{Nclr}') traceback.print_exc() else: # In normal mode, show a clean error message - print(f"{Red}ERROR in {func.__name__}: {str(e)}\nUse " - f"--debug for more information.{Nclr}") + print(f'{Red}ERROR in {func.__name__}: {str(e)}\nUse ' + f'--debug for more information.{Nclr}') return 1 # Error exit code return wrapper @@ -73,11 +99,11 @@ def wrapper(*args, **kwargs): parser = argparse.ArgumentParser( prog=('MarsPull'), description=( - f"{Yellow}Uility for downloading NASA Ames Mars Global Climate " - f"Model output files from the NAS Data Portal at:\n" - f"{Cyan}https://data.nas.nasa.gov/mcmcref/\n{Nclr}" - f"Requires ``-f`` or ``-ls``." - f"{Nclr}\n\n" + f'{Yellow}Uility for downloading NASA Ames Mars Global Climate ' + f'Model output files from the NAS Data Portal at:\n' + f'{Cyan}https://data.nas.nasa.gov/mcmcref/\n{Nclr}' + f'Requires ``-f`` or ``-ls``.' + f'{Nclr}\n\n' ), formatter_class=argparse.RawTextHelpFormatter ) @@ -87,49 +113,50 @@ def wrapper(*args, **kwargs): 'FV3BETAOUT1', 'ACTIVECLDS', 'INERTCLDS', 'NEWBASE_ACTIVECLDS', 'ACTIVECLDS_NCDF'], help=( - f"Selects the simulation directory from the " - f"NAS data portal (" - f"{Cyan}https://data.nas.nasa.gov/mcmcref/){Nclr}\n" - f"Current directory options are:\n{Yellow}FV3BETAOUT1, ACTIVECLDS, " - f"ACTIVECLDS, INERTCLDS, NEWBASE_ACTIVECLDS, ACTIVECLDS_NCDF\n" - f"{Red}MUST be used with either ``-f`` or ``-ls``\n" - f"{Green}Example:\n" - f"> MarsPull INERTCLDS -f fort.11_0690\n" - f"{Blue}OR{Green}\n" - f"> MarsPull INERTCLDS -ls 90\n" - f"{Nclr}\n\n" + f'Selects the simulation directory from the ' + f'NAS data portal (' + f'{Cyan}https://data.nas.nasa.gov/mcmcref/){Nclr}\n' + f'Current directory options are:\n{Yellow}FV3BETAOUT1, ACTIVECLDS, ' + f'ACTIVECLDS, INERTCLDS, NEWBASE_ACTIVECLDS, ACTIVECLDS_NCDF\n' + f'{Red}MUST be used with either ``-f`` or ``-ls``\n' + f'{Green}Example:\n' + f'> MarsPull INERTCLDS -f fort.11_0690\n' + f'{Blue}OR{Green}\n' + f'> MarsPull INERTCLDS -ls 90\n' + f'{Nclr}\n\n' ) ) parser.add_argument('-list', '--list_files', action='store_true', help=( - f"Return a list of all the files available for download from " - f"{Cyan}https://data.nas.nasa.gov/mcmcref/{Nclr}\n" - f"{Green}Example:\n" - f"> MarsPull -list" - f"{Nclr}\n\n" + f'Return a list of the directories and files available for download ' + f'from {Cyan}https://data.nas.nasa.gov/mcmcref/{Nclr}\n' + f'{Green}Example:\n' + f'> MarsPull -list {Blue}# lists all directories{Green}\n' + f'> MarsPull -list ACTIVECLDS {Blue}# lists files under ACTIVECLDS ' + f'{Nclr}\n\n' ) ) parser.add_argument('-f', '--filename', nargs='+', type=str, help=( - f"The name(s) of the file(s) to download.\n" - f"{Green}Example:\n" - f"> MarsPull INERTCLDS -f fort.11_0690" - f"{Nclr}\n\n" + f'The name(s) of the file(s) to download.\n' + f'{Green}Example:\n' + f'> MarsPull INERTCLDS -f fort.11_0690' + f'{Nclr}\n\n' ) ) parser.add_argument('-ls', '--ls', nargs='+', type=float, help=( - f"Selects the file(s) to download based on a range of solar " - f"longitudes (Ls).\n" - f"This only works on data in the {Yellow}ACTIVECLDS{Nclr} and " - f"{Yellow}INERTCLDS{Nclr} folders.\n" - f"{Green}Example:\n" - f"> MarsPull INERTCLDS -ls 90\n" - f"> MarsPull INERTCLDS -ls 90 180" - f"{Nclr}\n\n" + f'Selects the file(s) to download based on a range of solar ' + f'longitudes (Ls).\n' + f'This only works on data in the {Yellow}ACTIVECLDS{Nclr} and ' + f'{Yellow}INERTCLDS{Nclr} folders.\n' + f'{Green}Example:\n' + f'> MarsPull INERTCLDS -ls 90\n' + f'> MarsPull INERTCLDS -ls 90 180' + f'{Nclr}\n\n' ) ) @@ -137,11 +164,11 @@ def wrapper(*args, **kwargs): parser.add_argument('--debug', action='store_true', help=( - f"Use with any other argument to pass all Python errors and\n" - f"status messages to the screen when running CAP.\n" - f"{Green}Example:\n" - f"> MarsPull INERTCLDS -ls 90 --debug" - f"{Nclr}\n\n" + f'Use with any other argument to pass all Python errors and\n' + f'status messages to the screen when running CAP.\n' + f'{Green}Example:\n' + f'> MarsPull INERTCLDS -ls 90 --debug' + f'{Nclr}\n\n' ) ) @@ -152,7 +179,7 @@ def wrapper(*args, **kwargs): # ------------------------------------------------------ # DEFINITIONS # ------------------------------------------------------ -save_dir = (f"{os.getcwd()}/") +save_dir = (f'{os.getcwd()}/') # available files by Ls: Ls_ini = np.array([ @@ -175,17 +202,44 @@ def wrapper(*args, **kwargs): def download(url, file_name): """ Downloads a file from the NAS Data Portal (data.nas.nasa.gov). - - :param url: The url to download from, e.g 'https://data.nas.nasa.\ - gov/legacygcm/download_data.php?file=/legacygcmdata/LegacyGCM_\ - Ls000_Ls004.nc' - :type url: str - :param file_name: The local file_name e.g '/lou/la4/akling/Data/L\ - egacyGCM_Ls000_Ls004.nc' - :type file_name: str - :return: The requested file(s), downloaded and saved to the \ - current directory. + The function takes a URL and a file name as input, and downloads the + file from the URL, saving it to the specified file name. It also + provides a progress bar to show the download progress if the file + size is known. If the file size is unknown, it simply downloads the + file without showing a progress bar. + The function handles errors during the download process and prints + appropriate messages to the console. + + :param url: The url to download from, e.g., + 'https://data.nas.nasa.gov/legacygcm/fv3betaout1data/03340.fixed.nc' + :type url: str + :param file_name: The local file_name e.g., + '/lou/la4/akling/Data/LegacyGCM_Ls000_Ls004.nc' + :type file_name: str + :return: The requested file(s), downloaded and saved to the current + directory. + :rtype: None :raises FileNotFoundError: A file-not-found error. + :raises PermissionError: A permission error. + :raises OSError: An operating system error. + :raises ValueError: A value error. + :raises TypeError: A type error. + :raises requests.exceptions.RequestException: A request error. + :raises requests.exceptions.HTTPError: An HTTP error. + :raises requests.exceptions.ConnectionError: A connection error. + :raises requests.exceptions.Timeout: A timeout error. + :raises requests.exceptions.TooManyRedirects: A too many redirects + error. + :raises requests.exceptions.URLRequired: A URL required error. + :raises requests.exceptions.InvalidURL: An invalid URL error. + :raises requests.exceptions.InvalidSchema: An invalid schema error. + :raises requests.exceptions.MissingSchema: A missing schema error. + :raises requests.exceptions.InvalidHeader: An invalid header error. + :raises requests.exceptions.InvalidProxyURL: An invalid proxy URL + error. + :raises requests.exceptions.InvalidRequest: An invalid request error. + :raises requests.exceptions.InvalidResponse: An invalid response + error. """ _, fname = os.path.split(file_name) @@ -193,7 +247,8 @@ def download(url, file_name): total = response.headers.get('content-length') if response.status_code == 404: - print(f'Error during download, error code is: {response.status_code}') + print(f'{Red}Error during download, error code is: ' + f'{response.status_code}{Nclr}') else: if total is not None: # If file size is known, return progress bar @@ -207,19 +262,35 @@ def download(url, file_name): downloaded += len(data) f.write(data) status = int(50*downloaded/total) - sys.stdout.write(f"\r[{'#'*status}{'.'*(50 - status)}]") + sys.stdout.write( + f'\rProgress: ' + f'[{"#"*status}{"."*(50 - status)}] {status}%' + ) sys.stdout.flush() - sys.stdout.write('\n') + sys.stdout.write('\n\n') else: # If file size is unknown, skip progress bar - print(f"Downloading {fname}...") + print(f'Downloading {fname}...') with open(file_name, 'wb')as f: f.write(response.content) - print(f"{fname} Done") + print(f'{fname} Done') def print_file_list(list_of_files): - print("Available files:") + """ + Prints a list of files. + + :param list_of_files: The list of files to print. + :type list_of_files: list + :return: None + :rtype: None + :raises TypeError: If list_of_files is not a list. + :raises ValueError: If list_of_files is empty. + :raises IndexError: If list_of_files is out of range. + :raises KeyError: If list_of_files is not found. + :raises OSError: If list_of_files is not accessible. + :raises IOError: If list_of_files is not open. + """ for file in list_of_files: print(file) @@ -230,69 +301,242 @@ def print_file_list(list_of_files): @debug_wrapper def main(): - # validation + """ + The main function that handles the command-line arguments + + Handles the command-line arguments and coordinates the download + process. It checks for the presence of the required arguments, + validates the input, and calls the appropriate functions to download + the requested files. It also handles the logic for listing available + directories and files, as well as downloading files based on + specified solar longitudes (Ls) or file names. + + :return: 0 if successful, 1 if an error occurred. + :rtype: int + :raises SystemExit: If an error occurs during the execution of the + program, the program will exit with a non-zero status code. + """ + global debug + if not args.list_files and not args.directory_name: - print("Error: You must specify either -list or a directory.") + print('Error: You must specify either -list or a directory.') sys.exit(1) + base_dir = 'https://data.nas.nasa.gov' + legacy_home_url = f'{base_dir}/mcmcref/legacygcm/' + legacy_data_url = f'{base_dir}/legacygcm/legacygcmdata/' + fv3_home_url = f'{base_dir}/mcmcref/fv3betaout1/' + fv3_data_url = f'{base_dir}/legacygcm/fv3betaout1data/' + if args.list_files: # Send an HTTP GET request to the URL and store the response. - legacy_data = requests.get( - 'https://data.nas.nasa.gov/mcmcref/legacygcm/' - ) - fv3_data = requests.get( - 'https://data.nas.nasa.gov/mcmcref/fv3betaout1/' - ) + legacy_home_html = requests.get(f'{legacy_home_url}') + fv3_home_html = requests.get(f'{fv3_home_url}') # Access the text content of the response, which contains the # webpage's HTML. - legacy_dir_text = legacy_data.text - fv3_dir_text = fv3_data.text + legacy_dir_text = legacy_home_html.text + fv3_dir_text = fv3_home_html.text # Search for the URLs beginning with the below string - legacy_dir_search = ( - "https://data\.nas\.nasa\.gov/legacygcm/legacygcmdata/" + legacy_subdir_search = ( + 'https://data\.nas\.nasa\.gov/legacygcm/legacygcmdata/' + ) + fv3_subdir_search = ( + 'https://data\.nas\.nasa\.gov/legacygcm/fv3betaout1data/' ) legacy_urls = re.findall( - fr"{legacy_dir_search}[a-zA-Z0-9_\-\.~:/?#\[\]@!$&'()*+,;=]+", + fr'{legacy_subdir_search}[a-zA-Z0-9_\-\.~:/?#\[\]@!$&"()*+,;=]+', legacy_dir_text ) - print("Available directories:") + # NOTE: The FV3-based MGCM data only has one directory and it is + # not listed in the FV3BETAOUT1 directory. The URL is + # hardcoded below. The regex below is commented out, but + # left in place in case the FV3BETAOUT1 directory is + # updated with subdirectories in the future. + # fv3_urls = re.findall( + # fr'{fv3_subdir_search}[a-zA-Z0-9_\-\.~:/?#\[\]@!$&"()*+,;=]+', + # fv3_dir_text + # ) + fv3_urls = [f'{fv3_data_url}'] + + print(f'\nSearching for available directories...') + print(f'--------------------------------------') for url in legacy_urls: - dir_option = url.split('legacygcmdata/')[1] - print(dir_option) - - legacy_data = requests.get(legacy_urls[0]) - legacy_dir_text = legacy_data.text + legacy_dir_option = url.split('legacygcmdata/')[1] + print(f'{"(Legacy MGCM)":<17} {legacy_dir_option:<20} ' + f'{Cyan}{url}{Nclr}') + + # NOTE: See above comment for the FV3-based MGCM data note + # for url in fv3_urls: + # fv3_dir_option = url.split('fv3betaout1data/')[1] + # print(f'{"(FV3-based MGCM)":<17} {fv3_dir_option:<17} ' + # f'{Cyan}{url}{Nclr}') + print(f'{"(FV3-based MGCM)":<17} {"FV3BETAOUT1":<20} ' + f'{Cyan}{fv3_home_url}{Nclr}') + print(f'---------------------\n') + + if args.directory_name: + # If a directory is provided, list the files in that directory + portal_dir = args.directory_name + if portal_dir == 'FV3BETAOUT1': + # FV3-based MGCM + print(f'\n{Green}Selected: (FV3-based MGCM) FV3BETAOUT1{Nclr}') + print(f'\nSearching for available files...') + print(f'--------------------------------') + fv3_dir_url = f'{fv3_home_url}' + fv3_data = requests.get(fv3_dir_url) + fv3_file_text = fv3_data.text + + # This looks for download attributes or href links + # ending with the .nc pattern + fv3_files_available = [] + + # Try multiple patterns to find .nc files + download_files = re.findall( + r'download="([^"]+\.nc)"', + fv3_file_text + ) + if download_files: + fv3_files_available = download_files + else: + # Look for href attributes with .nc files + href_files = re.findall( + r'href="[^"]*\/([^"\/]+\.nc)"', + fv3_file_text + ) + if href_files: + fv3_files_available = href_files + else: + # Look for links with .nc text + link_files = re.findall( + r']*>([^<]+\.nc)', + fv3_file_text + ) + if link_files: + fv3_files_available = link_files + + # Filter out any potential HTML or Javascript that might + # match the pattern + fv3_files_available = [f for f in fv3_files_available if ( + not f.startswith('<') and + not f.startswith('function') and + not f.startswith('var') and + '.nc' in f + )] + + # Sort the files + fv3_files_available.sort() + + # Print the files + if fv3_files_available: + print_file_list(fv3_files_available) + else: + print('No .nc files found. Run with --debug for more info') + if debug: + # Try a different approach for debugging + table_rows = re.findall( + r'.*?', + fv3_file_text, + re.DOTALL + ) + for row in table_rows: + if '.nc' in row: + print(f'Debug - Found row with .nc: {row}') + + print(f'---------------') + # The download URL differs from the listing URL + print(f'{Cyan}({fv3_dir_url}){Nclr}\n') + + print(f'{Yellow}You can download files using the -f ' + f'option with the directory name, e.g.\n' + f'> MarsPull FV3BETAOUT1 -f 03340.fixed.nc\n' + f'> MarsPull FV3BETAOUT1 -f 03340.fixed.nc ' + f'03340.atmos_average.nc{Nclr}\n') + + elif portal_dir in [ + 'ACTIVECLDS', 'INERTCLDS', 'NEWBASE_ACTIVECLDS', + 'ACTIVECLDS_NCDF' + ]: + # Legacy MGCM + print(f'\n{Green}Selected: (Legacy MGCM) {portal_dir}{Nclr}') + print(f'\nAvailable files:') + print(f'---------------') + legacy_dir_url = (f'{legacy_data_url}' + portal_dir + r'/') + legacy_data = requests.get(legacy_dir_url) + legacy_file_text = legacy_data.text + + # This looks for download attributes or href links + # ending with the fort.11_ pattern + legacy_files_available = [] + + # First try to find download attributes which are more reliable + download_files = re.findall( + r'download="(fort\.11_[0-9]+)"', + legacy_file_text + ) + if download_files: + legacy_files_available = download_files + else: + # Fallback to looking for href links with fort.11_ pattern + href_files = re.findall( + r'href="[^"]*\/?(fort\.11_[0-9]+)"', + legacy_file_text + ) + if href_files: + legacy_files_available = href_files + # If still empty, try another pattern to match links + if not legacy_files_available: + href_files = re.findall( + r']*>(fort\.11_[0-9]+)', + legacy_file_text + ) + legacy_files_available = href_files + + print_file_list(legacy_files_available) + print(f'---------------') + print(f'{Cyan}({legacy_dir_url}){Nclr}\n') + + print(f'{Yellow}You can download these files using the ' + f'-f or -ls options with the directory name, e.g.\n' + f'> MarsPull ACTIVECLDS -f fort.11_0690\n' + f'> MarsPull ACTIVECLDS -f fort.11_0700 fort.11_0701 \n' + f'> MarsPull ACTIVECLDS -ls 90\n' + f'> MarsPull ACTIVECLDS -ls 90 180{Nclr}\n') - legacy_files_available = re.findall(r'download="(fort\.11_[0-9]+)"', - legacy_dir_text) - fv3_files_available = re.findall(r'href="[^"]*\/([^"\/]+\.nc)"', - fv3_dir_text) + else: + print(f'Error: Directory {portal_dir} does not exist.') + sys.exit(1) + sys.exit(0) - print_file_list(legacy_files_available) - print_file_list(fv3_files_available) + else: + # If no directory is provided, exit with an error + print(f'{Yellow}You can list the files in a directory by using ' + f'the -list option with a directory name, e.g.\n' + f'> MarsPull -list ACTIVECLDS{Nclr}\n') - if args.directory_name: - portal_dir=args.directory_name + if args.directory_name and not args.list_files: + portal_dir = args.directory_name if portal_dir in [ 'ACTIVECLDS', 'INERTCLDS', 'NEWBASE_ACTIVECLDS', 'ACTIVECLDS_NCDF' ]: - requested_url = ( - "https://data.nas.nasa.gov/legacygcm/legacygcmdata/" - + portal_dir - + "/" - ) + requested_url = (f'{legacy_data_url}' + portal_dir + '/') elif portal_dir in ['FV3BETAOUT1']: - requested_url = ( - "https://data.nas.nasa.gov/legacygcm/fv3betaout1data/" - ) + requested_url = (f'{fv3_data_url}') if not (args.ls or args.filename): - prYellow("ERROR No file requested. Use [-ls --ls] or " - "[-f --filename] to specify a file to download.") + print(f'{Yellow}ERROR No file requested. Use [-ls --ls] or ' + f'[-f --filename] to specify a file to download.{Nclr}') + sys.exit(1) # Return a non-zero exit code + portal_dir = args.directory_name + + if portal_dir == 'FV3BETAOUT1' and args.ls: + print(f'{Red}ERROR: The FV3BETAOUT1 directory does not support ' + f'[-ls --ls] queries. Please query by file name(s) ' + f'[-f --filename], e.g.\n' + f'> MarsPull FV3BETAOUT1 -f 03340.fixed.nc{Nclr}') sys.exit(1) # Return a non-zero exit code if args.ls: @@ -316,21 +560,21 @@ def main(): file_list = np.arange(i_start, i_end + 1) - prCyan(f"Saving {len(file_list)} file(s) to {save_dir}") - for ii in file_list: if portal_dir == 'ACTIVECLDS_NCDF': # Legacy .nc files file_name = ( - f"LegacyGCM_Ls{Ls_ini[ii]:03d}_Ls{Ls_end[ii]:03d}.nc" + f'LegacyGCM_Ls{Ls_ini[ii]:03d}_Ls{Ls_end[ii]:03d}.nc' ) else: # fort.11 files - file_name = f"fort.11_{670+ii:04d}" + file_name = f'fort.11_{670+ii:04d}' url = requested_url + file_name file_name = save_dir + file_name - print(f"Downloading {url}...") + print(f'\nDownloading {Cyan}{url}{Nclr}...') + print(f'Saving {Cyan}{len(file_list)}{Nclr} file(s) to ' + f'{Cyan}{save_dir}{Nclr}') download(url, file_name) elif args.filename: @@ -338,18 +582,19 @@ def main(): for f in requested_files: url = requested_url + f file_name = save_dir + f - print(f"Downloading {url}...") + print(f'\nDownloading {url}...') download(url, file_name) elif not args.list_files: # If no directory is provided and its not a -list request - prYellow("ERROR: A directory must be specified unless using -list.") + print(f'{Yellow}ERROR: A directory must be specified unless using ' + f'-list.{Nclr}') sys.exit(1) # ------------------------------------------------------ # END OF PROGRAM # ------------------------------------------------------ -if __name__ == "__main__": +if __name__ == '__main__': exit_code = main() sys.exit(exit_code) diff --git a/bin/MarsVars.py b/bin/MarsVars.py index 08428d8..fbcf552 100755 --- a/bin/MarsVars.py +++ b/bin/MarsVars.py @@ -85,8 +85,38 @@ def debug_wrapper(func): """ - A decorator that wraps a function with error handling based on the - --debug flag. + A decorator that wraps a function with error handling + based on the --debug flag. + If the --debug flag is set, it prints the full traceback + of any exception that occurs. Otherwise, it prints a + simplified error message. + + :param func: The function to wrap. + :type func: function + :return: The wrapped function. + :rtype: function + :raises Exception: If an error occurs during the function call. + :raises TypeError: If the function is not callable. + :raises ValueError: If the function is not found. + :raises NameError: If the function is not defined. + :raises AttributeError: If the function does not have the + specified attribute. + :raises ImportError: If the function cannot be imported. + :raises RuntimeError: If the function cannot be run. + :raises KeyError: If the function does not have the + specified key. + :raises IndexError: If the function does not have the + specified index. + :raises IOError: If the function cannot be opened. + :raises OSError: If the function cannot be accessed. + :raises EOFError: If the function cannot be read. + :raises MemoryError: If the function cannot be allocated. + :raises OverflowError: If the function cannot be overflowed. + :raises ZeroDivisionError: If the function cannot be divided by zero. + :raises StopIteration: If the function cannot be stopped. + :raises KeyboardInterrupt: If the function cannot be interrupted. + :raises SystemExit: If the function cannot be exited. + :raises AssertionError: If the function cannot be asserted. """ @functools.wraps(func) @@ -294,6 +324,15 @@ def wrapper(*args, **kwargs): def add_help(var_list): + """ + Create a help string for the add_variable argument. + + :param var_list: Dictionary of variables and their attributes + :type var_list: dict + :return: Formatted help string + :rtype: str + """ + help_text = (f"{'VARIABLE':9s} {'DESCRIPTION':33s} {'UNIT':11s} " f"{'REQUIRED VARIABLES':24s} {'SUPPORTED FILETYPES'}" f"\n{Cyan}") @@ -593,10 +632,19 @@ def add_help(var_list): def ensure_file_closed(filepath, delay=0.5): """ Try to ensure a file is not being accessed by the system. + This is especially helpful for Windows environments. :param filepath: Path to the file :param delay: Delay in seconds to wait for handles to release + :return: None + :rtype: None + :raises FileNotFoundError: If the file does not exist + :raises OSError: If the file is locked or cannot be accessed + :raises Exception: If any other error occurs + :raises TypeError: If the filepath is not a string + :raises ValueError: If the filepath is empty + :raises RuntimeError: If the file cannot be closed """ if not os.path.exists(filepath): @@ -629,6 +677,13 @@ def safe_remove_file(filepath, max_attempts=5, delay=1): :param max_attempts: Number of attempts to make :param delay: Delay between attempts in seconds :return: True if successful, False otherwise + :rtype: bool + :raises FileNotFoundError: If the file does not exist + :raises OSError: If the file is locked or cannot be accessed + :raises Exception: If any other error occurs + :raises TypeError: If the filepath is not a string + :raises ValueError: If the filepath is empty + :raises RuntimeError: If the file cannot be removed """ if not os.path.exists(filepath): @@ -661,13 +716,20 @@ def safe_remove_file(filepath, max_attempts=5, delay=1): def safe_move_file(src_file, dst_file, max_attempts=5, delay=1): """ - Safely move a file with retries for Windows file locking issues + Safely move a file with retries for Windows file locking issues. :param src_file: Source file path :param dst_file: Destination file path :param max_attempts: Number of attempts to make :param delay: Delay between attempts in seconds :return: True if successful, False otherwise + :rtype: bool + :raises FileNotFoundError: If the source file does not exist + :raises OSError: If the file is locked or cannot be accessed + :raises Exception: If any other error occurs + :raises TypeError: If the src_file or dst_file is not a string + :raises ValueError: If the src_file or dst_file is empty + :raises RuntimeError: If the file cannot be moved """ print(f"Moving file: {src_file} -> {dst_file}") @@ -731,7 +793,17 @@ def safe_move_file(src_file, dst_file, max_attempts=5, delay=1): # Helper function to handle Unicode output properly on Windows def safe_print(text): """ - Print text safely, handling encoding issues on Windows + Print text safely, handling encoding issues on Windows. + + :param text: Text to print + :type text: str + :return: None + :rtype: None + :raises UnicodeEncodeError: If the text cannot be encoded + :raises TypeError: If the text is not a string + :raises ValueError: If the text is empty + :raises Exception: If any other error occurs + :raises RuntimeError: If the text cannot be printed """ try: @@ -746,7 +818,21 @@ def safe_print(text): # Patch argparse.ArgumentParser._print_message to handle Unicode original_print_message = argparse.ArgumentParser._print_message def patched_print_message(self, message, file=None): - """Patched version of _print_message that handles Unicode encoding errors""" + """ + Patched version of _print_message that handles Unicode encoding errors. + + :param self: The ArgumentParser instance + :param message: The message to print + :param file: The file to print to (default is sys.stdout) + :type file: file-like object + :return: None + :rtype: None + :raises UnicodeEncodeError: If the message cannot be encoded + :raises TypeError: If the message is not a string + :raises ValueError: If the message is empty + :raises Exception: If any other error occurs + :raises RuntimeError: If the message cannot be printed + """ if file is None: file = sys.stdout @@ -766,10 +852,21 @@ def patched_print_message(self, message, file=None): # ==== IMPROVED FILE HANDLING FOR WINDOWS ==== def force_close_netcdf_files(file_or_dir, delay=1.0): """ - Aggressively try to ensure netCDF files are closed on Windows systems + Aggressively try to ensure netCDF files are closed on Windows systems. :param file_or_dir: Path to the file or directory to process :param delay: Delay in seconds after forcing closure + :return: None + :rtype: None + :raises FileNotFoundError: If the file or directory does not exist + :raises OSError: If the file is locked or cannot be accessed + :raises Exception: If any other error occurs + :raises TypeError: If the file_or_dir is not a string + :raises ValueError: If the file_or_dir is empty + :raises RuntimeError: If the file or directory cannot be processed + :raises ImportError: If the netCDF4 module is not available + :raises AttributeError: If the netCDF4 module does not have the required attributes + :raises Exception: If any other error occurs """ import gc @@ -788,7 +885,8 @@ def force_close_netcdf_files(file_or_dir, delay=1.0): def safe_copy_replace(src_file, dst_file, max_attempts=5, delay=1.0): """ - Windows-specific approach to copy file contents and replace destination + Windows-specific approach to copy file contents and replace destination. + This avoids move operations which are more likely to fail with locking :param src_file: Source file path @@ -796,6 +894,13 @@ def safe_copy_replace(src_file, dst_file, max_attempts=5, delay=1.0): :param max_attempts: Maximum number of retry attempts :param delay: Base delay between attempts (increases with retries) :return: True if successful, False otherwise + :rtype: bool + :raises FileNotFoundError: If the source file does not exist + :raises OSError: If the file is locked or cannot be accessed + :raises Exception: If any other error occurs + :raises TypeError: If the src_file or dst_file is not a string + :raises ValueError: If the src_file or dst_file is empty + :raises RuntimeError: If the file cannot be copied or replaced """ import gc @@ -853,17 +958,21 @@ def compute_p_3D(ps, ak, bk, shape_out): Compute the 3D pressure at layer midpoints. :param ps: Surface pressure (Pa) - :type ps: array [time, lat, lon] + :type ps: array [time, lat, lon] :param ak: Vertical coordinate pressure value (Pa) - :type ak: array [phalf] + :type ak: array [phalf] :param bk: Vertical coordinate sigma value (None) - :type bk: array [phalf] + :type bk: array [phalf] :param shape_out: Determines how to handle the dimensions of p_3D. If ``len(time) = 1`` (one timestep), ``p_3D`` is returned as [1, lev, lat, lon] as opposed to [lev, lat, lon] - :type shape_out: float + :type shape_out: float :return: ``p_3D`` The full 3D pressure array (Pa) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the pressure calculation fails """ p_3D = fms_press_calc(ps, ak, bk, lev_type="full") @@ -878,11 +987,14 @@ def compute_rho(p_3D, temp): Compute density. :param p_3D: Pressure (Pa) - :type p_3D: array [time, lev, lat, lon] + :type p_3D: array [time, lev, lat, lon] :param temp: Temperature (K) - :type temp: array [time, lev, lat, lon] + :type temp: array [time, lev, lat, lon] :return: Density (kg/m^3) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ return p_3D / (rgas*temp) @@ -892,20 +1004,26 @@ def compute_rho(p_3D, temp): def compute_xzTau(q, temp, lev, const, f_type): """ Compute the dust or ice extinction rate. + Adapted from Heavens et al. (2011) observations from MCS (JGR). + [Courtney Batterson, 2023] :param q: Dust or ice mass mixing ratio (ppm) - :type q: array [time, lev, lat, lon] + :type q: array [time, lev, lat, lon] :param temp: Temperature (K) - :type temp: array [time, lev, lat, lon] + :type temp: array [time, lev, lat, lon] :param lev: Vertical coordinate (e.g., pstd) (e.g., Pa) - :type lev: array [lev] + :type lev: array [lev] :param const: Dust or ice constant - :type const: array + :type const: array :param f_type: The FV3 file type: diurn, daily, or average - :type f_stype: str + :type f_stype: str :return: ``xzTau`` Dust or ice extinction rate (km-1) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the extinction rate calculation fails """ if f_type == "diurn": @@ -942,20 +1060,26 @@ def compute_xzTau(q, temp, lev, const, f_type): def compute_mmr(xTau, temp, lev, const, f_type): """ Compute the dust or ice mixing ratio. + Adapted from Heavens et al. (2011) observations from MCS (JGR). + [Courtney Batterson, 2023] :param xTau: Dust or ice extinction rate (km-1) - :type xTau: array [time, lev, lat, lon] + :type xTau: array [time, lev, lat, lon] :param temp: Temperature (K) - :type temp: array [time, lev, lat, lon] + :type temp: array [time, lev, lat, lon] :param lev: Vertical coordinate (e.g., pstd) (e.g., Pa) - :type lev: array [lev] + :type lev: array [lev] :param const: Dust or ice constant - :type const: array + :type const: array :param f_type: The FV3 file type: diurn, daily, or average - :type f_stype: str + :type f_stype: str :return: ``q``, Dust or ice mass mixing ratio (ppm) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the mixing ratio calculation fails """ if f_type == "diurn": @@ -992,15 +1116,20 @@ def compute_mmr(xTau, temp, lev, const, f_type): def compute_Vg_sed(xTau, nTau, T): """ Calculate the sedimentation rate of the dust. + [Courtney Batterson, 2023] :param xTau: Dust or ice MASS mixing ratio (ppm) - :type xTau: array [time, lev, lat, lon] + :type xTau: array [time, lev, lat, lon] :param nTau: Dust or ice NUMBER mixing ratio (None) - :type nTau: array [time, lev, lat, lon] + :type nTau: array [time, lev, lat, lon] :param T: Temperature (K) - :type T: array [time, lev, lat, lon] + :type T: array [time, lev, lat, lon] :return: ``Vg`` Dust sedimentation rate (m/s) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the sedimentation rate calculation fails """ r0 = ( @@ -1021,17 +1150,23 @@ def compute_Vg_sed(xTau, nTau, T): # ===================================================================== def compute_w_net(Vg, wvar): """ - Computes the net vertical wind, which is the vertical wind (w) - minus the sedimentation rate (``Vg_sed``):: + Computes the net vertical wind. + + w_net = vertical wind (w) - sedimentation rate (``Vg_sed``):: w_net = w - Vg_sed + [Courtney Batterson, 2023] + :param Vg: Dust sedimentation rate (m/s) - :type Vg: array [time, lev, lat, lon] + :type Vg: array [time, lev, lat, lon] :param wvar: Vertical wind (m/s) - :type wvar: array [time, lev, lat, lon] + :type wvar: array [time, lev, lat, lon] :return: `w_net` Net vertical wind speed (m/s) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ w_net = np.subtract(wvar, Vg) @@ -1044,15 +1179,18 @@ def compute_theta(p_3D, ps, T, f_type): Compute the potential temperature. :param p_3D: The full 3D pressure array (Pa) - :type p_3D: array [time, lev, lat, lon] + :type p_3D: array [time, lev, lat, lon] :param ps: Surface pressure (Pa) - :type ps: array [time, lat, lon] + :type ps: array [time, lat, lon] :param T: Temperature (K) - :type T: array [time, lev, lat, lon] + :type T: array [time, lev, lat, lon] :param f_type: The FV3 file type: diurn, daily, or average - :type f_type: str + :type f_type: str :return: Potential temperature (K) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ theta_exp = R / (M_co2*Cp) @@ -1086,11 +1224,18 @@ def compute_w(rho, omega): omega = -rho * g * w :param rho: Atmospheric density (kg/m^3) - :type rho: array [time, lev, lat, lon] + :type rho: array [time, lev, lat, lon] :param omega: Rate of change in pressure at layer midpoint (Pa/s) - :type omega: array [time, lev, lat, lon] + :type omega: array [time, lev, lat, lon] :return: vertical wind (m/s) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the vertical wind calculation fails + :raises ZeroDivisionError: If rho or omega is zero + :raises OverflowError: If the calculation results in an overflow + :raises Exception: If any other error occurs """ return -omega / (rho*g) @@ -1102,15 +1247,18 @@ def compute_zfull(ps, ak, bk, T): Calculate the altitude of the layer midpoints above ground level. :param ps: Surface pressure (Pa) - :type ps: array [time, lat, lon] + :type ps: array [time, lat, lon] :param ak: Vertical coordinate pressure value (Pa) - :type ak: array [phalf] + :type ak: array [phalf] :param bk: Vertical coordinate sigma value (None) - :type bk: array [phalf] + :type bk: array [phalf] :param T: Temperature (K) - :type T: array [time, lev, lat, lon] + :type T: array [time, lev, lat, lon] :return: ``zfull`` (m) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ zfull = fms_Z_calc( @@ -1130,15 +1278,18 @@ def compute_zhalf(ps, ak, bk, T): Calculate the altitude of the layer interfaces above ground level. :param ps: Surface pressure (Pa) - :type ps: array [time, lat, lon] + :type ps: array [time, lat, lon] :param ak: Vertical coordinate pressure value (Pa) - :type ak: array [phalf] + :type ak: array [phalf] :param bk: Vertical coordinate sigma value (None) - :type bk: array [phalf] + :type bk: array [phalf] :param T: Temperature (K) - :type T: array [time, lev, lat, lon] + :type T: array [time, lev, lat, lon] :return: ``zhalf`` (m) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ zhalf = fms_Z_calc( @@ -1155,8 +1306,9 @@ def compute_zhalf(ps, ak, bk, T): # ===================================================================== def compute_DZ_full_pstd(pstd, T, ftype="average"): """ - Calculate the thickness of a layer from the midpoint of the - standard pressure levels (``pstd``). + Calculate layer thickness. + + Computes from the midpoint of the standard pressure levels (``pstd``). In this context, ``pfull=pstd`` with the layer interfaces defined somewhere in between successive layers:: @@ -1171,13 +1323,18 @@ def compute_DZ_full_pstd(pstd, T, ftype="average"): / / / / :param pstd: Vertical coordinate (pstd; Pa) - :type pstd: array [lev] + :type pstd: array [lev] :param T: Temperature (K) - :type T: array [time, lev, lat, lon] + :type T: array [time, lev, lat, lon] :param f_type: The FV3 file type: diurn, daily, or average - :type f_stype: str + :type f_stype: str :return: DZ_full_pstd, Layer thicknesses (Pa) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the layer thickness calculation fails + :raises ZeroDivisionError: If the temperature is zero """ # Determine whether the lev dimension is located at i = 1 or i = 2 @@ -1230,11 +1387,14 @@ def compute_N(theta, zfull): Calculate the Brunt Vaisala freqency. :param theta: Potential temperature (K) - :type theta: array [time, lev, lat, lon] + :type theta: array [time, lev, lat, lon] :param zfull: Altitude above ground level at the layer midpoint (m) - :type zfull: array [time, lev, lat, lon] + :type zfull: array [time, lev, lat, lon] :return: ``N``, Brunt Vaisala freqency [rad/s] - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ # Differentiate theta w.r.t. zfull to obdain d(theta)/dz @@ -1254,13 +1414,17 @@ def compute_N(theta, zfull): def compute_Tco2(P_3D): """ Calculate the frost point of CO2. + Adapted from Fannale (1982) - Mars: The regolith-atmosphere cap system and climate change. Icarus. :param P_3D: The full 3D pressure array (Pa) - :type p_3D: array [time, lev, lat, lon] + :type p_3D: array [time, lev, lat, lon] :return: CO2 frost point [K] - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ # Set some constants @@ -1286,13 +1450,16 @@ def compute_scorer(N, ucomp, zfull): Calculate the Scorer wavelength. :param N: Brunt Vaisala freqency (rad/s) - :type N: float [time, lev, lat, lon] + :type N: float [time, lev, lat, lon] :param ucomp: Zonal wind (m/s) - :type ucomp: array [time, lev, lat, lon] + :type ucomp: array [time, lev, lat, lon] :param zfull: Altitude above ground level at the layer midpoint (m) - :type zfull: array [time, lev, lat, lon] + :type zfull: array [time, lev, lat, lon] :return: ``scorer_wl`` Scorer horizontal wavelength (m) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ # Differentiate U w.r.t. zfull TWICE to obdain d^2U/dz^2 @@ -1321,17 +1488,20 @@ def compute_DP_3D(ps, ak, bk, shape_out): Calculate the thickness of a layer in pressure units. :param ps: Surface pressure (Pa) - :type ps: array [time, lat, lon] + :type ps: array [time, lat, lon] :param ak: Vertical coordinate pressure value (Pa) - :type ak: array [phalf] + :type ak: array [phalf] :param bk: Vertical coordinate sigma value (None) - :type bk: array [phalf] + :type bk: array [phalf] :param shape_out: Determines how to handle the dimensions of DP_3D. If len(time) = 1 (one timestep), DP_3D is returned as [1, lev, lat, lon] as opposed to [lev, lat, lon] - :type shape_out: float + :type shape_out: float :return: ``DP`` Layer thickness in pressure units (Pa) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ # Get the 3D pressure field from fms_press_calc @@ -1354,17 +1524,20 @@ def compute_DZ_3D(ps, ak, bk, temp, shape_out): Calculate the thickness of a layer in altitude units. :param ps: Surface pressure (Pa) - :type ps: array [time, lat, lon] + :type ps: array [time, lat, lon] :param ak: Vertical coordinate pressure value (Pa) - :type ak: array [phalf] + :type ak: array [phalf] :param bk: Vertical coordinate sigma value (None) - :type bk: array [phalf] + :type bk: array [phalf] :param shape_out: Determines how to handle the dimensions of DZ_3D. If len(time) = 1 (one timestep), DZ_3D is returned as [1, lev, lat, lon] as opposed to [lev, lat, lon] - :type shape_out: float + :type shape_out: float :return: ``DZ`` Layer thickness in altitude units (m) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ # Get the 3D altitude field from fms_Z_calc @@ -1389,14 +1562,19 @@ def compute_DZ_3D(ps, ak, bk, temp, shape_out): # ===================================================================== def compute_Ep(temp): """ - Calculate wave potential energy:: + Calculate wave potential energy. + + Calculation:: Ep = 1/2 (g/N)^2 (temp'/temp)^2 :param temp: Temperature (K) - :type temp: array [time, lev, lat, lon] + :type temp: array [time, lev, lat, lon] :return: ``Ep`` Wave potential energy (J/kg) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ return 0.5 * g**2 * (zonal_detrend(temp) / (temp*N))**2 @@ -1405,16 +1583,21 @@ def compute_Ep(temp): # ===================================================================== def compute_Ek(ucomp, vcomp): """ - Calculate wave kinetic energ:: + Calculate wave kinetic energy + + Calculation:: Ek = 1/2 (u'**2+v'**2) :param ucomp: Zonal wind (m/s) - :type ucomp: array [time, lev, lat, lon] + :type ucomp: array [time, lev, lat, lon] :param vcomp: Meridional wind (m/s) - :type vcomp: array [time, lev, lat, lon] + :type vcomp: array [time, lev, lat, lon] :return: ``Ek`` Wave kinetic energy (J/kg) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ return 0.5 * (zonal_detrend(ucomp)**2 + zonal_detrend(vcomp)**2) @@ -1426,11 +1609,14 @@ def compute_MF(UVcomp, w): Calculate zonal or meridional momentum fluxes. :param UVcomp: Zonal or meridional wind (ucomp or vcomp)(m/s) - :type UVcomp: array + :type UVcomp: array :param w: Vertical wind (m/s) - :type w: array [time, lev, lat, lon] + :type w: array [time, lev, lat, lon] :return: ``u'w'`` or ``v'w'``, Zonal/meridional momentum flux (J/kg) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ return zonal_detrend(UVcomp) * zonal_detrend(w) @@ -1439,7 +1625,9 @@ def compute_MF(UVcomp, w): # ===================================================================== def compute_WMFF(MF, rho, lev, interp_type): """ - Calculate the zonal or meridional wave-mean flow forcing:: + Calculate the zonal or meridional wave-mean flow forcing. + + Calculation:: ax = -1/rho d(rho u'w')/dz ay = -1/rho d(rho v'w')/dz @@ -1454,16 +1642,21 @@ def compute_WMFF(MF, rho, lev, interp_type): [du/dz = (du/dp).(-rho*g)] > [du/dz = -rho*g * (du/dp)] :param MF: Zonal/meridional momentum flux (J/kg) - :type MF: array [time, lev, lat, lon] + :type MF: array [time, lev, lat, lon] :param rho: Atmospheric density (kg/m^3) - :type rho: array [time, lev, lat, lon] + :type rho: array [time, lev, lat, lon] :param lev: Array for the vertical grid (zagl, zstd, pstd, or pfull) - :type lev: array [lev] + :type lev: array [lev] :param interp_type: The vertical grid type (``zagl``, ``zstd``, ``pstd``, or ``pfull``) - :type interp_type: str + :type interp_type: str :return: The zonal or meridional wave-mean flow forcing (m/s2) - :rtype: array [time, lev, lat, lon] + :rtype: array [time, lev, lat, lon] + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the wave-mean flow forcing calculation fails + :raises ZeroDivisionError: If rho is zero """ # Differentiate the momentum flux (MF) @@ -1484,8 +1677,7 @@ def compute_WMFF(MF, rho, lev, interp_type): def check_dependencies(f, var, master_list, add_missing=True, dependency_chain=None): """ - Check if all dependencies (deps.) for a variable are present in the file, - and optionally try to add missing deps.. + Check for variable dependencies in a file, add missing dependencies. :param f: NetCDF file object :param var: Variable to check deps. for @@ -1493,6 +1685,8 @@ def check_dependencies(f, var, master_list, add_missing=True, :param add_missing: Whether to try adding missing deps. (default: True) :param dependency_chain: List of vars in the current dep. chain (for detecting cycles) :return: True if all deps. are present or successfully added, False otherwise + :raises RuntimeError: If the variable is not in the master list + :raises Exception: If any other error occurs """ # Initialize dependency chain if None @@ -1590,8 +1784,19 @@ def check_dependencies(f, var, master_list, add_missing=True, # ===================================================================== def check_variable_exists(var_name, file_vars): """ - Check if a variable exists in the file, considering alternative - naming conventions + Check if a variable exists in a file. + + Considers alternative naming conventions. + + :param var_name: Variable name to check + :type var_name: str + :param file_vars: Set of variable names in the file + :type file_vars: set + :return: True if the variable exists, False otherwise + :rtype: bool + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ if var_name in file_vars: @@ -1617,7 +1822,19 @@ def check_variable_exists(var_name, file_vars): # ===================================================================== def get_existing_var_name(var_name, file_vars): """ - Get the actual variable name that exists in the file + Get the actual variable name that exists in the file. + + Considers alternative naming conventions. + + :param var_name: Variable name to check + :type var_name: str + :param file_vars: Set of variable names in the file + :type file_vars: set + :return: Actual variable name in the file + :rtype: str + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs """ if var_name in file_vars: @@ -1643,12 +1860,21 @@ def get_existing_var_name(var_name, file_vars): # ===================================================================== def process_add_variables(file_name, add_list, master_list, debug=False): """ - Process the list of variables to add, handling dependencies appropriately. + Process a list of variables to add. + + Dependent variables are added in the correct order. + If a variable is already in the file, it is skipped. + If a variable cannot be added, an error message is printed. :param file_name: Input file path :param add_list: List of variables to add :param master_list: Dictionary of supported variables and their dependencies :param debug: Whether to show debug information + :type debug: bool + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the variable cannot be added """ # Create a topologically sorted list of variables to add @@ -1688,7 +1914,17 @@ def process_add_variables(file_name, add_list, master_list, debug=False): def add_with_dependencies(var): """ - Helper function to add a variable and its dependencies to the list + Helper function to add variable and dependencies to the add list. + + :param var: Variable to add + :type var: str + :return: True if the variable and its dependencies can be added, + False otherwise + :rtype: bool + :raises ValueError: If the input dimensions are not compatible + :raises TypeError: If the input types are not compatible + :raises Exception: If any other error occurs + :raises RuntimeError: If the variable cannot be added """ # Skip if already processed @@ -2044,6 +2280,54 @@ def add_with_dependencies(var): @debug_wrapper def main(): + """ + Main function for variable manipulations in NetCDF files. + + This function performs a sequence of operations on one or more + NetCDF files, as specified by command-line arguments. The operations + include removing variables, extracting variables, adding variables, + vertical differentiation, zonal detrending, opacity conversions, + column integration, and editing variable metadata or values. + + Workflow: + - Iterates over all input NetCDF files. + - For each file, performs the following operations as requested + by arguments: + * Remove specified variables and update the file. + * Extract specified variables into a new file. + * Add new variables using provided methods. + * Compute vertical derivatives of variables with respect to + height or pressure. + * Remove zonal mean (detrend) from specified variables. + * Convert variables between dp/dz and dz/dp representations. + * Perform column integration of variables. + * Edit variable metadata (name, long_name, units) or scale + values. + + Arguments: + args: Namespace + Parsed command-line arguments specifying which operations + to perform and their parameters. + master_list: list + List of available variables and their properties (used for + adding variables). + debug: bool + If True, prints detailed error messages and stack traces. + Notes: + - Handles both Unix and Windows file operations for safe file + replacement. + - Uses helper functions for NetCDF file manipulation, variable + existence checks, and error handling. + - Assumes global constants and utility functions (e.g., Dataset, + Ncdf, check_file_tape, etc.) are defined elsewhere. + - Uses global variables lev_T and lev_T_out for axis + manipulation in vertical operations. + + Raises: + Exceptions are caught and logged for each operation; files are + cleaned up on error. + """ + # Load all the .nc files file_list = [f.name for f in args.input_file] @@ -2060,6 +2344,11 @@ def main(): # Create a wrapper object to pass to check_file_tape class FileWrapper: def __init__(self, name): + """ + Initialize the FileWrapper with a file name. + :param name: Name of the file + :type name: str + """ self.name = name file_wrapper = FileWrapper(input_file)